Overridden functions GameRules.PreventDeath and GameRules.ModifyDamage can prevent death of a pawn
function Died(pawn Killer, name damageType, vector HitLocation)
{
local pawn OtherPawn;
local GameRules GR;
if ( bDeleteMe || Level.NetMode==NM_Client )
return; //already destroyed
if ( Level.Game!=None && Level.Game.GameRules!=None )
{
for ( GR=Level.Game.GameRules; GR!=None; GR=GR.NextRules )
if ( GR.bHandleDeaths && GR.PreventDeath(Self,Killer,damageType) )
{
Health = Max(1,Health);
Return;
}
and taking a damage
function TakeDamage( int Damage, Pawn instigatedBy, Vector hitlocation,
Vector momentum, name damageType)
{
local int actualDamage;
local bool bAlreadyDead;
local GameRules GR;
if ( bDeleteMe || Level.NetMode==NM_Client )
return;
if ( Level.Game!=None && Level.Game.GameRules!=None )
{
for ( GR=Level.Game.GameRules; GR!=None; GR=GR.NextRules )
if ( GR.bModifyDamage )
GR.ModifyDamage(Self,instigatedBy,Damage,hitlocation,damageType,momentum);
}
PreventDeath and ModifyDamage together can be used to make the pawn invulnerable to many destructive factors - the pawn won't lose health/armor or die. However, there are 4 functions that unconditionally change Health, independently of PreventDeath and ModifyDamage:
1. A mover whose MoverEncroachType is ME_CrushWhenEncroach may call PlayerPawn's KilledBy:
else if ( MoverEncroachType == ME_CrushWhenEncroach )
{
// Kill it.
Other.KilledBy( Instigator );
return false;
}
function KilledBy( pawn EventInstigator )
{
Health = 0;
Died( EventInstigator, 'suicided', Location );
}
PreventDeath may save the player from dying. But in order to restore the original value of Health that was changed by the assignment
Health = 0;
the mod has to store it beforehand somehow. It's possible to save Health of every protected Pawn on every game tick, but such a solution looks a bit expensive and it does not guarantee correct results if Health is increased shortly before the call to KilledBy (operations can be done in the following order: saving the value of Health -> increasing Health -> calling KilledBy).
2. A pawn may call gibbedBy to perform telefragging by other pawn:
event EncroachedBy( actor Other )
{
if ( Pawn(Other) != None )
gibbedBy(Other);
}
function gibbedBy(actor Other)
{
local pawn instigatedBy;
if ( Role < ROLE_Authority )
return;
instigatedBy = pawn(Other);
if (instigatedBy == None)
instigatedBy = Other.instigator;
health = -1000; //make sure gibs
Died(instigatedBy, 'Gibbed', Location);
}
This implementation implies the same issue: Health is unconditionally changed to -1000 and PreventDeath is not informed of the previous value.
3. When a player reaches an area that is outside of any valid zone, FellOutOfWorld is called:
event FellOutOfWorld()
{
if (PlayerReplicationInfo != none)
{
Health = -1;
SetPhysics(PHYS_None);
Died(None, 'fell', Location);
}
else
{
Died(None, 'fell', Location);
Destroy();
}
}
Again, setting Health to -1 makes recovering the original value harder. In the second branch, Destroy is called even if death was prevented.
4. When a pawn "touches" a TriggeredDeath actor, pawn's Health and inventory may be affected:
function Touch( Actor Other )
{
local inventory Inv;
local Pawn P;
local int VNum;
// Something has contacted the death trigger.
// If it is a PlayerPawn, have it screen flash and
// die.
if ( Other.bIsPawn )
{
P = Pawn(Other);
P.Weapon = None;
P.SelectedItem = None;
for ( Inv=Other.Inventory; Inv!=None; Inv=Inv.Inventory )
Inv.Destroy();
if ( P.Health <= 0 )
return;
if ( Other.IsA('PlayerPawn') )
{
Enable('Tick');
While ( (VNum < 7) && (Victim[VNum] != None) )
VNum++;
Victim[Vnum] = PlayerPawn(Other);
TimePassed[VNum] = 0;
}
else
KillVictim(P);
P.Health = 1;
if ( P.bIsPlayer )
{
if ( P.bIsFemale )
Other.PlaySound( FemaleDeathSound, SLOT_Talk );
else
Other.PlaySound( MaleDeathSound, SLOT_Talk );
}
else
P.Playsound(P.Die, SLOT_Talk);
}
else if ( bDestroyItems )
Other.Destroy();
}
function KillVictim(Pawn Victim)
{
Victim.NextState = '';
Victim.Health = -1;
Victim.Died(None, DeathName, Victim.Location);
Victim.HidePlayer();
}
function Tick( float DeltaTime )
{
local Float CurScale;
local vector CurFog;
local float TimeRatio;
local int i;
local bool bFoundVictim;
for ( i=0; i<8; i++ )
if ( Victim[i] != None )
{
if ( Victim[i].Health > 1 )
Victim[i] = None;
else
{
// Check the timing
TimePassed[i] += DeltaTime;
if ( TimePassed[i] >= ChangeTime )
{
TimeRatio = 1;
Victim[i].ClientFlash( EndFlashScale, 1000 * EndFlashFog );
if ( Victim[i].Health > 0 )
KillVictim(Victim[i]);
Victim[i] = None;
}
else
{
bFoundVictim = true;
// Continue the screen flashing
TimeRatio = TimePassed[i]/ChangeTime;
CurScale = (EndFlashScale-StartFlashScale)*TimeRatio + StartFlashScale;
CurFog = (EndFlashFog -StartFlashFog )*TimeRatio + StartFlashFog;
Victim[i].ClientFlash( CurScale, 1000 * CurFog );
}
}
}
if ( !bFoundVictim )
Disable('Tick');
}
}
Recovering the pawn's inventory is also a very tricky thing.
Suggested resolution:
Option #1:- Modify PlayerPawn.KilledBy, Pawn.GibbedBy, Pawn.FellOutOfWorld, and the functions of state TriggeredDeath.Enabled as indicated below:
function KilledBy( pawn EventInstigator )
{
+ local int OldHealth;
+
+ OldHealth = Health;
Health = 0;
Died(EventInstigator, 'suicided', Location);
+ if (Health > 0)
+ Health = OldHealth;
}
function gibbedBy(actor Other)
{
local pawn instigatedBy;
+ local int OldHealth;
if ( Role < ROLE_Authority )
return;
instigatedBy = pawn(Other);
- if (instigatedBy == None)
+ if (instigatedBy == None && Other != None)
instigatedBy = Other.instigator;
- health = -1000; //make sure gibs
- Died(instigatedBy, 'Gibbed', Location);
+
+ if (Health > 0)
+ {
+ OldHealth = Health;
+ Health = -1000; //make sure gibs
+ Died(instigatedBy, 'Gibbed', Location);
+ if (Health > 0)
+ Health = OldHealth;
+ }
}
event FellOutOfWorld()
{
+ local int OldHealth;
+
+ if (Role != ROLE_Authority)
+ return;
if (PlayerReplicationInfo != none)
{
- Health = -1;
SetPhysics(PHYS_None);
- Died(None, 'fell', Location);
+ if (Health > 0)
+ {
+ OldHealth = Health;
+ Health = -1;
+ Died(None, 'fell', Location);
+ if (Health > 0)
+ Health = OldHealth;
+ }
}
- else
+ else if (Health > 0)
{
Died(None, 'fell', Location);
- Destroy();
+ if (Health <= 0)
+ Destroy();
}
}
auto state Enabled
{
function Touch( Actor Other )
{
local inventory Inv;
local Pawn P;
- local int VNum;
+ local GameRules GR;
// Something has contacted the death trigger.
// If it is a PlayerPawn, have it screen flash and
// die.
if ( Other.bIsPawn )
{
P = Pawn(Other);
+ if (P.Health <= 0)
+ return;
+
+ for (GR = Level.Game.GameRules; GR != none; GR = GR.NextRules )
+ if (GR.bHandleDeaths && GR.PreventDeath(P, none, DeathName))
+ return;
+
P.Weapon = None;
P.SelectedItem = None;
for ( Inv=Other.Inventory; Inv!=None; Inv=Inv.Inventory )
Inv.Destroy();
- if ( P.Health <= 0 )
- return;
- if ( Other.IsA('PlayerPawn') )
- {
- Enable('Tick');
- While ( (VNum < 7) && (Victim[VNum] != None) )
- VNum++;
- Victim[Vnum] = PlayerPawn(Other);
- TimePassed[VNum] = 0;
- }
- else
- KillVictim(P);
-
- P.Health = 1;
- if ( P.bIsPlayer )
- {
- if ( P.bIsFemale )
- Other.PlaySound( FemaleDeathSound, SLOT_Talk );
- else
- Other.PlaySound( MaleDeathSound, SLOT_Talk );
- }
- else
- P.Playsound(P.Die, SLOT_Talk);
+ if (PlayerPawn(Other) != none)
+ InitTriggeredPlayerPawnDeath(PlayerPawn(Other));
+ else
+ KillVictim(P);
}
else if ( bDestroyItems )
Other.Destroy();
}
+
+ function InitTriggeredPlayerPawnDeath(PlayerPawn P)
+ {
+ local TriggeredPlayerPawnDeath TriggeredPlayerPawnDeath;
+ local int VNum;
+
+ if (Class == class'TriggeredDeath')
+ {
+ foreach P.ChildActors(class'TriggeredPlayerPawnDeath', TriggeredPlayerPawnDeath)
+ return;
+ TriggeredPlayerPawnDeath = Spawn(class'TriggeredPlayerPawnDeath', P);
+ }
+ if (TriggeredPlayerPawnDeath != none)
+ {
+ TriggeredPlayerPawnDeath.Victim = P;
+ TriggeredPlayerPawnDeath.TriggeredDeath = self;
+ }
+ else
+ {
+ Enable('Tick');
+ While ( (VNum < 7) && (Victim[VNum] != None) )
+ VNum++;
+ Victim[Vnum] = P;
+ TimePassed[VNum] = 0;
+ }
+
+ P.Health = 1;
+ if ( P.bIsFemale )
+ P.PlaySound( FemaleDeathSound, SLOT_Talk );
+ else
+ P.PlaySound( MaleDeathSound, SLOT_Talk );
+ }
function KillVictim(Pawn Victim)
{
+ local int OldHealth;
+
+ if (Victim.Health <= 0)
+ return;
+ OldHealth = Victim.Health;
Victim.NextState = '';
Victim.Health = -1;
Victim.Died(None, DeathName, Victim.Location);
- Victim.HidePlayer();
+ if (Victim.Health > 0)
+ Victim.Health = OldHealth;
}
function Tick( float DeltaTime )
{
local Float CurScale;
local vector CurFog;
local float TimeRatio;
local int i;
local bool bFoundVictim;
for ( i=0; i<8; i++ )
if ( Victim[i] != None )
{
if ( Victim[i].Health > 1 )
Victim[i] = None;
else
{
// Check the timing
TimePassed[i] += DeltaTime;
if ( TimePassed[i] >= ChangeTime )
{
- TimeRatio = 1;
Victim[i].ClientFlash( EndFlashScale, 1000 * EndFlashFog );
if ( Victim[i].Health > 0 )
KillVictim(Victim[i]);
Victim[i] = None;
}
else
{
bFoundVictim = true;
// Continue the screen flashing
TimeRatio = TimePassed[i]/ChangeTime;
CurScale = (EndFlashScale-StartFlashScale)*TimeRatio + StartFlashScale;
CurFog = (EndFlashFog -StartFlashFog )*TimeRatio + StartFlashFog;
Victim[i].ClientFlash( CurScale, 1000 * CurFog );
}
}
}
if ( !bFoundVictim )
Disable('Tick');
}
}
- Add new class TriggeredPlayerPawnDeath:
class TriggeredPlayerPawnDeath expands Triggers;
var PlayerPawn Victim;
var TriggeredDeath TriggeredDeath;
var float TimePassed;
event Tick(float DeltaTime)
{
local float CurScale;
local vector CurFog;
local float TimeRatio;
if (Victim == none || Victim.bDeleteMe || Victim.Health > 1 ||
TriggeredDeath == none || TriggeredDeath.bDeleteMe)
{
Destroy();
return;
}
// Check the timing
TimePassed += DeltaTime;
if (TimePassed >= TriggeredDeath.ChangeTime)
{
Victim.ClientFlash(TriggeredDeath.EndFlashScale, 1000 * TriggeredDeath.EndFlashFog);
if (Victim.Health > 0)
KillVictim();
Destroy();
}
else
{
// Continue the screen flashing
TimeRatio = TimePassed / TriggeredDeath.ChangeTime;
CurScale = (TriggeredDeath.EndFlashScale - TriggeredDeath.StartFlashScale) * TimeRatio + TriggeredDeath.StartFlashScale;
CurFog = (TriggeredDeath.EndFlashFog - TriggeredDeath.StartFlashFog ) * TimeRatio + TriggeredDeath.StartFlashFog;
Victim.ClientFlash(CurScale, 1000 * CurFog);
}
}
function KillVictim()
{
local int OldHealth;
if (Victim.Health <= 0)
return;
OldHealth = Victim.Health;
Victim.Health = -1;
Victim.Died(None, TriggeredDeath.DeathName, Victim.Location);
if (Victim.Health > 0)
Victim.Health = OldHealth;
}
(Using a separate actor for each player guarantees that any reasonable number of players can be properly handled)
This option implies that if PreventDeath prevents death, the value of Health is restored to the original value (PreventDeath cannot establish the resulting Health).
Option #2:- Add new variable to Engine.Pawn:
var transient int HealthBeforeDying;
- Modify PlayerPawn.KilledBy, Pawn.GibbedBy, Pawn.FellOutOfWorld, and the functions of state TriggeredDeath.Enabled as indicated below:
function KilledBy( pawn EventInstigator )
{
+ HealthBeforeDying = Health;
Health = 0;
Died(EventInstigator, 'suicided', Location);
+ HealthBeforeDying = 0;
}
function gibbedBy(actor Other)
{
local pawn instigatedBy;
if ( Role < ROLE_Authority )
return;
instigatedBy = pawn(Other);
- if (instigatedBy == None)
+ if (instigatedBy == None && Other != None)
instigatedBy = Other.instigator;
- health = -1000; //make sure gibs
- Died(instigatedBy, 'Gibbed', Location);
+
+ if (Health > 0)
+ {
+ HealthBeforeDying = Health;
+ Health = -1000; //make sure gibs
+ Died(instigatedBy, 'Gibbed', Location);
+ HealthBeforeDying = 0;
+ }
}
event FellOutOfWorld()
{
+ if (Role != ROLE_Authority)
+ return;
if (PlayerReplicationInfo != none)
{
- Health = -1;
SetPhysics(PHYS_None);
- Died(None, 'fell', Location);
+ if (Health > 0)
+ {
+ HealthBeforeDying = Health;
+ Health = -1;
+ Died(None, 'fell', Location);
+ HealthBeforeDying = 0;
+ }
}
- else
+ else if (Health > 0)
{
Died(None, 'fell', Location);
- Destroy();
+ if (Health <= 0)
+ Destroy();
}
}
auto state Enabled
{
function Touch( Actor Other )
{
local inventory Inv;
local Pawn P;
- local int VNum;
+ local GameRules GR;
// Something has contacted the death trigger.
// If it is a PlayerPawn, have it screen flash and
// die.
if ( Other.bIsPawn )
{
P = Pawn(Other);
+ if (P.Health <= 0)
+ return;
+
+ for (GR = Level.Game.GameRules; GR != none; GR = GR.NextRules )
+ if (GR.bHandleDeaths && GR.PreventDeath(P, none, DeathName))
+ return;
+
P.Weapon = None;
P.SelectedItem = None;
for ( Inv=Other.Inventory; Inv!=None; Inv=Inv.Inventory )
Inv.Destroy();
- if ( P.Health <= 0 )
- return;
- if ( Other.IsA('PlayerPawn') )
- {
- Enable('Tick');
- While ( (VNum < 7) && (Victim[VNum] != None) )
- VNum++;
- Victim[Vnum] = PlayerPawn(Other);
- TimePassed[VNum] = 0;
- }
- else
- KillVictim(P);
-
- P.Health = 1;
- if ( P.bIsPlayer )
- {
- if ( P.bIsFemale )
- Other.PlaySound( FemaleDeathSound, SLOT_Talk );
- else
- Other.PlaySound( MaleDeathSound, SLOT_Talk );
- }
- else
- P.Playsound(P.Die, SLOT_Talk);
+ if (PlayerPawn(Other) != none)
+ InitTriggeredPlayerPawnDeath(PlayerPawn(Other));
+ else
+ KillVictim(P);
}
else if ( bDestroyItems )
Other.Destroy();
}
+
+ function InitTriggeredPlayerPawnDeath(PlayerPawn P)
+ {
+ local TriggeredPlayerPawnDeath TriggeredPlayerPawnDeath;
+ local int VNum;
+
+ if (Class == class'TriggeredDeath')
+ {
+ foreach P.ChildActors(class'TriggeredPlayerPawnDeath', TriggeredPlayerPawnDeath)
+ return;
+ TriggeredPlayerPawnDeath = Spawn(class'TriggeredPlayerPawnDeath', P);
+ }
+ if (TriggeredPlayerPawnDeath != none)
+ {
+ TriggeredPlayerPawnDeath.Victim = P;
+ TriggeredPlayerPawnDeath.TriggeredDeath = self;
+ }
+ else
+ {
+ Enable('Tick');
+ While ( (VNum < 7) && (Victim[VNum] != None) )
+ VNum++;
+ Victim[Vnum] = P;
+ TimePassed[VNum] = 0;
+ }
+
+ P.Health = 1;
+ if ( P.bIsFemale )
+ P.PlaySound( FemaleDeathSound, SLOT_Talk );
+ else
+ P.PlaySound( MaleDeathSound, SLOT_Talk );
+ }
function KillVictim(Pawn Victim)
{
+ if (Victim.Health <= 0)
+ return;
+ Victim.HealthBeforeDying = Victim.Health;
Victim.NextState = '';
Victim.Health = -1;
Victim.Died(None, DeathName, Victim.Location);
- Victim.HidePlayer();
+ Victim.HealthBeforeDying = 0;
}
function Tick( float DeltaTime )
{
local Float CurScale;
local vector CurFog;
local float TimeRatio;
local int i;
local bool bFoundVictim;
for ( i=0; i<8; i++ )
if ( Victim[i] != None )
{
if ( Victim[i].Health > 1 )
Victim[i] = None;
else
{
// Check the timing
TimePassed[i] += DeltaTime;
if ( TimePassed[i] >= ChangeTime )
{
- TimeRatio = 1;
Victim[i].ClientFlash( EndFlashScale, 1000 * EndFlashFog );
if ( Victim[i].Health > 0 )
KillVictim(Victim[i]);
Victim[i] = None;
}
else
{
bFoundVictim = true;
// Continue the screen flashing
TimeRatio = TimePassed[i]/ChangeTime;
CurScale = (EndFlashScale-StartFlashScale)*TimeRatio + StartFlashScale;
CurFog = (EndFlashFog -StartFlashFog )*TimeRatio + StartFlashFog;
Victim[i].ClientFlash( CurScale, 1000 * CurFog );
}
}
}
if ( !bFoundVictim )
Disable('Tick');
}
}
- Add new class TriggeredPlayerPawnDeath:
class TriggeredPlayerPawnDeath expands Triggers;
var PlayerPawn Victim;
var TriggeredDeath TriggeredDeath;
var float TimePassed;
event Tick(float DeltaTime)
{
local float CurScale;
local vector CurFog;
local float TimeRatio;
if (Victim == none || Victim.bDeleteMe || Victim.Health > 1 ||
TriggeredDeath == none || TriggeredDeath.bDeleteMe)
{
Destroy();
return;
}
// Check the timing
TimePassed += DeltaTime;
if (TimePassed >= TriggeredDeath.ChangeTime)
{
Victim.ClientFlash(TriggeredDeath.EndFlashScale, 1000 * TriggeredDeath.EndFlashFog);
if (Victim.Health > 0)
KillVictim();
Destroy();
}
else
{
// Continue the screen flashing
TimeRatio = TimePassed / TriggeredDeath.ChangeTime;
CurScale = (TriggeredDeath.EndFlashScale - TriggeredDeath.StartFlashScale) * TimeRatio + TriggeredDeath.StartFlashScale;
CurFog = (TriggeredDeath.EndFlashFog - TriggeredDeath.StartFlashFog ) * TimeRatio + TriggeredDeath.StartFlashFog;
Victim.ClientFlash(CurScale, 1000 * CurFog);
}
}
function KillVictim()
{
if (Victim.Health <= 0)
return;
Victim.HealthBeforeDying = Victim.Health;
Victim.Health = -1;
Victim.Died(None, TriggeredDeath.DeathName, Victim.Location);
Victim.HealthBeforeDying = 0;
}
This option implies that if PreventDeath prevents death, the value of Health is set to the maximum of 1 and the value of Health after returning from PreventDeath (PreventDeath can establish the resulting Health within the range [1; MaxInt]; in particular, PreventDeath can set the value of Health to the value of HealthBeforeDying).
A non-positive value of HealthBeforeDying means that the health before dying is unknown (Died may be called from third-party mods that are not aware of HealthBeforeDying, so it's important to reset HealthBeforeDying when the value stored in this variable is not needed anymore and further changes of Health can make the value of HealthBeforeDying irrelevant).