Reworked Morphing

Removed StaticPointerSubstitution in favor of a much safer function that only changes select pointers. As a result the ability to properly modify morphing has been opened back up to ZScript. Many missing virtual callbacks were amended and MorphedDeath has been reworked to only be called back on an actual morphed death. MorphedMonster is no longer required to morph an Actor. CheckUnmorph virtual added that gets called back on morphed Actors. Fixed numerous bugs related to morph behavior.
This commit is contained in:
Boondorl 2023-12-31 21:41:03 -05:00 committed by Rachael Alexanderson
parent b469770ecc
commit 2c09a443b4
15 changed files with 711 additions and 686 deletions

View file

@ -1711,7 +1711,7 @@ int FLevelLocals::FinishTravel ()
pawn->flags2 &= ~MF2_BLASTED;
if (oldpawn != nullptr)
{
StaticPointerSubstitution (oldpawn, pawn);
PlayerPointerSubstitution (oldpawn, pawn);
oldpawn->Destroy();
}
if (pawndup != NULL)

View file

@ -145,6 +145,7 @@ xx(Reflection)
xx(CustomInventory)
xx(Inventory)
xx(StateProvider)
xx(ObtainInventory)
xx(CallTryPickup)
xx(QuestItem25)
xx(QuestItem28)
@ -465,6 +466,7 @@ xx(MonsterClass)
xx(MorphedMonster)
xx(Wi_NoAutostartMap)
xx(MorphFlags)
xx(Duration)
xx(MorphStyle)
xx(MorphFlash)

View file

@ -1735,8 +1735,8 @@ struct FTranslatedLineTarget
bool unlinked; // found by a trace that went through an unlinked portal.
};
void StaticPointerSubstitution(AActor* old, AActor* notOld);
void PlayerPointerSubstitution(AActor* oldPlayer, AActor* newPlayer);
int MorphPointerSubstitution(AActor* from, AActor* to);
#define S_FREETARGMOBJ 1

View file

@ -313,36 +313,38 @@ EXTERN_CVAR (Int, fraglimit)
void AActor::Die (AActor *source, AActor *inflictor, int dmgflags, FName MeansOfDeath)
{
// Handle possible unmorph on death
bool wasgibbed = (health < GetGibHealth());
// Check to see if unmorph Actors need to be killed as well. Originally this was always
// called but that puts an unnecessary burden on the modder to determine whether it's
// a valid call or not.
if (alternative != nullptr && !(flags & MF_UNMORPHED))
{
IFVIRTUAL(AActor, MorphedDeath)
{
AActor *realthis = NULL;
int realstyle = 0;
int realhealth = 0;
// Return values are no longer used to ensure things stay properly managed.
AActor* const realMo = alternative;
const int morphStyle = player != nullptr ? player->MorphStyle : IntVar(NAME_MorphFlags);
VMValue params[] = { this };
VMReturn returns[3];
returns[0].PointerAt((void**)&realthis);
returns[1].IntAt(&realstyle);
returns[2].IntAt(&realhealth);
VMCall(func, params, 1, returns, 3);
VMCall(func, params, 1, nullptr, 0);
if (realthis && !(realstyle & MORPH_UNDOBYDEATHSAVES))
// Always kill the dummy Actor if it didn't unmorph, otherwise checking the morph flags.
if (realMo != nullptr && (alternative != nullptr || !(morphStyle & MORPH_UNDOBYDEATHSAVES)))
{
if (wasgibbed)
{
int realgibhealth = realthis->GetGibHealth();
if (realthis->health >= realgibhealth)
{
realthis->health = realgibhealth - 1; // if morphed was gibbed, so must original be (where allowed)l
}
const int realGibHealth = realMo->GetGibHealth();
if (realMo->health >= realGibHealth)
realMo->health = realGibHealth - 1; // If morphed was gibbed, so must original be (where allowed).
}
else if (realMo->health > 0)
{
realMo->health = 0;
}
realthis->CallDie(source, inflictor, dmgflags, MeansOfDeath);
}
realMo->CallDie(source, inflictor, dmgflags, MeansOfDeath);
}
}
}

View file

