Unit status management for Units. Stores active status effects, provides RPCs to add/remove statuses (burning, frozen, poison, fear, shock, shield, stun, mark, invincible, chained), updates status objects each tick, and creates/destroys VFX and plays SFX for state changes.
using System;
using Sandbox;
using Sandbox.Diagnostics;
public partial class Unit
{
public Dictionary<TypeDescription, UnitStatus> UnitStatuses = new();
public bool IsBurning { get; set; }
private GameObject _burningVfx;
public bool IsFrozen { get; set; }
public TimeSince TimeSinceFrozen { get; set; }
private GameObject _frozenVfx;
public bool IsPoisoned { get; set; }
private GameObject _poisonVfx;
public bool IsFearful { get; set; }
private GameObject _fearVfx;
public bool IsShocked { get; set; }
private GameObject _shockVfx;
public bool IsShielded { get; set; }
private GameObject _shieldVfx;
public bool IsStunned { get; set; }
private GameObject _stunnedVfx;
public bool IsInvincible { get; set; }
public bool IsMarked { get; set; }
private GameObject _markedVfx;
public bool IsChained { get; set; }
public Dictionary<int, UnitChainedVfx> _chainedVfxs;
public virtual bool CanBeStunned => !IsDying && !IsInanimate;
void HandleStatuses( float dt )
{
Assert.True( !IsProxy );
for ( int i = UnitStatuses.Count - 1; i >= 0; i-- )
{
if ( i >= UnitStatuses.Count )
continue;
var status = UnitStatuses.Values.ElementAt( i );
if ( IsDying )
continue;
if ( status.ShouldUpdate )
status.Update( dt );
}
}
[Rpc.Owner]
public void Ignite( Player playerSource, Enemy enemySource, EnemyType enemyType, float damage, float lifetime, float spreadChance, bool canStack )
{
if ( IsDying )
return;
if ( this is Player player && player.IsInvincible )
return;
bool wasAlreadyBurning = IsBurning;
var fire = AddUnitStatus<UnitStatusFire>();
fire.PlayerSource = playerSource;
fire.EnemySource = enemySource;
fire.EnemyType = enemyType;
//if ( wasAlreadyBurning && player.Stats[PlayerStat.FireDmgStack] > 0f )
// fire.AddDamageStack( player.Stats[PlayerStat.FireDamage], player.Stats[PlayerStat.FireLifetime] );
//else
// fire.Damage = player.Stats[PlayerStat.FireDamage] * player.GetDamageMultiplier();
if ( wasAlreadyBurning && canStack )
fire.AddDamageStack( damage, lifetime );
else
fire.SetStartingDamage( damage );
//fire.Lifetime = player.Stats[PlayerStat.FireLifetime];
//fire.SpreadChance = player.Stats[PlayerStat.FireSpreadChance];
fire.Lifetime = lifetime;
fire.SpreadChance = spreadChance;
if ( playerSource.IsValid() && this is Enemy enemy )
playerSource.IgniteEnemy( enemy );
}
[Rpc.Owner]
public void Freeze( Player playerSource, Enemy enemySource, float timeScale, float lifetime, bool playSfx = true )
{
if ( IsDying || IsFrozen )
return;
var frozen = AddUnitStatus<UnitStatusFreeze>();
frozen.SetValues( timeScale, lifetime );
frozen.PlayerSource = playerSource;
frozen.EnemySource = enemySource;
if ( playerSource.IsValid() && this is Enemy enemy )
playerSource.FreezeEnemy( enemy );
if ( playSfx )
Manager.Instance.PlaySfxNearbyRpc( "frozen_02", Position2D, pitch: Game.Random.Float( 1.2f, 1.35f ), volume: 1.1f, maxDist: 250f );
}
[Rpc.Owner]
public void Poison( Player playerSource, Enemy enemySource, EnemyType enemyType, float damage, float finishDmgPercent, float dieSpreadChance, float radiusMultiplier, bool flammable, float tickTimeModifier = 1f, int hitsToRemove = 1 )
{
if ( IsDying )
return;
if ( this is Player player && player.IsInvincible )
return;
var poison = AddUnitStatus<UnitStatusPoison>();
poison.PlayerSource = playerSource;
poison.EnemySource = enemySource;
poison.EnemyType = enemyType;
poison.SetValues( damage, finishDmgPercent, dieSpreadChance, radiusMultiplier, flammable, tickTimeModifier, hitsToRemove );
if ( playerSource.IsValid() && this is Enemy enemy )
playerSource.PoisonEnemy( enemy );
}
[Rpc.Owner]
public void Fear( Player playerSource, Enemy enemySource, float lifetime )
{
if ( IsDying || IsInanimate )
return;
var fear = AddUnitStatus<UnitStatusFear>();
fear.PlayerSource = playerSource;
fear.EnemySource = enemySource;
fear.Lifetime = lifetime;
if ( playerSource.IsValid() && this is Enemy fearEnemy )
playerSource.FearEnemy( fearEnemy );
OnFear();
}
public virtual void OnFear()
{
}
[Rpc.Owner]
public void Shock( Player playerSource, Enemy enemySource, float damage, int currSpreadCount, int spreadLimit )
{
if ( IsDying || IsShocked )
return;
if ( this is Player player && player.IsInvincible )
return;
var shock = AddUnitStatus<UnitStatusShock>();
shock.PlayerSource = playerSource;
shock.EnemySource = enemySource;
shock.Damage = damage;
shock.CurrSpreadCount = currSpreadCount; // todo: count down instead?
shock.SpreadLimit = spreadLimit;
}
[Rpc.Owner]
public void GainShield( float breakDmg = 0f )
{
if ( IsDying || IsShielded )
return;
var shield = AddUnitStatus<UnitStatusShield>();
if( breakDmg > 0f )
shield.EnemyAoeDamage = breakDmg;
OnGainShield();
}
public virtual void OnGainShield()
{
}
[Rpc.Owner]
public void Stun( Player playerSource, Enemy enemySource, float lifetime )
{
if ( !CanBeStunned )
return;
var stun = AddUnitStatus<UnitStatusStun>();
stun.PlayerSource = playerSource;
stun.EnemySource = enemySource;
stun.Lifetime = lifetime;
//ShakeRpc( startStrength: 1f, endStrength: 0.5f, lifetime );
}
public virtual void OnStun()
{
}
[Rpc.Owner]
public void Mark( Player playerSource, float damage )
{
if ( IsDying )
return;
var marked = AddUnitStatus<UnitStatusMarked>();
marked.PlayerSource = playerSource;
marked.Damage = damage;
// todo: vfx
}
[Rpc.Owner]
public void UnMark()
{
if ( IsDying )
return;
RemoveUnitStatus<UnitStatusMarked>();
// todo: vfx
}
public virtual void OnStunFinish()
{
}
[Rpc.Owner]
public void Punched( Player playerSource )
{
if ( IsDying )
return;
var punched = AddUnitStatus<UnitStatusPunched>();
punched.PlayerSource = playerSource;
}
[Rpc.Owner]
public void BecomeInvincible( float lifetime = 0f )
{
if ( IsDying )
return;
if( IsInvincible )
{
var existing = GetUnitStatus<UnitStatusInvincible>();
if( existing.Lifetime > 0f )
{
var timeRemaining = existing.Lifetime - existing.ElapsedTime;
if ( lifetime > timeRemaining )
{
existing.Lifetime = lifetime;
existing.ElapsedTime = 0f;
}
}
return;
}
var invincible = AddUnitStatus<UnitStatusInvincible>();
invincible.Lifetime = lifetime;
}
[Rpc.Owner]
public void Chain( Unit anchorUnit, Vector2 chainPos, float chainLength, float lifetime )
{
if ( IsDying )
return;
if ( this is Player player && player.IsInvincible )
return;
OnChain();
var chained = AddUnitStatus<UnitStatusChained>();
chained.AddChain( anchorUnit, chainPos, chainLength, lifetime );
}
public virtual void OnChain()
{
}
public TStatus AddUnitStatus<TStatus>()
where TStatus : UnitStatus
{
Assert.True( !IsProxy );
var type = TypeLibrary.GetType<TStatus>();
if ( UnitStatuses.TryGetValue( type, out var status ) )
{
status.Refresh();
return (TStatus)status;
}
else
{
status = type.Create<UnitStatus>();
UnitStatuses.Add( type, status );
status.Init( this );
return (TStatus)status;
}
}
public void RemoveUnitStatus<TStatus>( TStatus status )
where TStatus : UnitStatus
{
Assert.True( !IsProxy );
if ( UnitStatuses.Remove( TypeLibrary.GetType<TStatus>(), out var existing ) )
{
Assert.AreEqual( existing, status );
status.OnRemove();
}
}
public void RemoveUnitStatus<TStatus>()
where TStatus : UnitStatus
{
Assert.True( !IsProxy );
if ( UnitStatuses.Remove( TypeLibrary.GetType<TStatus>(), out var status ) )
{
status.OnRemove();
}
}
public void RemoveAllUnitStatuses()
{
for ( int i = UnitStatuses.Count - 1; i >= 0; i-- )
{
var type = UnitStatuses.Keys.ElementAt( i );
var status = UnitStatuses.Values.ElementAt( i );
status.OnRemove( playEffects: false );
UnitStatuses.Remove( type );
}
UnitStatuses.Clear();
IsBurning = false;
IsFrozen = false;
IsPoisoned = false;
IsFearful = false;
IsShocked = false;
IsShielded = false;
IsStunned = false;
IsInvincible = false;
}
public TStatus GetUnitStatus<TStatus>()
where TStatus : UnitStatus
{
Assert.True( !IsProxy );
return UnitStatuses.TryGetValue( TypeLibrary.GetType<TStatus>(), out var status )
? (TStatus)status
: null;
}
public bool HasUnitStatus<TStatus>( TStatus status )
where TStatus : UnitStatus
{
return UnitStatuses.TryGetValue( TypeLibrary.GetType<TStatus>(), out var existing ) && existing == status;
}
public bool HasUnitStatus<TStatus>()
where TStatus : UnitStatus
{
return UnitStatuses.ContainsKey( TypeLibrary.GetType<TStatus>() );
}
[Rpc.Broadcast]
public void SetStatusBurning( bool burning )
{
if ( burning == IsBurning )
return;
IsBurning = burning;
if ( burning )
{
_burningVfx = GameObject.Clone( "prefabs/effects/unit_status_fire.prefab", new CloneConfig { StartEnabled = true, Parent = GameObject } );
var yPos = Height * (ParticleYPosOverride > 0f ? ParticleYPosOverride : 0.75f);
_burningVfx.LocalPosition = new Vector3( 0f, 0f, yPos );
var boxEmitter = _burningVfx.GetComponent<ParticleBoxEmitter>();
boxEmitter.Size = new Vector3( Radius * 1.5f, Radius * 1.5f, Height * 0.25f );
Manager.Instance.PlaySfxNearby( "burn", Position2D, pitch: Game.Random.Float( 0.95f, 1.05f ), volume: 1f, maxDist: 280f );
}
else
{
if ( _burningVfx.IsValid() )
{
Manager.DestroyParticlesWhenFinished( _burningVfx );
_burningVfx = null;
}
}
}
[Rpc.Broadcast]
public void SetStatusFrozen( bool frozen )
{
if ( frozen == IsFrozen )
return;
IsFrozen = frozen;
if ( frozen )
{
TimeSinceFrozen = 0f;
_frozenVfx = GameObject.Clone( "prefabs/effects/unit_status_freeze.prefab", new CloneConfig { StartEnabled = true, Parent = GameObject } );
var yPos = Height * (ParticleYPosOverride > 0f ? ParticleYPosOverride : 0.95f);
_frozenVfx.LocalPosition = new Vector3( 0f, 0f, yPos );
var boxEmitter = _frozenVfx.GetComponent<ParticleBoxEmitter>();
boxEmitter.Size = new Vector3( Radius * 1.6f, Radius * 1.6f, Height * 1.2f );
}
else
{
if ( _frozenVfx.IsValid() )
{
Manager.DestroyParticlesWhenFinished( _frozenVfx );
_frozenVfx = null;
}
}
}
[Rpc.Broadcast]
public void SetStatusPoison( bool poisoned, bool playEffects = true )
{
if ( poisoned == IsPoisoned )
return;
IsPoisoned = poisoned;
if ( poisoned )
{
_poisonVfx = GameObject.Clone( "prefabs/effects/unit_status_poison.prefab", new CloneConfig { StartEnabled = true, Parent = GameObject } );
var yPos = Height * (ParticleYPosOverride > 0f ? ParticleYPosOverride : 0.85f);
_poisonVfx.LocalPosition = new Vector3( 0f, 0f, yPos );
var boxEmitter = _poisonVfx.GetComponent<ParticleBoxEmitter>();
boxEmitter.Size = new Vector3( Radius * 2f, Radius * 2f, Height * 0.5f );
Manager.Instance.PlaySfxNearby( "poisoned", Position2D, pitch: Game.Random.Float( 1.1f, 1.2f ), volume: 0.6f, maxDist: 350f );
}
else
{
if ( playEffects )
{
Manager.Instance.PlaySfxNearby( "splash", Position2D, pitch: Game.Random.Float( 1.2f, 1.3f ), volume: 0.95f, maxDist: 300f );
}
if ( _poisonVfx.IsValid() )
{
Manager.DestroyParticlesWhenFinished( _poisonVfx );
_poisonVfx = null;
}
}
}
[Rpc.Broadcast]
public void SetStateFear( bool fearful )
{
if ( fearful == IsFearful )
return;
IsFearful = fearful;
if ( fearful )
{
_fearVfx = GameObject.Clone( "prefabs/effects/unit_status_fear.prefab", new CloneConfig { StartEnabled = true, Parent = GameObject } );
var yPos = Height * (ParticleYPosOverride > 0f ? ParticleYPosOverride : 0.85f);
_fearVfx.LocalPosition = new Vector3( 0f, 0f, yPos );
var boxEmitter = _fearVfx.GetComponent<ParticleBoxEmitter>();
boxEmitter.Size = new Vector3( Radius * 2f, Radius * 2f, Height * 0.95f );
Manager.Instance.PlaySfxNearby( "fear", Position2D, pitch: Game.Random.Float(0.95f, 1.05f), volume: 0.7f, maxDist: 350f );
}
else
{
if ( _fearVfx.IsValid() )
{
Manager.DestroyParticlesWhenFinished( _fearVfx );
_fearVfx = null;
}
}
}
[Rpc.Broadcast]
public void SetStateMarked( bool marked )
{
if ( marked == IsMarked )
return;
IsMarked = marked;
if ( marked )
{
_markedVfx = GameObject.Clone( "prefabs/effects/unit_status_marked.prefab", new CloneConfig { StartEnabled = true, Parent = GameObject } );
var yPos = Height * (ParticleYPosOverride > 0f ? ParticleYPosOverride : 0.85f);
_markedVfx.LocalPosition = new Vector3( 0f, 0f, yPos );
var boxEmitter = _markedVfx.GetComponent<ParticleBoxEmitter>();
boxEmitter.Size = new Vector3( Radius * 2f, Radius * 2f, Height * 0.95f );
// todo: new sfx
Manager.Instance.PlaySfxNearby( "evil_cast", Position2D, pitch: Game.Random.Float( 3.15f, 3.45f ), volume: 1.2f, maxDist: 350f );
}
else
{
if ( _markedVfx.IsValid() )
{
Manager.DestroyParticlesWhenFinished( _markedVfx );
_markedVfx = null;
}
}
}
[Rpc.Broadcast]
public void SetStateShocked( bool shocked )
{
if ( shocked == IsShocked )
return;
IsShocked = shocked;
if ( shocked )
{
_shockVfx = GameObject.Clone( "prefabs/effects/unit_status_shock.prefab", new CloneConfig { StartEnabled = true, Parent = GameObject } );
var yPos = Height * (ParticleYPosOverride > 0f ? ParticleYPosOverride : 0.55f);
_shockVfx.LocalPosition = new Vector3( 0f, 0f, yPos );
var boxEmitter = _shockVfx.GetComponent<ParticleBoxEmitter>();
boxEmitter.Size = new Vector3( Radius * 2f, Radius * 2f, Height * 0.65f );
}
else
{
if ( _shockVfx.IsValid() )
{
Manager.DestroyParticlesWhenFinished( _shockVfx );
_shockVfx = null;
}
}
}
[Rpc.Broadcast]
public void SetStateShield( bool shielded, bool playEffects = true )
{
if ( shielded == IsShielded )
return;
IsShielded = shielded;
if ( shielded )
{
_shieldVfx = GameObject.Clone( "prefabs/effects/unit_status_shield.prefab", new CloneConfig { StartEnabled = true, Parent = GameObject } );
var yPos = Height * (ParticleYPosOverride > 0f ? ParticleYPosOverride : 0.7f);
_shieldVfx.LocalPosition = new Vector3( 0f, 0f, yPos );
Manager.Instance.PlaySfxNearby( "shield_gain", Position2D, pitch: Game.Random.Float( 1.2f, 1.25f ), volume: 0.75f, maxDist: 350f );
}
else
{
if( playEffects )
{
var pos = WorldPosition + new Vector3( 0f, 0f, Height * 0.7f );
var shieldBreakGo = GameObject.Clone( "prefabs/effects/unit_status_shield_break.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( pos ) } );
var particleRenderer = shieldBreakGo.GetComponent<ParticleSpriteRenderer>();
particleRenderer.Scale = 3f * Radius;
Manager.Instance.PlaySfxNearby( "shield_break", Position2D, pitch: Game.Random.Float( 0.95f, 1.05f ), volume: 1f, maxDist: 400f );
}
if ( _shieldVfx.IsValid() )
{
_shieldVfx.Destroy();
_shieldVfx = null;
}
}
}
[Rpc.Broadcast]
public void SetStateStunned( bool stunned )
{
if ( stunned == IsStunned )
return;
IsStunned = stunned;
if ( stunned )
{
_stunnedVfx = GameObject.Clone( "prefabs/effects/unit_status_stun.prefab", new CloneConfig { StartEnabled = true, Parent = GameObject } );
var yPos = Height * (StunParticleYPosOverride > 0f ? StunParticleYPosOverride : 1.25f);
_stunnedVfx.LocalPosition = new Vector3( 0f, 0f, yPos );
OnStun();
// todo: sfx
//Manager.Instance.PlaySfxNearby( "fear", Position2D, pitch: Game.Random.Float( 5.35f, 5.55f ), volume: 0.5f, maxDist: 350f );
var shakeStrength = 5f;
Shake( shakeStrength, 0f, 0.33f, EasingType.SineOut );
}
else
{
OnStunFinish();
if ( _stunnedVfx.IsValid() )
{
Manager.DestroyParticlesWhenFinished( _stunnedVfx );
_stunnedVfx = null;
}
}
}
[Rpc.Broadcast]
public void SetStateInvincible( bool invincible, bool playEffects = true )
{
if ( invincible == IsInvincible )
return;
IsInvincible = invincible;
if ( invincible )
{
var outline = ModelRenderer.AddComponent<HighlightOutline>();
outline.Color = Color.Yellow.WithAlpha( 0f );
outline.InsideColor = Color.Yellow.WithAlpha( 0f );
outline.Width = 0.15f;
FadeInInvincibleOutline( outline );
//Manager.Instance.PlaySfxNearby( "shield_gain", Position2D, pitch: Game.Random.Float( 1.8f, 1.85f ), volume: 0.6f, maxDist: 320f );
}
else
{
var outline = ModelRenderer.GetComponent<HighlightOutline>();
if ( outline.IsValid() )
{
if ( playEffects )
{
FadeOutInvincibleOutline( outline );
}
else
{
outline.Destroy();
outline = null;
}
}
//if( playEffects )
//{
// Manager.Instance.PlaySfxNearby( "shield_break", Position2D, pitch: Game.Random.Float( 1.35f, 1.45f ), volume: 0.5f, maxDist: 320f );
//}
}
}
async void FadeInInvincibleOutline( HighlightOutline outline )
{
const float DURATION = 0.1f;
float elapsed = 0f;
while ( elapsed < DURATION )
{
if ( !outline.IsValid() ) return;
if ( !IsInvincible ) { outline.Destroy(); return; }
elapsed += Time.Delta;
float t = MathF.Min( elapsed / DURATION, 1f );
outline.Color = Color.Yellow.WithAlpha( 0.4f * t );
outline.InsideColor = Color.Yellow.WithAlpha( 0.05f * t );
await Task.Frame();
}
}
async void FadeOutInvincibleOutline( HighlightOutline outline )
{
const float DURATION = 0.1f;
var startColor = outline.Color;
var startInsideColor = outline.InsideColor;
float elapsed = 0f;
while ( elapsed < DURATION )
{
if ( !outline.IsValid() ) return;
if ( IsInvincible ) { outline.Destroy(); return; }
elapsed += Time.Delta;
float t = MathF.Min( elapsed / DURATION, 1f );
outline.Color = startColor.WithAlpha( startColor.a * (1f - t) );
outline.InsideColor = startInsideColor.WithAlpha( startInsideColor.a * (1f - t) );
await Task.Frame();
}
outline?.Destroy();
}
[Rpc.Broadcast]
public void SetStatusChained( bool chained, bool playEffects = true )
{
if ( chained == IsChained )
return;
IsChained = chained;
if ( chained )
{
//if ( _chainedVfxs == null )
// _chainedVfxs = new();
//var chainedVfx = GameObject.Clone( "prefabs/effects/unit_status_chained.prefab", new CloneConfig { StartEnabled = true, Parent = GameObject } ).GetComponent<UnitChainedVfx>();
//chainedVfx.ChainedUnit = this;
//_chainedVfxs.Add( chainId, chainedVfx );
//Manager.Instance.PlaySfxNearby( "poisoned", Position2D, pitch: Game.Random.Float( 1.1f, 1.2f ), volume: 0.6f, maxDist: 350f );
}
else
{
if ( playEffects )
{
//Manager.Instance.PlaySfxNearby( "splash", Position2D, pitch: Game.Random.Float( 1.2f, 1.3f ), volume: 0.95f, maxDist: 300f );
}
//if ( ChainedVfx.IsValid() )
//{
// ChainedVfx.GameObject.Destroy();
// ChainedVfx = null;
//}
}
}
[Rpc.Broadcast]
public void AddChainRpc( int chainId, float lifetime, float chainLength )
{
if ( _chainedVfxs == null )
_chainedVfxs = new();
var chainedVfx = GameObject.Clone( "prefabs/effects/unit_status_chained.prefab", new CloneConfig { StartEnabled = true, Parent = GameObject } ).GetComponent<UnitChainedVfx>();
chainedVfx.ChainedUnit = this;
chainedVfx.Lifetime = lifetime;
chainedVfx.ChainLength = chainLength;
_chainedVfxs.Add( chainId, chainedVfx );
}
[Rpc.Broadcast]
public void RemoveChainRpc( int chainId )
{
if ( _chainedVfxs != null && _chainedVfxs.ContainsKey( chainId ) )
{
var chainedVfx = _chainedVfxs[chainId];
if ( chainedVfx.IsValid() )
chainedVfx.GameObject.Destroy();
_chainedVfxs.Remove( chainId );
}
}
[Rpc.Broadcast]
public void SetChainAnchorPosRpc( int chainId, Vector2 pos )
{
var chainedVfx = _chainedVfxs[chainId];
if ( chainedVfx.IsValid() )
chainedVfx.SetAnchorPos( pos );
}
[Rpc.Broadcast]
public void SetChainAnchorUnitRpc( int chainId, Unit unit )
{
var chainedVfx = _chainedVfxs[chainId];
if ( chainedVfx.IsValid() )
chainedVfx.SetAnchorUnit( unit );
}
}