Reapply "Improvements to death and cheat handling"

This reverts commit c7bba2a126.
This commit is contained in:
Boondorl 2024-04-17 19:55:08 -04:00 committed by Rachael Alexanderson
parent 949cd5b746
commit bcd6c6170e
No known key found for this signature in database
GPG key ID: 26A8ACCE97115EE0
25 changed files with 887 additions and 758 deletions

View file

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

View file

@ -479,7 +479,7 @@ FGameTexture *FMugShot::GetFace(player_t *player, const char *default_face, int
if (CurrentState != NULL)
{
int skin = player->userinfo.GetSkin();
const char *skin_face = (stateflags & FMugShot::CUSTOM) ? nullptr : (player->morphTics ? (GetDefaultByType(player->MorphedPlayerClass))->NameVar(NAME_Face).GetChars() : Skins[skin].Face.GetChars());
const char *skin_face = (stateflags & FMugShot::CUSTOM) ? nullptr : (player->mo->alternative != nullptr ? (GetDefaultByType(player->MorphedPlayerClass))->NameVar(NAME_Face).GetChars() : Skins[skin].Face.GetChars());
return CurrentState->GetCurrentFrameTexture(default_face, skin_face, level, angle);
}
return NULL;

View file

@ -626,10 +626,13 @@ public:
double plyrdmgfact = Pawn->DamageFactor;
Pawn->DamageFactor = 1.;
P_DamageMobj (Pawn, Pawn, Pawn, TELEFRAG_DAMAGE, NAME_Suicide);
Pawn->DamageFactor = plyrdmgfact;
if (Pawn->health <= 0)
if (Pawn != nullptr)
{
Pawn->flags &= ~MF_SHOOTABLE;
Pawn->DamageFactor = plyrdmgfact;
if (Pawn->health <= 0)
{
Pawn->flags &= ~MF_SHOOTABLE;
}
}
Destroy();
}

View file