@ -3767,6 +3767,22 @@ void AActor::Tick ()
static const uint8_t HereticScrollDirs[4] = { 6, 9, 1, 4 };
static const uint8_t HereticSpeedMuls[5] = { 5, 10, 25, 30, 35 };
// Check for Actor unmorphing, but only on the thing that is the morphed Actor.
// Players do their own special checking for this.
if (alternative != nullptr && !(flags & MF_UNMORPHED) && player == nullptr)
{
int res = false;
IFVIRTUAL(AActor, CheckUnmorph)
{
VMValue params[] = { this };
VMReturn ret[] = { &res };
VMCall(func, params, 1, ret, 1);
}
if (res)
return;
}
if (freezetics > 0)
{
freezetics--;
@ -5062,6 +5078,16 @@ void AActor::CallDeactivate(AActor *activator)
void AActor::OnDestroy ()
{
// If the Actor is leaving behind a premorph Actor, make sure it gets cleaned up as
// well so it's not stuck in the map.
if (alternative != nullptr && !(flags & MF_UNMORPHED))
{
alternative->ClearCounters();
alternative->alternative = nullptr;
alternative->Destroy();
alternative = nullptr;
}
// [ZZ] call destroy event hook.
// note that this differs from ThingSpawned in that you can actually override OnDestroy to avoid calling the hook.
// but you can't really do that without utterly breaking the game, so it's ok.
@ -5183,59 +5209,191 @@ extern bool demonew;
//==========================================================================
//
// This once was the main method for pointer cleanup, but
// nowadays its only use is swapping out PlayerPawns.
// This requires pointer fixing throughout all objects and a few
// global variables, but it only needs to look at pointers that
// can point to a player.
// This function is dangerous and only designed for swapping player pawns
// over to their new ones upon changing levels or respawning. It SHOULD NOT be
// used for anything else! Do not export this functionality as it's
// meant strictly for internal usage. Only swap pointers if the thing being swapped
// to is a type of the thing being swapped from.
//
//==========================================================================
void StaticPointerSubstitution(AActor* old, AActor* notOld)
void PlayerPointerSubstitution(AActor* oldPlayer, AActor* newPlayer)
{
DObject* probe;
size_t changed = 0;
int i;
if (old == nullptr) return;
// This is only allowed to replace players or swap out morphed monsters
if (!old->IsKindOf(NAME_PlayerPawn) || (notOld != nullptr && !notOld->IsKindOf(NAME_PlayerPawn)))
if (oldPlayer == nullptr || newPlayer == nullptr || oldPlayer == newPlayer
|| !oldPlayer->IsKindOf(NAME_PlayerPawn) || !newPlayer->IsKindOf(NAME_PlayerPawn))
{
if (notOld == nullptr) return;
if (!old->IsKindOf(NAME_MorphedMonster) && !notOld->IsKindOf(NAME_MorphedMonster)) return;
}
// Go through all objects.
i = 0; DObject* last = 0;
for (probe = GC::Root; probe != NULL; probe = probe->ObjNext)
{
i++;
changed += probe->PointerSubstitution(old, notOld);
last = probe;
return;
}
// Go through players.
for (i = 0; i < MAXPLAYERS; i++)
// Swap over the inventory.
auto func = dyn_cast<PFunction>(newPlayer->GetClass()->FindSymbol(NAME_ObtainInventory, true));
if (func)
{
if (playeringame[i])
{
AActor* replacement = notOld;
auto& p = players[i];
if (p.mo == old) p.mo = replacement, changed++;
if (p.poisoner.ForceGet() == old) p.poisoner = replacement, changed++;
if (p.attacker.ForceGet() == old) p.attacker = replacement, changed++;
if (p.camera.ForceGet() == old) p.camera = replacement, changed++;
if (p.ConversationNPC.ForceGet() == old) p.ConversationNPC = replacement, changed++;
if (p.ConversationPC.ForceGet() == old) p.ConversationPC = replacement, changed++;
}
VMValue params[] = { newPlayer, oldPlayer };
VMCall(func->Variants[0].Implementation, params, 2, nullptr, 0);
}
// Go through sectors. Only the level this actor belongs to is relevant.
for (auto& sec : old->Level->sectors)
// Go through player infos.
for (int i = 0; i < MAXPLAYERS; ++i)
{
if (sec.SoundTarget == old) sec.SoundTarget = notOld;
if (!oldPlayer->Level->PlayerInGame(i))
continue;
auto p = oldPlayer->Level->Players[i];
if (p->mo == oldPlayer)
p->mo = newPlayer;
if (p->poisoner == oldPlayer)
p->poisoner = newPlayer;
if (p->attacker == oldPlayer)
p->attacker = newPlayer;
if (p->camera == oldPlayer)
p->camera = newPlayer;
if (p->ConversationNPC == oldPlayer)
p->ConversationNPC = newPlayer;
if (p->ConversationPC == oldPlayer)
p->ConversationPC = newPlayer;
}
// Go through sectors.
for (auto& sec : oldPlayer->Level->sectors)
{
if (sec.SoundTarget == oldPlayer)
sec.SoundTarget = newPlayer;
}
// Update all the remaining object pointers. This is dangerous but needed to ensure
// everything functions correctly when respawning or changing levels.
for (DObject* probe = GC::Root; probe != nullptr; probe = probe->ObjNext)
probe->PointerSubstitution(oldPlayer, newPlayer);
}
//==========================================================================
//
// This function is much safer than PlayerPointerSubstition as it only truly
// swaps a few safe pointers. This has some extra barriers to it to allow
// Actors to freely morph into other Actors which is its main usage.
// Previously this used raw pointer substitutions but that's far too
// volatile to use with modder-provided information. It also allows morphing
// to be more extendable from ZScript.
//
//==========================================================================
int MorphPointerSubstitution(AActor* from, AActor* to)
{
// Special care is taken here to make sure things marked as a dummy Actor for a morphed thing aren't
// allowed to be changed into other things. Anything being morphed into that's considered a player
// is automatically out of the question to ensure modders aren't swapping clients around.
if (from == nullptr || to == nullptr || from == to || to->player != nullptr
|| (from->flags & MF_UNMORPHED) // Another thing's dummy Actor, unmorphing the wrong way, etc.
|| (from->alternative == nullptr && to->alternative != nullptr) // Morphing into something that's already morphed.
|| (from->alternative != nullptr && from->alternative != to)) // Only allow something morphed to unmorph.
{
return false;
}
const bool toIsPlayer = to->IsKindOf(NAME_PlayerPawn);
if (from->IsKindOf(NAME_PlayerPawn))
{
// Players are only allowed to turn into other valid player pawns. For
// valid pawns, make sure an actual player is changing into an empty one.
// Voodoo dolls aren't allowed to morph since that should be passed to
// the main player directly.
if (!toIsPlayer || from->player == nullptr || from->player->mo != from)
return false;
}
else if (toIsPlayer || from->player != nullptr
|| (from->IsKindOf(NAME_Inventory) && from->PointerVar<AActor>(NAME_Owner) != nullptr)
|| (to->IsKindOf(NAME_Inventory) && to->PointerVar<AActor>(NAME_Owner) != nullptr))
{
// Only allow items to be swapped around if they aren't currently owned. Also prevent non-players from
// turning into fake players.
return false;
}
// Since the check is good, move the inventory items over. This should always be done when
// morphing to emulate Heretic/Hexen's behavior since those stored the inventory in their
// player structs.
auto func = dyn_cast<PFunction>(to->GetClass()->FindSymbol(NAME_ObtainInventory, true));
if (func)
{
VMValue params[] = { to, from };
VMCall(func->Variants[0].Implementation, params, 2, nullptr, 0);
}
// Only change some gameplay-related pointers that we know we can safely swap to whatever
// new Actor class is present.
AActor* mo = nullptr;
auto it = from->Level->GetThinkerIterator<AActor>();
while ((mo = it.Next()) != nullptr)
{
if (mo->target == from)
mo->target = to;
if (mo->tracer == from)
mo->tracer = to;
if (mo->master == from)
mo->master = to;
if (mo->goal == from)
mo->goal = to;
if (mo->lastenemy == from)
mo->lastenemy = to;
if (mo->LastHeard == from)
mo->LastHeard = to;
if (mo->LastLookActor == from)
mo->LastLookActor = to;
if (mo->Poisoner == from)
mo->Poisoner = to;
}
// Go through player infos.
for (int i = 0; i < MAXPLAYERS; ++i)
{
if (!from->Level->PlayerInGame(i))
continue;
auto p = from->Level->Players[i];
if (p->mo == from)
p->mo = to;
if (p->poisoner == from)
p->poisoner = to;
if (p->attacker == from)
p->attacker = to;
if (p->camera == from)
p->camera = to;
if (p->ConversationNPC == from)
p->ConversationNPC = to;
if (p->ConversationPC == from)
p->ConversationPC = to;
}
// Go through sectors.
for (auto& sec : from->Level->sectors)
{
if (sec.SoundTarget == from)
sec.SoundTarget = to;
}
// Remaining maintenance related to morphing.
if (from->player != nullptr)
{
to->player = from->player;
from->player = nullptr;
}
if (from->alternative != nullptr)
{
to->flags &= ~MF_UNMORPHED;
to->alternative = from->alternative = nullptr;
}
else
{
from->flags |= MF_UNMORPHED;
from->alternative = to;
to->alternative = from;
}
return true;
}
void FLevelLocals::PlayerSpawnPickClass (int playernum)
@ -5508,7 +5666,7 @@ AActor *FLevelLocals::SpawnPlayer (FPlayerStart *mthing, int playernum, int flag
if (sec.SoundTarget == oldactor) sec.SoundTarget = nullptr;
}
StaticPointerSubstitution (oldactor, p->mo);
PlayerPointerSubstitution (oldactor, p->mo);
localEventManager->PlayerRespawned(PlayerNum(p));
Behaviors.StartTypedScripts (SCRIPT_Respawn, p->mo, true);

View file

@ -1814,6 +1814,15 @@ DEFINE_CLASS_PROPERTY(playerclass, S, PowerMorph)
defaults->PointerVar<PClassActor>(NAME_PlayerClass) = FindClassTentative(str, RUNTIME_CLASS(AActor), bag.fromDecorate);
}
//==========================================================================
//
//==========================================================================
DEFINE_CLASS_PROPERTY(monsterclass, S, PowerMorph)
{
PROP_STRING_PARM(str, 0);
defaults->PointerVar<PClassActor>(NAME_MonsterClass) = FindClassTentative(str, RUNTIME_CLASS(AActor), bag.fromDecorate);
}
//==========================================================================
//
//==========================================================================

View file

@ -1693,28 +1693,11 @@ DEFINE_ACTION_FUNCTION_NATIVE(AActor, A_BossDeath, A_BossDeath)
return 0;
}
DEFINE_ACTION_FUNCTION_NATIVE(AActor, Substitute, StaticPointerSubstitution)
DEFINE_ACTION_FUNCTION_NATIVE(AActor, MorphInto, MorphPointerSubstitution)
{
PARAM_SELF_PROLOGUE(AActor);
PARAM_OBJECT(replace, AActor);
StaticPointerSubstitution(self, replace);
return 0;
}
DEFINE_ACTION_FUNCTION_NATIVE(_PlayerPawn, Substitute, StaticPointerSubstitution)
{
PARAM_SELF_PROLOGUE(AActor);
PARAM_OBJECT(replace, AActor);
StaticPointerSubstitution(self, replace);
return 0;
}
DEFINE_ACTION_FUNCTION_NATIVE(_MorphedMonster, Substitute, StaticPointerSubstitution)
{
PARAM_SELF_PROLOGUE(AActor);
PARAM_OBJECT(replace, AActor);
StaticPointerSubstitution(self, replace);
return 0;
PARAM_OBJECT(to, AActor);
ACTION_RETURN_INT(MorphPointerSubstitution(self, to));
}
DEFINE_ACTION_FUNCTION_NATIVE(AActor, GetSpawnableType, P_GetSpawnableType)

