diff --git a/src/g_level.cpp b/src/g_level.cpp index ff42656dac..fcf4906a58 100644 --- a/src/g_level.cpp +++ b/src/g_level.cpp @@ -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) diff --git a/src/namedef_custom.h b/src/namedef_custom.h index 2da881531d..f0cb451c60 100644 --- a/src/namedef_custom.h +++ b/src/namedef_custom.h @@ -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) diff --git a/src/playsim/actor.h b/src/playsim/actor.h index 25b51b8ef1..845e532820 100644 --- a/src/playsim/actor.h +++ b/src/playsim/actor.h @@ -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 diff --git a/src/playsim/p_interaction.cpp b/src/playsim/p_interaction.cpp index 88326acb53..ee35e71d2d 100644 --- a/src/playsim/p_interaction.cpp +++ b/src/playsim/p_interaction.cpp @@ -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); + } } } diff --git a/src/playsim/p_mobj.cpp b/src/playsim/p_mobj.cpp index aada9f88a5..f980c9a8db 100644 --- a/src/playsim/p_mobj.cpp +++ b/src/playsim/p_mobj.cpp @@ -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(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(NAME_Owner) != nullptr) + || (to->IsKindOf(NAME_Inventory) && to->PointerVar(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(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(); + 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); diff --git a/src/scripting/thingdef_properties.cpp b/src/scripting/thingdef_properties.cpp index fa7a5b13f3..bedd66cd37 100644 --- a/src/scripting/thingdef_properties.cpp +++ b/src/scripting/thingdef_properties.cpp @@ -1814,6 +1814,15 @@ DEFINE_CLASS_PROPERTY(playerclass, S, PowerMorph) defaults->PointerVar(NAME_PlayerClass) = FindClassTentative(str, RUNTIME_CLASS(AActor), bag.fromDecorate); } +//========================================================================== +// +//========================================================================== +DEFINE_CLASS_PROPERTY(monsterclass, S, PowerMorph) +{ + PROP_STRING_PARM(str, 0); + defaults->PointerVar(NAME_MonsterClass) = FindClassTentative(str, RUNTIME_CLASS(AActor), bag.fromDecorate); +} + //========================================================================== // //========================================================================== diff --git a/src/scripting/vmthunks_actors.cpp b/src/scripting/vmthunks_actors.cpp index 4879d9b3a2..6c52ef3c27 100644 --- a/src/scripting/vmthunks_actors.cpp +++ b/src/scripting/vmthunks_actors.cpp @@ -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) diff --git a/wadsrc/static/zscript/actors/actor.zs b/wadsrc/static/zscript/actors/actor.zs index 3fe7f18987..5326a61846 100644 --- a/wadsrc/static/zscript/actors/actor.zs +++ b/wadsrc/static/zscript/actors/actor.zs @@ -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. diff --git a/wadsrc/static/zscript/actors/heretic/hereticartifacts.zs b/wadsrc/static/zscript/actors/heretic/hereticartifacts.zs index d12bf2b198..3ea8c7a485 100644 --- a/wadsrc/static/zscript/actors/heretic/hereticartifacts.zs +++ b/wadsrc/static/zscript/actors/heretic/hereticartifacts.zs @@ -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)) { diff --git a/wadsrc/static/zscript/actors/inventory/powerups.zs b/wadsrc/static/zscript/actors/inventory/powerups.zs index 9c4903d011..381f26af04 100644 --- a/wadsrc/static/zscript/actors/inventory/powerups.zs +++ b/wadsrc/static/zscript/actors/inventory/powerups.zs @@ -1870,13 +1870,14 @@ class PowerReflection : Powerup //=========================================================================== // // PowerMorph +// Now works with monsters too! // //=========================================================================== class PowerMorph : Powerup { - Class PlayerClass; - Class MorphFlash, UnMorphFlash; + class PlayerClass; + class 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; } - - } - diff --git a/wadsrc/static/zscript/actors/morph.zs b/wadsrc/static/zscript/actors/morph.zs index 1dcfb31faf..b3a6c40863 100644 --- a/wadsrc/static/zscript/actors/morph.zs +++ b/wadsrc/static/zscript/actors/morph.zs @@ -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 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 playerclass, class monsterclass, int duration = 0, int style = 0, class morphflash = null, classunmorphflash = null) + virtual bool Morph(Actor activator, class playerClass, class monsterClass, int duration = 0, EMorphFlags style = 0, class morphFlash = "TeleportFog", class 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 type, int duration = 0, int style = 0, class morphflash = null, classunmorphflash = null) + bool A_Morph(class type, int duration = 0, EMorphFlags style = 0, class morphFlash = "TeleportFog", class unmorphFlash = "TeleportFog") { - if (self.player != null) - { - let playerclass = (class)(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)(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 spawntype, int duration, int style, Class enter_flash, Class exit_flash) + virtual bool MorphMonster(class spawnType, int duration, EMorphFlags style, class enterFlash = "TeleportFog", class 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)("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)("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 PlayerClass; + class 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); + } +} diff --git a/wadsrc/static/zscript/actors/player/player.zs b/wadsrc/static/zscript/actors/player/player.zs index 235b97c8da..ec020d2f32 100644 --- a/wadsrc/static/zscript/actors/player/player.zs +++ b/wadsrc/static/zscript/actors/player/player.zs @@ -40,7 +40,7 @@ class PlayerPawn : Actor double SideMove1, SideMove2; TextureID ScoreIcon; int SpawnMask; - Name MorphWeapon; + Name MorphWeapon; // This should really be a class 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 spawntype, int duration, int style, Class enter_flash = null, Class exit_flash = null) + deprecated("3.7", "MorphPlayer() should be used on a PlayerPawn object") bool MorphPlayer(PlayerInfo activator, class spawnType, int duration, EMorphFlags style, class enterFlash = "TeleportFog", class 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() diff --git a/wadsrc/static/zscript/actors/player/player_cheat.zs b/wadsrc/static/zscript/actors/player/player_cheat.zs index adacdfb522..c2d2581d94 100644 --- a/wadsrc/static/zscript/actors/player/player_cheat.zs +++ b/wadsrc/static/zscript/actors/player/player_cheat.zs @@ -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"); } diff --git a/wadsrc/static/zscript/actors/player/player_morph.zs b/wadsrc/static/zscript/actors/player/player_morph.zs index 77df1ef269..a11878fff4 100644 --- a/wadsrc/static/zscript/actors/player/player_morph.zs +++ b/wadsrc/static/zscript/actors/player/player_morph.zs @@ -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 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 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 spawntype, int duration, int style, Class enter_flash = null, Class exit_flash = null) + virtual bool MorphPlayer(PlayerInfo activator, class spawnType, int duration, EMorphFlags style, class enterFlash = "TeleportFog", class 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 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 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 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 PlayerClass; - Class 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 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; - } - -} - diff --git a/wadsrc/static/zscript/actors/raven/artitele.zs b/wadsrc/static/zscript/actors/raven/artitele.zs index 042ae67dfc..38ead58373 100644 --- a/wadsrc/static/zscript/actors/raven/artitele.zs +++ b/wadsrc/static/zscript/actors/raven/artitele.zs @@ -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; }