@ -1138,7 +1138,7 @@ void FLevelLocals::UnSnapshotLevel(bool hubLoad)
// If this isn't the unmorphed original copy of a player, destroy it, because it's extra.
for (i = 0; i < MAXPLAYERS; ++i)
{
if (PlayerInGame(i) && Players[i]->morphTics && Players[i]->mo->alternative == pawn)
if (PlayerInGame(i) && Players[i]->mo->alternative == pawn)
{
break;
}

View file

@ -37,7 +37,7 @@ bool P_MorphActor(AActor *activator, AActor *victim, PClassActor *ptype, PClassA
bool P_UnmorphActor(AActor *activator, AActor *morphed, int flags, bool force)
{
IFVIRTUALPTR(morphed, AActor, UnMorph)
IFVIRTUALPTR(morphed, AActor, Unmorph)
{
VMValue params[] = { morphed, activator, flags, force };
int retval;

View file

@ -26,6 +26,8 @@ enum
MORPH_UNDOBYTIMEOUT = 0x00001000, // Player unmorphs once countdown expires
MORPH_UNDOALWAYS = 0x00002000, // Powerups must always unmorph, no matter what.
MORPH_TRANSFERTRANSLATION = 0x00004000, // Transfer translation from the original actor to the morphed one
MORPH_KEEPARMOR = 0x00008000, // Don't lose current armor value when morphing.
MORPH_IGNOREINVULN = 0x00010000, // Completely ignore invulnerability status on players.
MORPH_STANDARDUNDOING = MORPH_UNDOBYTOMEOFPOWER | MORPH_UNDOBYCHAOSDEVICE | MORPH_UNDOBYTIMEOUT,
};

View file

@ -1747,8 +1747,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, bool removeOld);
int MorphPointerSubstitution(AActor* from, AActor* to);
#define S_FREETARGMOBJ 1

View file

@ -3139,7 +3139,7 @@ DEFINE_ACTION_FUNCTION(AActor, A_Pain)
PARAM_SELF_PROLOGUE(AActor);
// [RH] Vary player pain sounds depending on health (ala Quake2)
if (self->player && self->player->morphTics == 0)
if (self->player && self->alternative == nullptr)
{
const char *pain_amount;
FSoundID sfx_id = NO_SOUND;

View file

@ -313,36 +313,54 @@ 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;
int morphStyle = 0;
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);
if (realthis && !(realstyle & MORPH_UNDOBYDEATHSAVES))
{
IFVM(Actor, GetMorphStyle)
{
VMReturn ret[] = { &morphStyle };
VMCall(func, params, 1, ret, 1);
}
}
VMCall(func, params, 1, nullptr, 0);
// Kill the dummy Actor if it didn't unmorph, otherwise checking the morph flags. Player pawns need
// to stay, otherwise they won't respawn correctly.
if (realMo != nullptr && !(realMo->flags6 & MF6_KILLED)
&& ((alternative != nullptr && player == 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);
}
// Pass appropriate damage information along when it's confirmed to die.
realMo->DamageTypeReceived = DamageTypeReceived;
realMo->DamageType = DamageType;
realMo->special1 = special1;
realMo->CallDie(source, inflictor, dmgflags, MeansOfDeath);
}
}
}
@ -458,7 +476,7 @@ void AActor::Die (AActor *source, AActor *inflictor, int dmgflags, FName MeansOf
++source->player->spreecount;
}
if (source->player->morphTics)
if (source->alternative != nullptr)
{ // Make a super chicken
source->GiveInventoryType (PClass::FindActor(NAME_PowerWeaponLevel2));
}
@ -1329,7 +1347,7 @@ static int DamageMobj (AActor *target, AActor *inflictor, AActor *source, int da
if (damage >= player->health && !telefragDamage
&& (G_SkillProperty(SKILLP_AutoUseHealth) || deathmatch)
&& !player->morphTics)
&& target->alternative == nullptr)
{ // Try to use some inventory health
P_AutoUseHealth (player, damage - player->health + 1);
}
@ -1463,7 +1481,7 @@ static int DamageMobj (AActor *target, AActor *inflictor, AActor *source, int da
// check for special fire damage or ice damage deaths
if (mod == NAME_Fire)
{
if (player && !player->morphTics)
if (player && target->alternative == nullptr)
{ // Check for flame death
if (!inflictor ||
((target->health > -50) && (damage > 25)) ||
@ -1797,7 +1815,7 @@ void P_PoisonDamage (player_t *player, AActor *source, int damage, bool playPain
}
if (damage >= player->health
&& (G_SkillProperty(SKILLP_AutoUseHealth) || deathmatch)
&& !player->morphTics)
&& target->alternative == nullptr)
{ // Try to use some inventory health
P_AutoUseHealth(player, damage - player->health+1);
}
@ -1827,7 +1845,7 @@ void P_PoisonDamage (player_t *player, AActor *source, int damage, bool playPain
else
{
target->special1 = damage;
if (player && !player->morphTics)
if (player && target->alternative == nullptr)
{ // Check for flame death
if ((player->poisontype == NAME_Fire) && (target->health > -50) && (damage > 25))
{

View file

@ -819,7 +819,7 @@ int P_GetRealMaxHealth(AActor *actor, int max)
{
max = actor->GetMaxHealth(true);
// [MH] First step in predictable generic morph effects
if (player->morphTics)
if (actor->alternative != nullptr)
{
if (player->MorphStyle & MORPH_FULLHEALTH)
{
@ -841,7 +841,7 @@ int P_GetRealMaxHealth(AActor *actor, int max)
else
{
// Bonus health should be added on top of the item's limit.
if (player->morphTics == 0 || (player->MorphStyle & MORPH_ADDSTAMINA))
if (actor->alternative == nullptr || (player->MorphStyle & MORPH_ADDSTAMINA))
{
max += actor->IntVar(NAME_BonusHealth);
}
@ -3788,6 +3788,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--;
@ -5082,6 +5098,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.
@ -5203,59 +5229,157 @@ 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 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.
//
//==========================================================================
void StaticPointerSubstitution(AActor* old, AActor* notOld)
void PlayerPointerSubstitution(AActor* oldPlayer, AActor* newPlayer, bool removeOld)
{
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, true);
last = probe;
return;
}
// Go through players.
for (i = 0; i < MAXPLAYERS; i++)
// Go through player infos.
for (int i = 0; i < MAXPLAYERS; ++i)
{
if (playeringame[i])
{
AActor* replacement = notOld;
auto& p = players[i];
if (!oldPlayer->Level->PlayerInGame(i))
continue;
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++;
}
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. Only the level this actor belongs to is relevant.
for (auto& sec : old->Level->sectors)
// Go through sectors.
for (auto& sec : oldPlayer->Level->sectors)
{
if (sec.SoundTarget == old) sec.SoundTarget = notOld;
if (sec.SoundTarget == oldPlayer)
sec.SoundTarget = newPlayer;
}
// Update all the remaining object pointers.
for (DObject* probe = GC::Root; probe != nullptr; probe = probe->ObjNext)
probe->PointerSubstitution(oldPlayer, newPlayer, removeOld);
}
//==========================================================================
//
// This has some extra barriers compared to PlayerPointerSubstitution to allow
// Actors to freely morph into other Actors which is its main usage.
// 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.
IFVM(Actor, ObtainInventory)
{
VMValue params[] = { to, from };
VMCall(func, params, 2, nullptr, 0);
}
// 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;
}
// Replace any object pointers that are safe to swap around.
for (DObject* probe = GC::Root; probe != nullptr; probe = probe->ObjNext)
probe->PointerSubstitution(from, to, false);
// 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)
@ -5529,7 +5653,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, false);
localEventManager->PlayerRespawned(PlayerNum(p));
Behaviors.StartTypedScripts (SCRIPT_Respawn, p->mo, true);

View file

@ -693,7 +693,7 @@ bool player_t::Resurrect()
P_BringUpWeapon(this);
}
if (morphTics)
if (mo->alternative != nullptr)
{
P_UnmorphActor(mo, mo);
}
@ -1172,7 +1172,7 @@ void P_CheckEnvironment(player_t *player)
P_PlayerOnSpecialFlat(player, P_GetThingFloorType(player->mo));
}
if (player->mo->Vel.Z <= -player->mo->FloatVar(NAME_FallingScreamMinSpeed) &&
player->mo->Vel.Z >= -player->mo->FloatVar(NAME_FallingScreamMaxSpeed) && !player->morphTics &&
player->mo->Vel.Z >= -player->mo->FloatVar(NAME_FallingScreamMaxSpeed) && player->mo->alternative == nullptr &&
player->mo->waterlevel == 0)
{
auto id = S_FindSkinnedSound(player->mo, S_FindSound("*falling"));

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

@ -504,7 +504,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.
@ -1410,7 +1410,7 @@ class Actor : Thinker native
bool grunted;
// [RH] only make noise if alive
if (self.health > 0 && self.player.morphTics == 0)
if (self.health > 0 && !Alternative)
{
grunted = false;
// Why should this number vary by gravity?

View file

@ -221,7 +221,7 @@ class ChickenPlayer : PlayerPawn
pspr.y = WEAPONTOP + player.chickenPeck / 2;
}
}
if (player.morphTics & 15)
if ((player.MorphTics ? player.MorphTics : Random[ChickenPlayerThink]()) & 15)
{
return;
}

View file

@ -66,28 +66,26 @@ Class ArtiTomeOfPower : PowerupGiver
Loop;
}
override bool Use (bool pickup)
override bool Use(bool pickup)
{
Playerinfo p = Owner.player;
if (p && p.morphTics && (p.MorphStyle & MRF_UNDOBYTOMEOFPOWER))
{ // Attempt to undo chicken
if (!p.mo.UndoPlayerMorph (p, MRF_UNDOBYTOMEOFPOWER))
{ // Failed
if (!(p.MorphStyle & MRF_FAILNOTELEFRAG))
{
Owner.DamageMobj (null, null, TELEFRAG_DAMAGE, 'Telefrag');
}
EMorphFlags mStyle = Owner.GetMorphStyle();
if (Owner.Alternative && (mStyle & MRF_UNDOBYTOMEOFPOWER))
{
// Attempt to undo chicken.
if (!Owner.Unmorph(Owner, MRF_UNDOBYTOMEOFPOWER))
{
if (!(mStyle & MRF_FAILNOTELEFRAG))
Owner.DamageMobj(null, null, TELEFRAG_DAMAGE, 'Telefrag');
}
else
{ // Succeeded
Owner.A_StartSound ("*evillaugh", CHAN_VOICE);
else if (Owner.player)
{
Owner.A_StartSound("*evillaugh", CHAN_VOICE);
}
return true;
}
else
{
return Super.Use (pickup);
}
return Super.Use(pickup);
}
}

View file

@ -145,7 +145,7 @@ class PigPlayer : PlayerPawn
override void MorphPlayerThink ()
{
if (player.morphTics & 15)
if ((player.MorphTics ? player.MorphTics : Random[PigPlayerThink]()) & 15)
{
return;
}

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,16 @@ 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 (Owner.player ? Owner.player.Health <= 0 : Owner.Health <= 0)
return;
}
int savedMorphTics = MorphedPlayer.morphTics;
MorphedPlayer.mo.UndoPlayerMorph (MorphedPlayer, 0, !!(MorphedPlayer.MorphStyle & MRF_UNDOALWAYS));
Owner.Unmorph(Owner, force: Owner.GetMorphStyle() & MRF_UNDOALWAYS);
MorphedPlayer = null;
}
}

View file

@ -262,7 +262,7 @@ class Weapon : StateProvider
}
let psp = player.GetPSprite(PSP_WEAPON);
if (!psp) return;
if (player.morphTics || player.cheats & CF_INSTANTWEAPSWITCH)
if (Alternative || player.cheats & CF_INSTANTWEAPSWITCH)
{
psp.y = WEAPONBOTTOM;
}

View file

@ -23,8 +23,78 @@
extend class Actor
{
// Blockmap, sector, and no interaction are the only relevant flags but the old ones are kept around
// for legacy reasons.
enum EPremorphProperty
{
MPROP_SOLID = 1 << 1,
MPROP_SHOOTABLE = 1 << 2,
MPROP_NO_BLOCKMAP = 1 << 3,
MPROP_NO_SECTOR = 1 << 4,
MPROP_NO_INTERACTION = 1 << 5,
MPROP_INVIS = 1 << 6,
}
int UnmorphTime;
EMorphFlags MorphFlags;
class<Actor> MorphExitFlash;
EPremorphProperty PremorphProperties;
// Players still track these separately for legacy reasons.
void SetMorphStyle(EMorphFlags flags)
{
if (player)
player.MorphStyle = flags;
else
MorphFlags = flags;
}
clearscope EMorphFlags GetMorphStyle() const
{
return player ? player.MorphStyle : MorphFlags;
}
void SetMorphExitFlash(class<Actor> flash)
{
if (player)
player.MorphExitFlash = flash;
else
MorphExitFlash = flash;
}
clearscope class<Actor> GetMorphExitFlash() const
{
return player ? player.MorphExitFlash : MorphExitFlash;
}
void SetMorphTics(int dur)
{
if (player)
player.MorphTics = dur;
else
UnmorphTime = Level.Time + dur;
}
clearscope int GetMorphTics() const
{
if (player)
return player.MorphTics;
if (UnmorphTime <= 0)
return UnmorphTime;
return UnmorphTime > Level.Time ? UnmorphTime - Level.Time : 0;
}
// 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()
{
EMorphFlags mStyle = GetMorphStyle();
if (mStyle & MRF_UNDOBYDEATH)
Unmorph(self, force: mStyle & MRF_UNDOBYDEATHFORCED);
return null, 0, 0;
}
@ -40,16 +110,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 +124,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 +135,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 && UnmorphTime <= Level.Time && Unmorph(self, MRF_UNDOBYTIMEOUT);
}
//---------------------------------------------------------------------------
//
@ -102,57 +156,265 @@ 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 || !bIsMonster || !spawnType || bDontMorph || Health <= 0 || spawnType == GetClass())
return false;
Actor morphed = Spawn(spawnType, Pos, ALLOW_REPLACE);
if (!MorphInto(morphed))
{
if (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.Special = Special;
for (int i; i < 5; ++i)
morphed.Args[i] = Args[i];
if (TID && (style & MRF_NEWTIDBEHAVIOUR))
{
morphed.ChangeTID(TID);
ChangeTID(0);
}
morphed.Tracer = Tracer;
morphed.Master = Master;
morphed.CopyFriendliness(self, true);
// Remove all armor.
if (!(style & MRF_KEEPARMOR))
{
for (Inventory item = morphed.Inv; item;)
{
Inventory next = item.Inv;
if (item is "Armor")
item.DepleteOrDestroy();
item = next;
}
}
morphed.SetMorphTics((duration ? duration : DEFMORPHTICS) + Random[morphmonst]());
morphed.SetMorphStyle(style);
morphed.SetMorphExitFlash(exitFlash);
morphed.PremorphProperties = (bSolid * MPROP_SOLID) | (bShootable * MPROP_SHOOTABLE)
| (bNoBlockmap * MPROP_NO_BLOCKMAP) | (bNoSector * MPROP_NO_SECTOR)
| (bNoInteraction * MPROP_NO_INTERACTION) | (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.MorphStyle = morphMon.GetMorphStyle();
morphMon.FlagsSave = morphMon.PremorphProperties;
}
Special = 0;
bNoInteraction = true;
A_ChangeLinkFlags(true, true);
// Legacy
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 (!Alternative || bStayMorphed || Alternative.bStayMorphed)
return false;
Actor alt = Alternative;
alt.SetOrigin(Pos, false);
if (!force && (PremorphProperties & MPROP_SOLID))
{
bool altSolid = alt.bSolid;
bool isSolid = bSolid;
bool isTouchy = bTouchy;
alt.bSolid = true;
bSolid = bTouchy = false;
bool res = alt.TestMobjLocation();
alt.bSolid = altSolid;
bSolid = isSolid;
bTouchy = isTouchy;
// Didn't fit.
if (!res)
{
SetMorphTics(5 * TICRATE);
return false;
}
}
if (!MorphInto(alt))
return false;
PreUnmorph(alt, false);
alt.PreUnmorph(self, true);
// Check to see if it had a powerup that caused it to morph.
for (Inventory item = alt.Inv; item;)
{
Inventory next = item.Inv;
if (item is "PowerMorph")
item.Destroy();
item = next;
}
alt.Angle = Angle;
alt.bShadow = bShadow;
alt.bGhost = bGhost;
alt.bSolid = (PremorphProperties & MPROP_SOLID);
alt.bShootable = (PremorphProperties & MPROP_SHOOTABLE);
alt.bInvisible = (PremorphProperties & MPROP_INVIS);
alt.Vel = Vel;
alt.Score = Score;
alt.bNoInteraction = (PremorphProperties & MPROP_NO_INTERACTION);
alt.A_ChangeLinkFlags((PremorphProperties & MPROP_NO_BLOCKMAP), (PremorphProperties & MPROP_NO_SECTOR));
EMorphFlags style = GetMorphStyle();
if (TID && (style & MRF_NEWTIDBEHAVIOUR))
{
alt.ChangeTID(TID);
ChangeTID(0);
}
alt.Special = Special;
for (int i; i < 5; ++i)
alt.Args[i] = Args[i];
alt.Tracer = Tracer;
alt.Master = Master;
alt.CopyFriendliness(self, true, false);
if (Health > 0 || (style & MRF_UNDOBYDEATHSAVES))
alt.Health = alt.SpawnHealth();
else
alt.Health = Health;
Special = 0;
PostUnmorph(alt, false); // From is false here: Leaving the caller's body.
alt.PostUnmorph(self, true); // True here: Entering this body from here.
if (MorphExitFlash)
{
Actor fog = Spawn(MorphExitFlash, alt.Pos.PlusZ(GameInfo.TELEFOGHEIGHT), ALLOW_REPLACE);
if (fog)
fog.Target = alt;
}
ClearCounters();
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;
SetMorphStyle(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.
@ -477,7 +477,7 @@ class PlayerPawn : Actor
let player = self.player;
if (!player) return;
if ((player.WeaponState & WF_DISABLESWITCH) || // Weapon changing has been disabled.
player.morphTics != 0) // Morphed classes cannot change weapons.
Alternative) // Morphed classes cannot change weapons.
{ // ...so throw away any pending weapon requests.
player.PendingWeapon = WP_NOCHANGE;
}
@ -651,7 +651,7 @@ class PlayerPawn : Actor
}
}
if (player.morphTics)
if (Alternative)
{
bob = 0;
}
@ -1084,7 +1084,7 @@ class PlayerPawn : Actor
virtual bool CanCrouch() const
{
return player.morphTics == 0 || bCrouchableMorph;
return !Alternative || bCrouchableMorph;
}
//----------------------------------------------------------------------------
@ -1250,7 +1250,7 @@ class PlayerPawn : Actor
side *= SideMove2;
}
if (!player.morphTics)
if (!Alternative)
{
double factor = 1.;
for(let it = Inv; it != null; it = it.Inv)
@ -1546,15 +1546,15 @@ class PlayerPawn : Actor
{
let player = self.player;
// Morph counter
if (player.morphTics)
if (Alternative)
{
if (player.chickenPeck)
{ // Chicken attack counter
player.chickenPeck -= 3;
}
if (!--player.morphTics)
if (player.MorphTics && !--player.MorphTics)
{ // Attempt to undo the chicken/pig
player.mo.UndoPlayerMorph(player, MRF_UNDOBYTIMEOUT);
Unmorph(self, MRF_UNDOBYTIMEOUT);
}
}
}
@ -1679,7 +1679,7 @@ class PlayerPawn : Actor
player.jumpTics = 0;
}
}
if (player.morphTics && !(player.cheats & CF_PREDICTING))
if (Alternative && !(player.cheats & CF_PREDICTING))
{
MorphPlayerThink ();
}
@ -2060,9 +2060,9 @@ class PlayerPawn : Actor
Inventory item, next;
let p = player;
if (p.morphTics != 0)
if (Alternative)
{ // Undo morph
Unmorph(self, 0, true);
Unmorph(self, force: true);
}
// 'self' will be no longer valid from here on in case of an unmorph
let me = p.mo;
@ -2869,23 +2869,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

@ -426,35 +426,40 @@ extend class PlayerPawn
}
virtual String CheatMorph(class<PlayerPawn> morphClass, bool quickundo)
virtual String CheatMorph(class<PlayerPawn> morphClass, bool undo)
{
let oldclass = GetClass();
// Set the standard morph style for the current game
int style = MRF_UNDOBYTOMEOFPOWER;
if (gameinfo.gametype == GAME_Hexen) style |= MRF_UNDOBYCHAOSDEVICE;
EMorphFlags style = MRF_UNDOBYTOMEOFPOWER;
if (GameInfo.GameType == GAME_Hexen)
style |= MRF_UNDOBYCHAOSDEVICE;
if (player.morphTics)
if (Alternative)
{
if (UndoPlayerMorph (player))
class<PlayerPawn> cls = GetClass();
Actor mo = Alternative;
if (!undo || Unmorph(self))
{
if (!quickundo && oldclass != morphclass && MorphPlayer (player, morphclass, 0, style))
if ((!undo && Morph(self, morphClass, null, 0, style))
|| (undo && morphClass != cls && mo.Morph(mo, morphClass, null, 0, style)))
{
return StringTable.Localize("$TXT_STRANGER");
}
return StringTable.Localize("$TXT_NOTSTRANGE");
if (undo)
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");
}
return "";
}
virtual void CheatTakeWeaps()
{
if (player.morphTics || health <= 0)
if (Alternative || health <= 0)
{
return;
}

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,122 @@ 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 (!player || !spawnType || bDontMorph || player.Health <= 0
|| (!(style & MRF_IGNOREINVULN) && bInvulnerable && (player != activator || !(style & MRF_WHENINVULNERABLE))))
{
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');
if (!duration)
duration = DEFMORPHTICS;
if (spawnType == GetClass())
{
// Player is already a beast.
if (Alternative && bCanSuperMorph
&& GetMorphTics() < 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));
let morphed = PlayerPawn(Spawn(spawnType, Pos, NO_REPLACE));
if (!MorphInto(morphed))
{
if (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)
| (bNoBlockmap * MPROP_NO_BLOCKMAP) | (bNoSector * MPROP_NO_SECTOR)
| (bNoInteraction * MPROP_NO_INTERACTION) | (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.
if (!(style & MRF_KEEPARMOR))
{
let next = item.Inv;
if (item is 'Armor')
for (Inventory item = morphed.Inv; item;)
{
item.DepleteOrDestroy();
Inventory next = item.Inv;
if (item is "Armor")
item.DepleteOrDestroy();
item = next;
}
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;
morphed.SetMorphTics(duration);
morphed.SetMorphStyle(style);
morphed.SetMorphExitFlash(exitFlash);
p.MorphedPlayerClass = spawnType;
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();
bNoInteraction = true;
A_ChangeLinkFlags(true, true);
// Legacy
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 +220,156 @@ 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 || bStayMorphed || Alternative.bStayMorphed)
return false;
if (!(unmorphFlags & MRF_IGNOREINVULN) && 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 && (PremorphProperties & MPROP_SOLID))
{
bool altSolid = alt.bSolid;
bool isSolid = bSolid;
bool isTouchy = bTouchy;
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 = bTouchy = false;
bool res = alt.TestMobjLocation();
alt.bSolid = altSolid;
bSolid = isSolid;
bTouchy = isTouchy;
if (!res)
{
SetMorphTics(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)
alt.bNoInteraction = (PremorphProperties & MPROP_NO_INTERACTION);
alt.A_ChangeLinkFlags((PremorphProperties & MPROP_NO_BLOCKMAP), (PremorphProperties & MPROP_NO_SECTOR));
let p = alt.player;
class<Actor> exitFlash = alt.GetMorphExitFlash();
EMorphFlags style = alt.GetMorphStyle();
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;
}
alt.SetMorphTics(0);
alt.SetMorphStyle(0);
alt.SetMorphExitFlash(null);
p.MorphedPlayerClass = 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();
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

@ -23,35 +23,32 @@ class ArtiTeleport : Inventory
Loop;
}
override bool Use (bool pickup)
override bool Use(bool pickup)
{
Vector3 dest;
int destAngle;
double destAngle;
if (deathmatch)
{
[dest, destAngle] = level.PickDeathmatchStart();
}
[dest, destAngle] = Level.PickDeathmatchStart();
else
{
[dest, destAngle] = level.PickPlayerStart(Owner.PlayerNumber());
}
if (!level.useplayerstartz)
[dest, destAngle] = Level.PickPlayerStart(Owner.PlayerNumber());
if (!Level.UsePlayerStartZ)
dest.Z = ONFLOORZ;
Owner.Teleport (dest, destAngle, TELF_SOURCEFOG | TELF_DESTFOG);
bool canlaugh = true;
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))
{
canlaugh = false;
}
}
if (canlaugh)
{ // Full volume laugh
Owner.A_StartSound ("*evillaugh", CHAN_VOICE, CHANF_DEFAULT, 1., ATTN_NONE);
Owner.Teleport(dest, destAngle, TELF_SOURCEFOG | TELF_DESTFOG);
bool canLaugh = Owner.player != null;
EMorphFlags mStyle = Owner.GetMorphStyle();
if (Owner.Alternative && (mStyle & MRF_UNDOBYCHAOSDEVICE))
{
// Teleporting away will undo any morph effects (pig).
if (!Owner.Unmorph(Owner, MRF_UNDOBYCHAOSDEVICE) && (mStyle & MRF_FAILNOLAUGH))
canLaugh = false;
}
if (canLaugh)
Owner.A_StartSound("*evillaugh", CHAN_VOICE, attenuation: ATTN_NONE);
return true;
}

View file

@ -232,6 +232,8 @@ enum EMorphFlags
MRF_UNDOBYTIMEOUT = 0x00001000,
MRF_UNDOALWAYS = 0x00002000,
MRF_TRANSFERTRANSLATION = 0x00004000,
MRF_KEEPARMOR = 0x00008000,
MRF_IGNOREINVULN = 0x00010000,
MRF_STANDARDUNDOING = MRF_UNDOBYTOMEOFPOWER | MRF_UNDOBYCHAOSDEVICE | MRF_UNDOBYTIMEOUT,
};