View file

@ -501,7 +501,7 @@ class Actor : Thinker native
virtual native bool Slam(Actor victim);
virtual void Touch(Actor toucher) {}
virtual native void FallAndSink(double grav, double oldfloorz);
private native void Substitute(Actor replacement);
native bool MorphInto(Actor morph);
native ui void DisplayNameTag();
native clearscope void DisableLocalRendering(uint playerNum, bool disable);
native ui bool ShouldRenderLocally(); // Only clients get to check this, never the playsim.

View file

@ -71,7 +71,7 @@ Class ArtiTomeOfPower : PowerupGiver
Playerinfo p = Owner.player;
if (p && p.morphTics && (p.MorphStyle & MRF_UNDOBYTOMEOFPOWER))
{ // Attempt to undo chicken
if (!p.mo.UndoPlayerMorph (p, MRF_UNDOBYTOMEOFPOWER))
if (!p.mo.Unmorph (p.mo, MRF_UNDOBYTOMEOFPOWER))
{ // Failed
if (!(p.MorphStyle & MRF_FAILNOTELEFRAG))
{

View file

@ -1870,13 +1870,14 @@ class PowerReflection : Powerup
//===========================================================================
//
// PowerMorph
// Now works with monsters too!
//
//===========================================================================
class PowerMorph : Powerup
{
Class<PlayerPawn> PlayerClass;
Class<Actor> MorphFlash, UnMorphFlash;
class<PlayerPawn> PlayerClass;
class<Actor> MonsterClass, MorphFlash, UnmorphFlash;
int MorphStyle;
PlayerInfo MorphedPlayer;
@ -1895,19 +1896,19 @@ class PowerMorph : Powerup
{
Super.InitEffect();
if (Owner != null && Owner.player != null && PlayerClass != null)
if (!Owner)
return;
if (Owner.Morph(Owner, PlayerClass, MonsterClass, int.max, MorphStyle, MorphFlash, UnmorphFlash))
{
let realplayer = Owner.player; // Remember the identity of the player
if (realplayer.mo.MorphPlayer(realplayer, PlayerClass, 0x7fffffff/*INDEFINITELY*/, MorphStyle, MorphFlash, UnMorphFlash))
{
Owner = realplayer.mo; // Replace the new owner in our owner; safe because we are not attached to anything yet
bCreateCopyMoved = true; // Let the caller know the "real" owner has changed (to the morphed actor)
MorphedPlayer = realplayer; // Store the player identity (morphing clears the unmorphed actor's "player" field)
}
else // morph failed - give the caller an opportunity to fail the pickup completely
{
bInitEffectFailed = true; // Let the caller know that the activation failed (can fail the pickup if appropriate)
}
// Get the real owner; safe because we are not attached to anything yet.
Owner = Owner.Alternative;
bCreateCopyMoved = true; // Let the caller know the "real" owner has changed (to the morphed actor).
MorphedPlayer = Owner.player;
}
else
{
bInitEffectFailed = true; // Let the caller know that the activation failed (can fail the pickup if appropriate).
}
}
@ -1921,24 +1922,18 @@ class PowerMorph : Powerup
{
Super.EndEffect();
// Abort if owner already destroyed or unmorphed
if (Owner == null || MorphedPlayer == null || Owner.alternative == null)
{
// Abort if owner already destroyed or unmorphed.
if (!Owner || !Owner.Alternative)
return;
}
// Abort if owner is dead; their Die() method will
// take care of any required unmorphing on death.
if (MorphedPlayer.health <= 0)
{
if (MorphedPlayer ? MorphedPlayer.Health <= 0 : Owner.Health <= 0)
return;
}
int savedMorphTics = MorphedPlayer.morphTics;
MorphedPlayer.mo.UndoPlayerMorph (MorphedPlayer, 0, !!(MorphedPlayer.MorphStyle & MRF_UNDOALWAYS));
EMorphFlags mStyle = MorphedPlayer ? MorphedPlayer.MorphStyle : MorphStyle;
Owner.Unmorph(Owner, 0, mStyle & MRF_UNDOALWAYS);
MorphedPlayer = null;
}
}

View file

@ -23,8 +23,26 @@
extend class Actor
{
enum EPremorphProperty
{
MPROP_SOLID = 1 << 1,
MPROP_SHOOTABLE = 1 << 2,
MPROP_INVIS = 1 << 6,
}
int UnmorphTime;
EMorphFlags MorphFlags;
class<Actor> MorphExitFlash;
EPremorphProperty PremorphProperties;
// This function doesn't return anything anymore since allowing so would be too volatile
// for morphing management. Instead it's now a function that lets special actions occur
// when a morphed Actor dies.
virtual Actor, int, int MorphedDeath()
{
if (MorphFlags & MRF_UNDOBYDEATH)
Unmorph(self, force: MorphFlags & MRF_UNDOBYDEATHFORCED);
return null, 0, 0;
}
@ -40,16 +58,12 @@ extend class Actor
//
//===========================================================================
virtual bool Morph(Actor activator, class<PlayerPawn> playerclass, class<MorphedMonster> monsterclass, int duration = 0, int style = 0, class<Actor> morphflash = null, class<Actor>unmorphflash = null)
virtual bool Morph(Actor activator, class<PlayerPawn> playerClass, class<Actor> monsterClass, int duration = 0, EMorphFlags style = 0, class<Actor> morphFlash = "TeleportFog", class<Actor> unmorphFlash = "TeleportFog")
{
if (player != null && player.mo != null && playerclass != null)
{
return player.mo.MorphPlayer(activator? activator.player : null, playerclass, duration, style, morphflash, unmorphflash);
}
else
{
return MorphMonster(monsterclass, duration, style, morphflash, unmorphflash);
}
if (player)
return player.mo.MorphPlayer(activator ? activator.player : null, playerClass, duration, style, morphFlash, unmorphFlash);
return MorphMonster(monsterClass, duration, style, morphFlash, unmorphFlash);
}
//===========================================================================
@ -58,18 +72,9 @@ extend class Actor
//
//===========================================================================
bool A_Morph(class<Actor> type, int duration = 0, int style = 0, class<Actor> morphflash = null, class<Actor>unmorphflash = null)
bool A_Morph(class<Actor> type, int duration = 0, EMorphFlags style = 0, class<Actor> morphFlash = "TeleportFog", class<Actor> unmorphFlash = "TeleportFog")
{
if (self.player != null)
{
let playerclass = (class<PlayerPawn>)(type);
if (playerclass && self.player.mo != null) return player.mo.MorphPlayer(self.player, playerclass, duration, style, morphflash, unmorphflash);
}
else
{
return MorphMonster(type, duration, style, morphflash, unmorphflash);
}
return false;
return Morph(self, (class<PlayerPawn>)(type), type, duration, style, morphFlash, unmorphFlash);
}
//===========================================================================
@ -78,21 +83,18 @@ extend class Actor
//
//===========================================================================
virtual bool UnMorph(Actor activator, int flags, bool force)
virtual bool Unmorph(Actor activator, EMorphFlags flags = 0, bool force = false)
{
if (player)
{
return player.mo.UndoPlayerMorph(activator? activator.player : null, flags, force);
}
else
{
let morphed = MorphedMonster(self);
if (morphed)
return morphed.UndoMonsterMorph(force);
}
return false;
return player.mo.UndoPlayerMorph(activator ? activator.player : null, flags, force);
return UndoMonsterMorph(force);
}
virtual bool CheckUnmorph()
{
return UnmorphTime <= Level.Time && Unmorph(self);
}
//---------------------------------------------------------------------------
//
@ -102,57 +104,212 @@ extend class Actor
//
//---------------------------------------------------------------------------
virtual bool MorphMonster (Class<Actor> spawntype, int duration, int style, Class<Actor> enter_flash, Class<Actor> exit_flash)
virtual bool MorphMonster(class<Actor> spawnType, int duration, EMorphFlags style, class<Actor> enterFlash = "TeleportFog", class<Actor> exitFlash = "TeleportFog")
{
if (player || spawntype == NULL || bDontMorph || !bIsMonster || !(spawntype is 'MorphedMonster'))
if (player || !spawnType || bDontMorph || !bIsMonster)
return false;
Actor morphed = Spawn(spawnType, Pos, ALLOW_REPLACE);
if (!MorphInto(morphed))
{
morphed.ClearCounters();
morphed.Destroy();
return false;
}
let morphed = MorphedMonster(Spawn (spawntype, Pos, NO_REPLACE));
// [MC] Notify that we're just about to start the transfer.
PreMorph(morphed, false); // False: No longer the current.
morphed.PreMorph(self, true); // True: Becoming this actor.
Substitute (morphed);
if ((style & MRF_TRANSFERTRANSLATION) && !morphed.bDontTranslate)
{
morphed.Translation = Translation;
}
morphed.ChangeTid(tid);
ChangeTid(0);
morphed.Angle = Angle;
morphed.UnmorphedMe = self;
morphed.Alpha = Alpha;
morphed.RenderStyle = RenderStyle;
morphed.Score = Score;
morphed.UnmorphTime = level.time + ((duration) ? duration : DEFMORPHTICS) + random[morphmonst]();
morphed.MorphStyle = style;
morphed.MorphExitFlash = (exit_flash) ? exit_flash : (class<Actor>)("TeleportFog");
morphed.FlagsSave = bSolid * 2 + bShootable * 4 + bInvisible * 0x40; // The factors are for savegame compatibility
morphed.special = special;
morphed.args[0] = args[0];
morphed.args[1] = args[1];
morphed.args[2] = args[2];
morphed.args[3] = args[3];
morphed.args[4] = args[4];
morphed.CopyFriendliness (self, true);
morphed.bShadow |= bShadow;
morphed.bGhost |= bGhost;
special = 0;
bSolid = false;
bShootable = false;
bUnmorphed = true;
morphed.Score = Score;
morphed.ChangeTID(TID);
morphed.Special = Special;
for (int i; i < 5; ++i)
morphed.Args[i] = Args[i];
morphed.CopyFriendliness(self, true);
morphed.UnmorphTime = Level.Time + (duration ? duration : DEFMORPHTICS) + Random[morphmonst]();
morphed.MorphFlags = style;
morphed.MorphExitFlash = exitFlash;
morphed.PremorphProperties = (bSolid * MPROP_SOLID) | (bShootable * MPROP_SHOOTABLE) | (bInvisible * MPROP_INVIS);
// This is just here for backwards compatibility as MorphedMonster used to be required.
let morphMon = MorphedMonster(morphed);
if (morphMon)
{
morphMon.UnmorphedMe = morphMon.Alternative;
morphMon.FlagsSave = morphMon.PremorphProperties;
}
ChangeTID(0);
Special = 0;
bInvisible = true;
let eflash = Spawn(enter_flash ? enter_flash : (class<Actor>)("TeleportFog"), Pos + (0, 0, gameinfo.TELEFOGHEIGHT), ALLOW_REPLACE);
if (eflash)
eflash.target = morphed;
bSolid = bShootable = false;
PostMorph(morphed, false);
morphed.PostMorph(self, true);
if (enterFlash)
{
Actor fog = Spawn(enterFlash, morphed.Pos.PlusZ(GameInfo.TELEFOGHEIGHT), ALLOW_REPLACE);
if (fog)
fog.Target = morphed;
}
return true;
}
//----------------------------------------------------------------------------
//
// FUNC P_UndoMonsterMorph
//
// Returns true if the monster unmorphs.
//
//----------------------------------------------------------------------------
virtual bool UndoMonsterMorph(bool force = false)
{
if (!UnmorphTime || !Alternative || bStayMorphed || Alternative.bStayMorphed)
return false;
Alternative.SetOrigin(Pos, false);
if (!force && (PremorphProperties & MPROP_SOLID))
{
bool altSolid = Alternative.bSolid;
bool isSolid = bSolid;
bool isTouchy = bTouchy;
Alternative.bSolid = true;
bSolid = bTouchy = false;
bool res = Alternative.TestMobjLocation();
Alternative.bSolid = altSolid;
bSolid = isSolid;
bTouchy = isTouchy;
// Didn't fit.
if (!res)
{
UnmorphTime = Level.Time + 5*TICRATE; // Next try in 5 seconds.
return false;
}
}
Actor unmorphed = Alternative;
if (!MorphInto(unmorphed))
return false;
PreUnmorph(unmorphed, false);
unmorphed.PreUnmorph(self, true);
unmorphed.Angle = Angle;
unmorphed.bShadow = bShadow;
unmorphed.bGhost = bGhost;
unmorphed.bSolid = (PremorphProperties & MPROP_SOLID);
unmorphed.bShootable = (PremorphProperties & MPROP_SHOOTABLE);
unmorphed.bInvisible = (PremorphProperties & MPROP_INVIS);
unmorphed.Vel = Vel;
unmorphed.Score = Score;
unmorphed.ChangeTID(TID);
unmorphed.Special = Special;
for (int i; i < 5; ++i)
unmorphed.Args[i] = Args[i];
unmorphed.CopyFriendliness(self, true);
ChangeTID(0);
Special = 0;
bInvisible = true;
bSolid = bShootable = false;
PostUnmorph(unmorphed, false); // From is false here: Leaving the caller's body.
unmorphed.PostUnmorph(self, true); // True here: Entering this body from here.
if (MorphExitFlash)
{
Actor fog = Spawn(MorphExitFlash, unmorphed.Pos.PlusZ(GameInfo.TELEFOGHEIGHT), ALLOW_REPLACE);
if (fog)
fog.Target = unmorphed;
}
Destroy();
return true;
}
}
//===========================================================================
//
//
//
//===========================================================================
class MorphProjectile : Actor
{
class<PlayerPawn> PlayerClass;
class<Actor> MonsterClass, MorphFlash, UnmorphFlash;
int Duration, MorphStyle;
Default
{
Damage 1;
Projectile;
MorphProjectile.MorphFlash "TeleportFog";
MorphProjectile.UnmorphFlash "TeleportFog";
-ACTIVATEIMPACT
-ACTIVATEPCROSS
}
override int DoSpecialDamage(Actor victim, int dmg, Name dmgType)
{
victim.Morph(Target, PlayerClass, MonsterClass, Duration, MorphStyle, MorphFlash, UnmorphFlash);
return -1;
}
}
//===========================================================================
//
// This class is redundant as it's no longer necessary for monsters to
// morph but is kept here for compatibility. Its previous fields either exist
// in the base Actor now or are just a shell for the actual fields
// which either already existed and weren't used for some reason or needed a
// better name.
//
//===========================================================================
class MorphedMonster : Actor
{
Actor UnmorphedMe;
EMorphFlags MorphStyle;
EPremorphProperty FlagsSave;
Default
{
Monster;
+FLOORCLIP
-COUNTKILL
}
// Make sure changes to these are propogated correctly when unmorphing. This is only
// set for monsters since originally players and this class were the only ones
// that were considered valid for morphing.
override bool UndoMonsterMorph(bool force)
{
Alternative = UnmorphedMe;
MorphFlags = MorphStyle;
PremorphProperties = FlagsSave;
return super.UndoMonsterMorph(force);
}
}

View file

@ -40,7 +40,7 @@ class PlayerPawn : Actor
double SideMove1, SideMove2;
TextureID ScoreIcon;
int SpawnMask;
Name MorphWeapon;
Name MorphWeapon; // This should really be a class<Weapon> but it's too late to change now.
double AttackZOffset; // attack height, relative to player center
double UseRange; // [NS] Distance at which player can +use
double AirCapacity; // Multiplier for air supply underwater.
@ -1536,7 +1536,7 @@ class PlayerPawn : Actor
}
if (!--player.morphTics)
{ // Attempt to undo the chicken/pig
player.mo.UndoPlayerMorph(player, MRF_UNDOBYTIMEOUT);
Unmorph(self, MRF_UNDOBYTIMEOUT);
}
}
}
@ -2851,23 +2851,15 @@ struct PlayerInfo native play // self is what internally is known as player_t
native clearscope bool HasWeaponsInSlot(int slot) const;
// The actual implementation is on PlayerPawn where it can be overridden. Use that directly in the future.
deprecated("3.7", "MorphPlayer() should be used on a PlayerPawn object") bool MorphPlayer(playerinfo p, Class<PlayerPawn> spawntype, int duration, int style, Class<Actor> enter_flash = null, Class<Actor> exit_flash = null)
deprecated("3.7", "MorphPlayer() should be used on a PlayerPawn object") bool MorphPlayer(PlayerInfo activator, class<PlayerPawn> spawnType, int duration, EMorphFlags style, class<Actor> enterFlash = "TeleportFog", class<Actor> exitFlash = "TeleportFog")
{
if (mo != null)
{
return mo.MorphPlayer(p, spawntype, duration, style, enter_flash, exit_flash);
}
return false;
return mo ? mo.MorphPlayer(activator, spawnType, duration, style, enterFlash, exitFlash) : false;
}
// This somehow got its arguments mixed up. 'self' should have been the player to be unmorphed, not the activator
deprecated("3.7", "UndoPlayerMorph() should be used on a PlayerPawn object") bool UndoPlayerMorph(playerinfo player, int unmorphflag = 0, bool force = false)
deprecated("3.7", "UndoPlayerMorph() should be used on a PlayerPawn object") bool UndoPlayerMorph(PlayerInfo player, EMorphFlags unmorphFlags = 0, bool force = false)
{
if (player.mo != null)
{
return player.mo.UndoPlayerMorph(self, unmorphflag, force);
}
return false;
return player.mo ? player.mo.UndoPlayerMorph(self, unmorphFlags, force) : false;
}
deprecated("3.7", "DropWeapon() should be used on a PlayerPawn object") void DropWeapon()

