Enemy component for a miniboss called MinibossAbsorber. It defines stats, movement and an absorb state machine where the miniboss can enter prepare/absorb/finish states, heal while absorbing damage, adjust acceleration based on health and broadcast RPCs for state transitions and animations.
using System;
using Sandbox;
public class MinibossAbsorber : Enemy
{
public override EnemyType EnemyType => EnemyType.MinibossAbsorber;
public override string GibFolder => "miniboss_absorber";
public override float OverrideGibChance => 1f;
public override int ExtraDeathBloodSprayAmount => 25;
protected override float MinibossHealthScale => 0.85f;
public override float GetMaxHealth() => MinibossBaseHealth * MinibossHealthScale;
public override Vector3 SpawnScale => new Vector3( 1.6f );
public override bool ShowHealthbar => true;
public override float HealthbarOffset => 100f;
public override float HealthbarOpacity => Utils.EasePercent( SpawnProgress, EasingType.QuadIn );
public override float HealthbarArmorOpacity => Utils.EasePercent( SpawnProgress, EasingType.QuadIn );
public override bool IsBoss => true;
public override bool IsMiniboss => true;
// todo: should targer player when boss has spawned?
public override bool CanTurn => base.CanTurn && !IsDying && State == MinibossAbsorberState.Default;
public override bool CanMove => base.CanMove && State == MinibossAbsorberState.Default;
public override bool CanAttack => base.CanAttack && State == MinibossAbsorberState.Default;
public override bool CanDamageByTouch => !IsDying && !IsStunned && !IsInTheAir && State == MinibossAbsorberState.Default;
private float _absorbDelayTimer;
private const float ABSORB_DELAY_MIN = 3f;
private const float ABSORB_DELAY_MAX = 12f;
private float _absorbDuration;
private const float ABSORB_DURATION_MIN = 1f;
private const float ABSORB_DURATION_MAX = 6.75f;
private const float ACCELERATION_MIN = 350f;
private const float ACCELERATION_MAX = 450f;
public override float ParticleYPosOverride => 0.5f;
public override float StunParticleYPosOverride => 0.8f;
protected enum MinibossAbsorberState
{
Default,
AbsorbPrepare,
Absorb,
AbsorbFinish,
}
[Sync] protected MinibossAbsorberState State { get; private set; } = MinibossAbsorberState.Default;
protected override void OnStart()
{
base.OnStart();
CoinValueMin = 10;
CoinValueMax = 20;
CoinChance = 1f;
PushStrength = 9000f;
Weight = 1.5f;
_personalSpeedScale = 1.5f;
_personalSpeedFreq = 12f;
if ( IsProxy )
return;
AggroRange = 150f;
DetectTargetRange = 850f;
LoseTargetRange = 1300f;
LoseTargetTime = 5f;
MeleeDamage = Utils.Select( Manager.Instance.Difficulty, 12f, 15f, 16f );
DamageTargetDelay = 0.8f;
Acceleration = ACCELERATION_MIN * Utils.Select( Manager.Instance.Difficulty, 0.9f, 1f, 1.05f );
AccelerationAttacking = ACCELERATION_MIN * 1.4f * Utils.Select( Manager.Instance.Difficulty, 0.9f, 1f, 1.05f );
Deceleration = 1.3f;
DecelerationAttacking = 1.1f;
_personalTurnSpeed = 4.5f;
_absorbDelayTimer = Game.Random.Float( ABSORB_DELAY_MIN, ABSORB_DELAY_MAX );
}
protected override void OnUpdate()
{
base.OnUpdate();
//Gizmo.Draw.Color = Color.White;
//Gizmo.Draw.Text( $"{Acceleration}", new global::Transform( WorldPosition ) );
if ( Manager.Instance.IsGameOver )
return;
_personalSpeedScale = Utils.Map( Health, MaxHealth, 0f, 1.4f, 1.8f, EasingType.Linear );
if ( IsProxy )
return;
if ( !IsStunned && !IsDying )
HandleState();
}
protected void HandleState()
{
switch ( State )
{
case MinibossAbsorberState.Default:
_absorbDelayTimer -= Time.Delta;
if ( _absorbDelayTimer < 0f && !IsInTheAir )
SetState( MinibossAbsorberState.AbsorbPrepare );
break;
case MinibossAbsorberState.AbsorbPrepare:
Velocity *= (1f - Time.Delta * 6f * Manager.Instance.GlobalFrictionModifier);
if ( _timeSinceChangeState > 0.33f )
SetState( MinibossAbsorberState.Absorb );
break;
case MinibossAbsorberState.Absorb:
if ( _timeSinceChangeState > _absorbDuration )
SetState( MinibossAbsorberState.AbsorbFinish );
break;
case MinibossAbsorberState.AbsorbFinish:
if ( _timeSinceChangeState > 0.4f )
SetState( MinibossAbsorberState.Default );
break;
}
}
protected void SetState( MinibossAbsorberState state )
{
State = state;
_timeSinceChangeState = 0f;
switch ( state )
{
case MinibossAbsorberState.Default:
EnterDefaultStateRpc();
break;
case MinibossAbsorberState.AbsorbPrepare:
AbsorbPrepareRpc();
Velocity *= 0.5f;
_absorbDuration = Game.Random.Float( ABSORB_DURATION_MIN, ABSORB_DURATION_MAX );
_absorbDelayTimer = Game.Random.Float( ABSORB_DELAY_MIN, ABSORB_DELAY_MAX * Utils.Map( HpPercent, 1f, 0f, 1f, 0.7f ) );
//SetPlaybackRate( Utils.MapReturn( _prepareSlamTime, 0f, 1f, 0.8f, 0f, EasingType.QuadIn ) );
break;
case MinibossAbsorberState.Absorb:
AbsorbRpc();
break;
case MinibossAbsorberState.AbsorbFinish:
AbsorbFinishRpc();
break;
}
}
protected override float GetMoveSpeedFactor()
{
var progress = ModelRenderer.SceneModel.CurrentSequence.TimeNormalized;
return Zombie.GetZombieMoveSpeedFactor( progress );
}
[Rpc.Broadcast]
void AbsorbPrepareRpc()
{
CanAnimate = false;
SetAnim( "Pray" );
//SS2Game.PlaySfx( "spitter.prepare", Position, pitch: Game.Random.Float( 1f, 1.1f ), volume: 0.6f );
}
[Rpc.Broadcast]
void AbsorbRpc()
{
//SetAnim( "Pray" );
//SS2Game.PlaySfx( "spitter.prepare", Position, pitch: Game.Random.Float( 1f, 1.1f ), volume: 0.6f );
}
[Rpc.Broadcast]
void AbsorbFinishRpc()
{
PlayWalkAnim();
//SetAnim( "Pray" );
//SS2Game.PlaySfx( "spitter.prepare", Position, pitch: Game.Random.Float( 1f, 1.1f ), volume: 0.6f );
}
protected override void Damage( float damage, Player player, DamageType damageType, Vector3 hitPos, Vector2 force, bool isCrit = false, bool shouldFlinch = true, DamageResultFlags damageFlags = DamageResultFlags.None )
{
if( State == MinibossAbsorberState.Absorb )
{
if( !IsProxy )
{
Heal( damage, playSfx: true );
// todo: sfx
RefreshAcceleration();
}
var scaleMultiplier = Utils.Map( damage, 1f, 5f, 0.5f, 1f, EasingType.Linear ) * Utils.Map( damage, 5f, 30f, 1f, 1.5f, EasingType.Linear );
Manager.Instance.SpawnBulletImpactParticlesRpc( hitPos, Vector3.Up, Color.Green, scaleMultiplier ); // todo: different impact effect
return;
}
base.Damage( damage, player, damageType, hitPos, force, isCrit, shouldFlinch, damageFlags );
if ( IsProxy )
return;
RefreshAcceleration();
}
void RefreshAcceleration()
{
Acceleration = Utils.Map( Health, MaxHealth, 0f, ACCELERATION_MIN, ACCELERATION_MAX, EasingType.Linear ) * Utils.Select( Manager.Instance.Difficulty, 0.9f, 1f, 1.05f );
AccelerationAttacking = Acceleration * 1.25f;
}
public override void OnStun()
{
base.OnStun();
PlayFlinchAnim();
if( State != MinibossAbsorberState.Absorb )
SetState( MinibossAbsorberState.Default );
}
[Rpc.Broadcast]
public void EnterDefaultStateRpc()
{
CanAnimate = true;
PlayWalkAnim();
}
}