View file

@ -436,16 +436,16 @@ extend class PlayerPawn
if (player.morphTics)
{
if (UndoPlayerMorph (player))
if (Unmorph (self))
{
if (!quickundo && oldclass != morphclass && MorphPlayer (player, morphclass, 0, style))
if (!quickundo && oldclass != morphclass && Morph(self, morphclass, null, 0, style))
{
return StringTable.Localize("$TXT_STRANGER");
}
return StringTable.Localize("$TXT_NOTSTRANGE");
}
}
else if (MorphPlayer (player, morphclass, 0, style))
else if (Morph (self, morphclass, null, 0, style))
{
return StringTable.Localize("$TXT_STRANGE");
}

View file

@ -1,26 +1,25 @@
extend class PlayerPawn
{
private native void Substitute(PlayerPawn replacement);
//===========================================================================
//
// EndAllPowerupEffects
// InitAllPowerupEffects
//
// Calls EndEffect() on every Powerup in the inventory list.
// Calls InitEffect() on every Powerup in the inventory list. Since these
// functions can be overridden it's safest to store what's next in the item
// list before calling it.
//
//===========================================================================
void InitAllPowerupEffects()
{
let item = Inv;
while (item != null)
for (Inventory item = Inv; item;)
{
Inventory next = item.Inv;
let power = Powerup(item);
if (power != null)
{
if (power)
power.InitEffect();
}
item = item.Inv;
item = next;
}
}
@ -34,15 +33,14 @@ extend class PlayerPawn
void EndAllPowerupEffects()
{
let item = Inv;
while (item != null)
for (Inventory item = Inv; item;)
{
Inventory next = item.Inv;
let power = Powerup(item);
if (power != null)
{
if (power)
power.EndEffect();
}
item = item.Inv;
item = next;
}
}
@ -52,46 +50,36 @@ extend class PlayerPawn
//
//===========================================================================
virtual void ActivateMorphWeapon ()
virtual void ActivateMorphWeapon()
{
class<Weapon> morphweaponcls = MorphWeapon;
player.PendingWeapon = WP_NOCHANGE;
if (player.ReadyWeapon != null)
if (player.ReadyWeapon)
{
let psp = player.GetPSprite(PSP_WEAPON);
if (psp)
{
psp.y = WEAPONTOP;
player.ReadyWeapon.ResetPSprite(psp);
}
psp.y = WEAPONTOP;
player.ReadyWeapon.ResetPSprite(psp);
}
if (morphweaponcls == null || !(morphweaponcls is 'Weapon'))
{ // No weapon at all while morphed!
class<Weapon> morphWeapCls = MorphWeapon;
if (!morphWeapCls)
{
player.ReadyWeapon = null;
}
else
{
player.ReadyWeapon = Weapon(FindInventory (morphweaponcls));
if (player.ReadyWeapon == null)
player.ReadyWeapon = Weapon(FindInventory(morphWeapCls));
if (!player.ReadyWeapon)
{
player.ReadyWeapon = Weapon(GiveInventoryType (morphweaponcls));
if (player.ReadyWeapon != null)
{
player.ReadyWeapon.GivenAsMorphWeapon = true; // flag is used only by new beastweap semantics in UndoPlayerMorph
}
}
if (player.ReadyWeapon != null)
{
player.SetPsprite(PSP_WEAPON, player.ReadyWeapon.GetReadyState());
player.ReadyWeapon = Weapon(GiveInventoryType(morphWeapCls));
if (player.ReadyWeapon)
player.ReadyWeapon.GivenAsMorphWeapon = true; // Flag is used only by new morphWeap semantics in UndoPlayerMorph
}
if (player.ReadyWeapon)
player.SetPSprite(PSP_WEAPON, player.ReadyWeapon.GetReadyState());
}
if (player.ReadyWeapon != null)
{
player.SetPsprite(PSP_FLASH, null);
}
if (player.ReadyWeapon)
player.SetPSprite(PSP_FLASH, null);
player.PendingWeapon = WP_NOCHANGE;
}
@ -107,125 +95,110 @@ extend class PlayerPawn
//
//---------------------------------------------------------------------------
virtual bool MorphPlayer(playerinfo activator, Class<PlayerPawn> spawntype, int duration, int style, Class<Actor> enter_flash = null, Class<Actor> exit_flash = null)
virtual bool MorphPlayer(PlayerInfo activator, class<PlayerPawn> spawnType, int duration, EMorphFlags style, class<Actor> enterFlash = "TeleportFog", class<Actor> exitFlash = "TeleportFog")
{
if (bDontMorph)
if (!spawnType || bDontMorph || player.Health <= 0 || (bInvulnerable && (player != activator || !(style & MRF_WHENINVULNERABLE))))
return false;
if (!duration)
duration = DEFMORPHTICS;
if (player.MorphTics)
{
return false;
}
if (bInvulnerable && (player != activator || !(style & MRF_WHENINVULNERABLE)))
{ // Immune when invulnerable unless this is a power we activated
return false;
}
if (player.morphTics)
{ // Player is already a beast
if ((GetClass() == spawntype) && bCanSuperMorph
&& (player.morphTics < (((duration) ? duration : DEFMORPHTICS) - TICRATE))
&& FindInventory('PowerWeaponLevel2', true) == null)
{ // Make a super chicken
GiveInventoryType ('PowerWeaponLevel2');
// Player is already a beast.
if (bCanSuperMorph && spawnType == GetClass()
&& player.MorphTics < duration - TICRATE
&& !FindInventory("PowerWeaponLevel2", true))
{
// Make a super chicken.
GiveInventoryType("PowerWeaponLevel2");
}
return false;
}
if (health <= 0)
{ // Dead players cannot morph
return false;
}
if (spawntype == null)
{
return false;
}
if (!(spawntype is 'PlayerPawn'))
{
return false;
}
if (spawntype == GetClass())
{
return false;
}
let morphed = PlayerPawn(Spawn (spawntype, Pos, NO_REPLACE));
if (spawnType == GetClass())
return false;
let morphed = PlayerPawn(Spawn(spawnType, Pos, NO_REPLACE));
if (!MorphInto(morphed))
{
morphed.Destroy();
return false;
}
// Use GetClass in the event someone actually allows replacements.
PreMorph(morphed, false);
morphed.PreMorph(self, true);
EndAllPowerupEffects();
Substitute(morphed);
morphed.EndAllPowerupEffects();
if ((style & MRF_TRANSFERTRANSLATION) && !morphed.bDontTranslate)
{
morphed.Translation = Translation;
}
if (tid != 0 && (style & MRF_NEWTIDBEHAVIOUR))
{
morphed.ChangeTid(tid);
ChangeTid(0);
}
morphed.Angle = Angle;
morphed.target = target;
morphed.tracer = tracer;
morphed.alternative = self;
morphed.Pitch = Pitch; // Allow pitch here since mouse look in GZDoom is far more common than Heretic/Hexen.
morphed.Target = Target;
morphed.Tracer = Tracer;
morphed.Master = Master;
morphed.FriendPlayer = FriendPlayer;
morphed.DesignatedTeam = DesignatedTeam;
morphed.Score = Score;
player.PremorphWeapon = player.ReadyWeapon;
morphed.special2 = bSolid * 2 + bShootable * 4 + bInvisible * 0x40; // The factors are for savegame compatibility
morphed.player = player;
if (morphed.ViewHeight > player.viewheight && player.deltaviewheight == 0)
{ // If the new view height is higher than the old one, start moving toward it.
player.deltaviewheight = player.GetDeltaViewHeight();
morphed.ScoreIcon = ScoreIcon;
morphed.Health = morphed.SpawnHealth();
if (TID && (style & MRF_NEWTIDBEHAVIOUR))
{
morphed.ChangeTid(TID);
ChangeTid(0);
}
// special2 is no longer used here since Actors now have a proper field for it.
morphed.PremorphProperties = (bSolid * MPROP_SOLID) | (bShootable * MPROP_SHOOTABLE) | (bInvisible * MPROP_INVIS);
morphed.bShadow |= bShadow;
morphed.bNoGravity |= bNoGravity;
morphed.bFly |= bFly;
morphed.bGhost |= bGhost;
if (enter_flash == null) enter_flash = 'TeleportFog';
let eflash = Spawn(enter_flash, Pos + (0, 0, gameinfo.telefogheight), ALLOW_REPLACE);
let p = player;
player = null;
alternative = morphed;
bSolid = false;
bShootable = false;
bUnmorphed = true;
bInvisible = true;
p.morphTics = (duration) ? duration : DEFMORPHTICS;
// [MH] Used by SBARINFO to speed up face drawing
p.MorphedPlayerClass = spawntype;
p.MorphStyle = style;
if (exit_flash == null) exit_flash = 'TeleportFog';
p.MorphExitFlash = exit_flash;
p.health = morphed.health;
p.mo = morphed;
p.vel = (0, 0);
morphed.ObtainInventory (self);
// Remove all armor
for (Inventory item = morphed.Inv; item != null; )
// Remove all armor.
for (Inventory item = morphed.Inv; item;)
{
let next = item.Inv;
if (item is 'Armor')
{
Inventory next = item.Inv;
if (item is "Armor")
item.DepleteOrDestroy();
}
item = next;
}
morphed.InitAllPowerupEffects();
morphed.ActivateMorphWeapon ();
if (p.camera == self) // can this happen?
{
p.camera = morphed;
}
// Players store their morph behavior into their PlayerInfo unlike regular Actors which use the
// morph properties. This is needed for backwards compatibility and to give the HUD info.
let p = morphed.player;
p.MorphTics = duration;
p.MorphedPlayerClass = spawnType;
p.MorphStyle = style;
p.MorphExitFlash = exitFlash;
p.PremorphWeapon = p.ReadyWeapon;
p.Health = morphed.Health;
p.Vel = (0.0, 0.0);
// If the new view height is higher than the old one, start moving toward it.
if (morphed.ViewHeight > p.ViewHeight && !p.DeltaViewHeight)
p.DeltaViewHeight = p.GetDeltaViewHeight();
bSolid = bShootable = false;
bInvisible = true;
morphed.ClearFOVInterpolation();
morphed.ScoreIcon = ScoreIcon; // [GRB]
if (eflash)
eflash.target = morphed;
morphed.InitAllPowerupEffects();
morphed.ActivateMorphWeapon();
PostMorph(morphed, false); // No longer the current body
morphed.PostMorph(self, true); // This is the current body
if (enterFlash)
{
Actor fog = Spawn(enterFlash, morphed.Pos.PlusZ(GameInfo.TelefogHeight), ALLOW_REPLACE);
if (fog)
fog.Target = morphed;
}
return true;
}
@ -235,400 +208,154 @@ extend class PlayerPawn
//
//----------------------------------------------------------------------------
virtual bool UndoPlayerMorph(playerinfo activator, int unmorphflag = 0, bool force = false)
virtual bool UndoPlayerMorph(PlayerInfo activator, EMorphFlags unmorphFlags = 0, bool force = false)
{
if (alternative == null)
if (!Alternative)
return false;
if (bInvulnerable
&& (player != activator || (!(player.MorphStyle & MRF_WHENINVULNERABLE) && !(unmorphFlags & MRF_STANDARDUNDOING))))
{
return false;
}
let player = self.player;
bool DeliberateUnmorphIsOkay = !!(MRF_STANDARDUNDOING & unmorphflag);
let alt = PlayerPawn(Alternative);
alt.SetOrigin(Pos, false);
// Test if there's room to unmorph.
if (!force && (special2 & MPROP_SOLID))
{
bool altSolid = alt.bSolid;
bool isSolid = bSolid;
if ((bInvulnerable) // If the player is invulnerable
&& ((player != activator) // and either did not decide to unmorph,
|| (!((player.MorphStyle & MRF_WHENINVULNERABLE) // or the morph style does not allow it
|| (DeliberateUnmorphIsOkay))))) // (but standard morph styles always allow it),
{ // Then the player is immune to the unmorph.
return false;
alt.bSolid = true;
bSolid = false;
bool res = alt.TestMobjLocation();
alt.bSolid = altSolid;
bSolid = isSolid;
if (!res)
{
player.MorphTics = 2 * TICRATE;
return false;
}
}
let altmo = PlayerPawn(alternative);
altmo.SetOrigin (Pos, false);
altmo.bSolid = true;
bSolid = false;
if (!force && !altmo.TestMobjLocation())
{ // Didn't fit
altmo.bSolid = false;
bSolid = true;
player.morphTics = 2*TICRATE;
if (!MorphInto(alt))
return false;
}
PreUnmorph(altmo, false); // This body's about to be left.
altmo.PreUnmorph(self, true); // This one's about to become current.
PreUnmorph(alt, false); // This body's about to be left.
alt.PreUnmorph(self, true); // This one's about to become current.
// No longer using tracer as morph storage. That is what 'alternative' is for.
// If the tracer has changed on the morph, change the original too.
altmo.target = target;
altmo.tracer = tracer;
self.player = null;
altmo.alternative = alternative = null;
alt.EndAllPowerupEffects();
// Remove the morph power if the morph is being undone prematurely.
for (Inventory item = Inv; item != null;)
for (Inventory item = alt.Inv; item;)
{
let next = item.Inv;
Inventory next = item.Inv;
if (item is "PowerMorph")
{
item.Destroy();
}
item = next;
}
EndAllPowerupEffects();
altmo.ObtainInventory (self);
Substitute(altmo);
if ((tid != 0) && (player.MorphStyle & MRF_NEWTIDBEHAVIOUR))
{
altmo.ChangeTid(tid);
}
altmo.Angle = Angle;
altmo.player = player;
altmo.reactiontime = 18;
altmo.bSolid = !!(special2 & 2);
altmo.bShootable = !!(special2 & 4);
altmo.bInvisible = !!(special2 & 0x40);
altmo.Vel = (0, 0, Vel.Z);
player.Vel = (0, 0);
altmo.floorz = floorz;
altmo.bShadow = bShadow;
altmo.bNoGravity = bNoGravity;
altmo.bGhost = bGhost;
altmo.bUnmorphed = false;
altmo.Score = Score;
altmo.InitAllPowerupEffects();
let exit_flash = player.MorphExitFlash;
bool correctweapon = !!(player.MorphStyle & MRF_LOSEACTUALWEAPON);
bool undobydeathsaves = !!(player.MorphStyle & MRF_UNDOBYDEATHSAVES);
alt.Angle = Angle;
alt.Pitch = Pitch;
alt.Target = Target;
alt.Tracer = Tracer;
alt.Master = Master;
alt.FriendPlayer = FriendPlayer;
alt.DesignatedTeam = DesignatedTeam;
alt.Score = Score;
alt.ScoreIcon = ScoreIcon;
alt.ReactionTime = 18;
alt.bSolid = (PremorphProperties & MPROP_SOLID);
alt.bShootable = (PremorphProperties & MPROP_SHOOTABLE);
alt.bInvisible = (PremorphProperties & MPROP_INVIS);
alt.bShadow = bShadow;
alt.bNoGravity = bNoGravity;
alt.bGhost = bGhost;
alt.bFly = bFly;
alt.Vel = (0.0, 0.0, Vel.Z);
player.morphTics = 0;
player.MorphedPlayerClass = null;
player.MorphStyle = 0;
player.MorphExitFlash = null;
player.viewheight = altmo.ViewHeight;
Inventory level2 = altmo.FindInventory("PowerWeaponLevel2", true);
if (level2 != null)
let p = alt.player;
class<Actor> exitFlash = p.MorphExitFlash;
EMorphFlags style = p.MorphStyle;
Weapon premorphWeap = p.PremorphWeapon;
if (TID && (style & MRF_NEWTIDBEHAVIOUR))
{
level2.Destroy ();
alt.ChangeTid(TID);
ChangeTID(0);
}
if ((player.health > 0) || undobydeathsaves)
{
player.health = altmo.health = altmo.SpawnHealth();
}
else // killed when morphed so stay dead
{
altmo.health = player.health;
}
p.MorphTics = 0;
p.MorphedPlayerClass = null;
p.MorphStyle = 0;
p.MorphExitFlash = null;
p.PremorphWeapon = null;
p.ViewHeight = alt.ViewHeight;
p.Vel = (0.0, 0.0);
if (p.Health > 0 || (style & MRF_UNDOBYDEATHSAVES))
p.Health = alt.Health = alt.SpawnHealth();
else
alt.Health = p.Health;
player.mo = altmo;
if (player.camera == self)
{
player.camera = altmo;
}
altmo.ClearFOVInterpolation();
Inventory level2 = alt.FindInventory("PowerWeaponLevel2", true);
if (level2)
level2.Destroy();
// [MH]
// If the player that was morphed is the one
// taking events, reset up the face, if any;
// this is only needed for old-skool skins
// and for the original DOOM status bar.
if (player == players[consoleplayer])
let morphWeap = p.ReadyWeapon;
if (premorphWeap)
{
if (face != 'None')
{
// Assume root-level base skin to begin with
let skinindex = 0;
let skin = player.GetSkin();
// If a custom skin was in use, then reload it
// or else the base skin for the player class.
if (skin >= PlayerClasses.Size () && skin < PlayerSkins.Size())
{
skinindex = skin;
}
else if (PlayerClasses.Size () > 1)
{
let whatami = altmo.GetClass();
for (int i = 0; i < PlayerClasses.Size (); ++i)
{
if (PlayerClasses[i].Type == whatami)
{
skinindex = i;
break;
}
}
}
}
}
Actor eflash = null;
if (exit_flash != null)
{
eflash = Spawn(exit_flash, Vec3Angle(20., altmo.Angle, gameinfo.telefogheight), ALLOW_REPLACE);
if (eflash) eflash.target = altmo;
}
WeaponSlots.SetupWeaponSlots(altmo); // Use original class's weapon slots.
let beastweap = player.ReadyWeapon;
if (player.PremorphWeapon != null)
{
player.PremorphWeapon.PostMorphWeapon ();
premorphWeap.PostMorphWeapon();
}
else
{
player.ReadyWeapon = player.PendingWeapon = null;
p.ReadyWeapon = null;
p.PendingWeapon = WP_NOCHANGE;
p.Refire = 0;
}
if (correctweapon)
{ // Better "lose morphed weapon" semantics
class<Actor> morphweaponcls = MorphWeapon;
if (morphweaponcls != null && morphweaponcls is 'Weapon')
{
let OriginalMorphWeapon = Weapon(altmo.FindInventory (morphweapon));
if ((OriginalMorphWeapon != null) && (OriginalMorphWeapon.GivenAsMorphWeapon))
{ // You don't get to keep your morphed weapon.
if (OriginalMorphWeapon.SisterWeapon != null)
{
OriginalMorphWeapon.SisterWeapon.Destroy ();
}
OriginalMorphWeapon.Destroy ();
}
}
}
else // old behaviour (not really useful now)
{ // Assumptions made here are no longer valid
if (beastweap != null)
{ // You don't get to keep your morphed weapon.
if (beastweap.SisterWeapon != null)
{
beastweap.SisterWeapon.Destroy ();
}
beastweap.Destroy ();
}
}
PostUnmorph(altmo, false); // This body is no longer current.
altmo.PostUnmorph(self, true); // altmo body is current.
Destroy ();
// Restore playerclass armor to its normal amount.
let hxarmor = HexenArmor(altmo.FindInventory('HexenArmor'));
if (hxarmor != null)
if (style & MRF_LOSEACTUALWEAPON)
{
hxarmor.Slots[4] = altmo.HexenArmor[0];
// Improved "lose morph weapon" semantics.
class<Weapon> morphWeapCls = MorphWeapon;
if (morphWeapCls)
{
let originalMorphWeapon = Weapon(alt.FindInventory(morphWeapCls));
if (originalMorphWeapon && originalMorphWeapon.GivenAsMorphWeapon)
originalMorphWeapon.Destroy();
}
}
else if (morphWeap) // Old behaviour (not really useful now).
{
morphWeap.Destroy();
}
// Reset the base AC of the player's Hexen armor back to its default.
let hexArmor = HexenArmor(alt.FindInventory("HexenArmor"));
if (hexArmor)
hexArmor.Slots[4] = alt.HexenArmor[0];
alt.ClearFOVInterpolation();
alt.InitAllPowerupEffects();
bInvisible = true;
bSolid = bShootable = false;
PostUnmorph(alt, false); // This body is no longer current.
alt.PostUnmorph(self, true); // altmo body is current.
if (exitFlash)
{
Actor fog = Spawn(exitFlash, alt.Vec3Angle(20.0, alt.Angle, GameInfo.TelefogHeight), ALLOW_REPLACE);
if (fog)
fog.Target = alt;
}
Destroy();
return true;
}
//===========================================================================
//
//
//
//===========================================================================
override Actor, int, int MorphedDeath()
{
// Voodoo dolls should not unmorph the real player here.
if (player && (player.mo == self) &&
(player.morphTics) &&
(player.MorphStyle & MRF_UNDOBYDEATH) &&
(alternative))
{
Actor realme = alternative;
int realstyle = player.MorphStyle;
int realhealth = health;
if (UndoPlayerMorph(player, 0, !!(player.MorphStyle & MRF_UNDOBYDEATHFORCED)))
{
return realme, realstyle, realhealth;
}
}
return null, 0, 0;
}
}
//===========================================================================
//
//
//
//===========================================================================
class MorphProjectile : Actor
{
Class<PlayerPawn> PlayerClass;
Class<Actor> MonsterClass, MorphFlash, UnMorphFlash;
int Duration, MorphStyle;
Default
{
Damage 1;
Projectile;
-ACTIVATEIMPACT
-ACTIVATEPCROSS
}
override int DoSpecialDamage (Actor target, int damage, Name damagetype)
{
if (target.player)
{
// Voodoo dolls forward this to the real player
target.player.mo.MorphPlayer (NULL, PlayerClass, Duration, MorphStyle, MorphFlash, UnMorphFlash);
}
else
{
target.MorphMonster (MonsterClass, Duration, MorphStyle, MorphFlash, UnMorphFlash);
}
return -1;
}
}
//===========================================================================
//
//
//
//===========================================================================
class MorphedMonster : Actor
{
Actor UnmorphedMe;
int UnmorphTime, MorphStyle;
Class<Actor> MorphExitFlash;
int FlagsSave;
Default
{
Monster;
-COUNTKILL
+FLOORCLIP
}
private native void Substitute(Actor replacement);
override void OnDestroy ()
{
if (UnmorphedMe != NULL)
{
UnmorphedMe.Destroy ();
}
Super.OnDestroy();
}
override void Die (Actor source, Actor inflictor, int dmgflags, Name MeansOfDeath)
{
Super.Die (source, inflictor, dmgflags, MeansOfDeath);
if (UnmorphedMe != NULL && UnmorphedMe.bUnmorphed)
{
UnmorphedMe.health = health;
UnmorphedMe.Die (source, inflictor, dmgflags, MeansOfDeath);
}
}
override void Tick ()
{
if (UnmorphTime > level.time || !UndoMonsterMorph())
{
Super.Tick();
}
}
//----------------------------------------------------------------------------
//
// FUNC P_UndoMonsterMorph
//
// Returns true if the monster unmorphs.
//
//----------------------------------------------------------------------------
virtual bool UndoMonsterMorph(bool force = false)
{
if (UnmorphTime == 0 || UnmorphedMe == NULL || bStayMorphed || UnmorphedMe.bStayMorphed)
{
return false;
}
let unmorphed = UnmorphedMe;
unmorphed.SetOrigin (Pos, false);
unmorphed.bSolid = true;
bSolid = false;
bool save = bTouchy;
bTouchy = false;
if (!force && !unmorphed.TestMobjLocation ())
{ // Didn't fit
unmorphed.bSolid = false;
bSolid = true;
bTouchy = save;
UnmorphTime = level.time + 5*TICRATE; // Next try in 5 seconds
return false;
}
PreUnmorph(unmorphed, false);
unmorphed.PreUnmorph(self, true);
unmorphed.Angle = Angle;
unmorphed.target = target;
unmorphed.bShadow = bShadow;
unmorphed.bGhost = bGhost;
unmorphed.bSolid = !!(flagssave & 2);
unmorphed.bShootable = !!(flagssave & 4);
unmorphed.bInvisible = !!(flagssave & 0x40);
unmorphed.health = unmorphed.SpawnHealth();
unmorphed.Vel = Vel;
unmorphed.ChangeTid(tid);
unmorphed.special = special;
unmorphed.Score = Score;
unmorphed.args[0] = args[0];
unmorphed.args[1] = args[1];
unmorphed.args[2] = args[2];
unmorphed.args[3] = args[3];
unmorphed.args[4] = args[4];
unmorphed.CopyFriendliness (self, true);
unmorphed.bUnmorphed = false;
PostUnmorph(unmorphed, false); // From is false here: Leaving the caller's body.
unmorphed.PostUnmorph(self, true); // True here: Entering this body from here.
UnmorphedMe = NULL;
Substitute(unmorphed);
Destroy ();
let eflash = Spawn(MorphExitFlash, Pos + (0, 0, gameinfo.TELEFOGHEIGHT), ALLOW_REPLACE);
if (eflash)
eflash.target = unmorphed;
return true;
}
//===========================================================================
//
//
//
//===========================================================================
override Actor, int, int MorphedDeath()
{
let realme = UnmorphedMe;
if (realme != NULL)
{
if ((UnmorphTime) &&
(MorphStyle & MRF_UNDOBYDEATH))
{
int realstyle = MorphStyle;
int realhealth = health;
if (UndoMonsterMorph(!!(MorphStyle & MRF_UNDOBYDEATHFORCED)))
{
return realme, realstyle, realhealth;
}
}
if (realme.bBossDeath)
{
realme.health = 0; // make sure that A_BossDeath considers it dead.
realme.A_BossDeath();
}
}
return null, 0, 0;
}
}

View file

@ -43,7 +43,7 @@ class ArtiTeleport : Inventory
Playerinfo p = Owner.player;
if (p && p.morphTics && (p.MorphStyle & MRF_UNDOBYCHAOSDEVICE))
{ // Teleporting away will undo any morph effects (pig)
if (!p.mo.UndoPlayerMorph (p, MRF_UNDOBYCHAOSDEVICE) && (p.MorphStyle & MRF_FAILNOLAUGH))
if (!p.mo.Unmorph(p.mo, MRF_UNDOBYCHAOSDEVICE) && (p.MorphStyle & MRF_FAILNOLAUGH))
{
canlaugh = false;
}