Boss enemy component. Extends Enemy, defines boss-specific stats, states and behaviors (shooting, charging, jumping, barrage, chain, sword, invincibility), handles state transitions, movement, animation velocity syncing and death/loot hooks.
using System;
using Sandbox;
using Sandbox.Citizen;
using Sandbox.UI;
public partial class Boss : Enemy
{
public override EnemyType EnemyType => EnemyType.Boss;
public override float MeleeForce => 30f;
public override float MeleeRagdollForce => Game.Random.Float( 1f, 2.5f );
public override float MeleeUpwardForceAmount => State == BossState.Charge
? Game.Random.Float( 0.25f, 2f )
: Game.Random.Float( 0f, 0.75f );
public override float GetMaxHealth()
{
switch ( Manager.Instance.Difficulty )
{
case 0: default: return 4000f;
case 1: return 5000f;
case 2: return 7000f;
}
}
//[Property] public CitizenAnimationHelper AnimationHelper { get; set; }
public override Vector3 SpawnScale => new Vector3( 1f );
public override float SpawnZPos => -250f;
public override float OverrideGibChance => 1f;
//override public float GibScaleMultiplier => 3f;
public override float GibOffsetMultiplier => 2f;
public override float OverrideGibLifetime => 9999f;
public override bool CanAttack => base.CanAttack && State == BossState.Default;
public override bool CanMove => base.CanMove && (State == BossState.Default || State == BossState.Charge) ;
public override bool CanTurn => base.CanTurn && (State == BossState.Default || State == BossState.Charge || State == BossState.ShootPrepare || State == BossState.Shoot );
public override bool CanDamageByTouch => !IsDying && !IsStunned && (IsAttacking || State == BossState.Charge) && !IsInTheAir;
public override bool CanBeStunned => base.CanBeStunned && !( State == BossState.InvinciblePrepare );
protected override bool ShouldRetreatFromTarget => IsFearful || (_isRetreating && !IsAttacking && !(State == BossState.ShootPrepare || State == BossState.Shoot || State == BossState.ChainPrepare || State == BossState.ChainThrow));
protected virtual bool ShouldCircleTarget => !IsAttacking && !_isRetreating && _timeSinceJumpingAway < _circleTargetTime;
protected bool _moveClockwise;
private float _circleTargetTime;
public bool IsReadyForAction => State == BossState.Default && !IsInTheAir && !IsSpawning && !IsStunned;
private bool _isRetreating;
private float _retreatTimer;
private TimeSince _timeSinceRetreat;
private const float RETREAT_TIME_MIN = 1f;
private const float RETREAT_TIME_MAX = 6f;
[Sync] public Vector2 AnimVelocity { get; set; }
protected float _baseWeight;
public override bool IsBoss => true;
public override float ParticleYPosOverride => 0.65f;
public override float StunParticleYPosOverride => 1.4f;
public override bool CanCombust => false;
protected enum BossState
{
Default,
ShootPrepare,
Shoot,
ShootFinish,
ChargePrepare,
Charge,
ChargeFinish,
JumpPrepare,
Jump,
JumpFinish,
JumpAwayPrepare,
JumpAway,
BarragePrepare,
Barrage,
BarrageFinish,
InvinciblePrepare,
Invincible,
ChainPrepare,
ChainThrow,
ChainFinish,
SwordPrepare,
Sword,
SwordFinish,
// todo: new behaviour when shield generators destroyed
// PuddlePrepare,
}
protected BossState State { get; private set; } = BossState.Default;
protected override void OnStart()
{
base.OnStart();
//CanAnimate = false;
CoinValueMin = 0;
CoinValueMax = 0;
CoinChance = 0f;
PushStrength = 15000f;
Weight = _baseWeight = 10f;
_personalSpeedScale = 1f;
_personalSpeedFreq = 1f;
InitShooting();
InitCharging();
InitJumping();
InitBarrage();
InitInvincible();
InitChain();
InitSword();
//ModelRenderer.Morphs.Set( "openjawL", 1f );
//ModelRenderer.Morphs.Set( "openjawR", 1f );
//ModelRenderer.Morphs.Set( "browlowererL", 1f );
//ModelRenderer.Morphs.Set( "browlowererR", 1f );
//AnimationHelper.Target.Set( "face_override", 1 );
//ModelRenderer.Morphs.Clear( "openjawL" );
//ModelRenderer.Morphs.Clear( "openjawR" );
//AnimationHelper.DuckLevel = 1f;
Manager.Instance.Boss = this;
ShouldCheckBounds = false;
if ( IsProxy )
return;
AggroRange = 80f;
DetectTargetRange = 1500f;
LoseTargetRange = 1100f;
LoseTargetTime = 12f;
MeleeDamage = Utils.Select( Manager.Instance.Difficulty, 8f, 12f, 13f );
DamageTargetDelay = 0.55f;
_personalTurnSpeed = 5f;
Acceleration = Utils.Select( Manager.Instance.Difficulty, 250f, 310f, 335f );
AccelerationAttacking = Utils.Select( Manager.Instance.Difficulty, 280f, 330f, 355f );
Deceleration = 1.7f;
DecelerationAttacking = 1.6f;
_moveClockwise = Game.Random.Int( 0, 1 ) == 0;
}
protected override void OnUpdate()
{
base.OnUpdate();
//Gizmo.Draw.Color = Color.White;
//Gizmo.Draw.Text( $"_jumpTargetPos: {_jumpTargetPos}", new global::Transform( WorldPosition ) );
//Gizmo.Draw.Text( $"{ModelRenderer.SceneModel.CurrentSequence.Name}\nprogress: {ModelRenderer.SceneModel.CurrentSequence.TimeNormalized.ToString( "N2" )}\nspeed: {GetMoveSpeedFactor().ToString( "N2" )}", new global::Transform( WorldPosition ) );
if ( Manager.Instance.IsGameOver )
return;
HandleAnimation();
if ( IsProxy )
return;
if ( !IsStunned )
HandleState();
HandleRetreating();
AnimVelocity = (IsSpawning || State != BossState.Default) ? Vector2.Zero : Velocity * 0.75f;
}
protected override void FinishSpawning()
{
base.FinishSpawning();
ShouldCheckBounds = true;
if ( IsProxy )
return;
var pos = new Vector2( Game.Random.Float( -300f, 300f ), Game.Random.Float( -300f, 300f ) );
WorldRotation = Rotation.LookAt( ( (Vector3)pos - WorldPosition).Normal, Vector3.Up );
JumpRpc( pos, height: Game.Random.Float( 250f, 400f ), lifetime: Game.Random.Float( 1.3f, 1.5f ) );
}
protected void HandleState()
{
HandleInvincible();
switch ( State )
{
case BossState.Default:
HandleShooting();
HandleCharging();
HandleJumping();
HandleBarrage();
HandleChain();
HandleSword();
break;
case BossState.ShootPrepare:
if ( _timeSinceChangeState > 1f ) // todo: add more variation
SetState( BossState.Shoot );
break;
case BossState.Shoot:
if ( _timeSinceChangeState > 0.6f )
SetState( BossState.ShootFinish );
break;
case BossState.ShootFinish:
break;
case BossState.ChargePrepare:
if ( _timeSinceChangeState > 0.75f )
SetState( BossState.Charge );
break;
case BossState.Charge:
if ( _timeSinceRedirect > _nextRedirectTime )
{
Player closestPlayer = Manager.Instance.GetClosestPlayer( Position2D );
if ( closestPlayer.IsValid() )
{
var targetPos = closestPlayer.Position2D + closestPlayer.Velocity * Game.Random.Float( 0.2f, 1f ) + new Vector2( Game.Random.Float( -1f, 1f ), Game.Random.Float( -1f, 1f ) ) * 100f;
Vector2 targetDir = (targetPos - Position2D).LengthSquared > Manager.TOUCH_DIST_REQUIRED_SQR
? (targetPos - Position2D).Normal
: Utils.GetRandomVector();
_chargeDir = Utils.RotateVector( targetDir, Game.Random.Float( -10f, 10f ) );
}
_nextRedirectTime = Game.Random.Float( 0.25f, 1f );
_timeSinceRedirect = 0f;
}
_chargeVel += (Vector2)WorldRotation.Forward * 650f * Utils.MapReturn( _timeSinceChangeState, 0f, _chargeTime, 0.5f, 1f, EasingType.QuadOut ) * Time.Delta;
if ( Manager.Instance.IsWindActive )
Velocity += Manager.Instance.GlobalWindForce * Time.Delta;
WorldPosition += (Vector3)(_chargeVel + Velocity) * Time.Delta;
Velocity *= Math.Max( 1f - Time.Delta * Deceleration * Manager.Instance.GlobalFrictionModifier, 0f );
_chargeVel *= Math.Max( 1f - Time.Delta * 1.5f, 0f );
// todo: wind?
if ( _timeSinceChangeState > _chargeTime )
SetState( BossState.ChargeFinish );
break;
case BossState.ChargeFinish:
SetState( BossState.Default );
break;
case BossState.JumpPrepare:
var dir = (_jumpTargetPos - Position2D).Normal;
WorldRotation = Rotation.Lerp( WorldRotation, Rotation.LookAt( dir ), 10f * Time.Delta * TimeScale );
if ( _timeSinceChangeState > _prepareJumpTime )
SetState( BossState.Jump );
break;
case BossState.Jump:
break;
case BossState.JumpFinish:
if ( _timeSinceChangeState > 0.5f )
SetState( BossState.Default );
break;
case BossState.JumpAwayPrepare:
var dirAway = (_jumpTargetPos - Position2D).Normal;
WorldRotation = Rotation.Lerp( WorldRotation, Rotation.LookAt( dirAway ), 10f * Time.Delta * TimeScale );
if ( _timeSinceChangeState > _prepareJumpTime )
SetState( BossState.JumpAway );
break;
case BossState.JumpAway:
break;
case BossState.BarragePrepare:
if ( _timeSinceChangeState > 1f )
SetState( BossState.Barrage );
break;
case BossState.Barrage:
if(_timeSinceBarrageShoot > _barrageEmitDelay )
{
BarrageEmitRpc();
_timeSinceBarrageShoot = 0f;
}
if ( _timeSinceChangeState > _barrageTotalTime )
SetState( BossState.BarrageFinish);
break;
case BossState.BarrageFinish :
break;
case BossState.InvinciblePrepare:
//AnimationHelper.WithLook( Vector3.Random );
if ( _timeSinceChangeState > 2.5f )
SetState( BossState.Invincible );
break;
case BossState.Invincible:
break;
case BossState.ChainPrepare:
if ( _timeSinceChangeState > 1.5f )
SetState( BossState.ChainThrow );
break;
case BossState.ChainThrow:
if ( _timeSinceChangeState > 1.75f )
SetState( BossState.ChainFinish );
break;
case BossState.ChainFinish:
break;
case BossState.SwordPrepare:
if ( _timeSinceChangeState > 1f )
SetState( BossState.Sword );
break;
case BossState.Sword:
if ( _timeSinceSwordShoot > _swordEmitDelay )
{
SwordEmitRpc();
_timeSinceSwordShoot = 0f;
_swordEmitDelay = Game.Random.Float( _swordEmitDelayMin, _swordEmitDelayMax ) * Utils.Map( HpPercent, 1f, 0f, 1f, 0.75f ) * Utils.Select( Manager.Instance.Difficulty, 1.25f, 1f, 0.925f );
}
if ( _timeSinceChangeState > _swordTotalTime )
SetState( BossState.SwordFinish );
break;
case BossState.SwordFinish:
break;
}
}
protected void SetState( BossState state )
{
State = state;
_timeSinceChangeState = 0f;
switch ( state )
{
case BossState.Default:
EnterDefaultStateRpc();
break;
case BossState.ShootPrepare:
_shootDelayTimer = Game.Random.Float( _shootDelayMin, _shootDelayMax ) * Utils.Select( Manager.Instance.Difficulty, 1.25f, 1f, 0.925f );
StartShootingRpc();
_isRetreating = false;
break;
case BossState.Shoot:
ShootRpc();
break;
case BossState.ShootFinish:
Velocity = Vector2.Zero;
SetState( BossState.Default );
break;
case BossState.ChargePrepare:
_chargeDelayTimer = Game.Random.Float( _chargeDelayMin, _chargeDelayMax ) * Utils.Select( Manager.Instance.Difficulty, 1.25f, 1f, 0.925f );
PrepareToChargeRpc();
Velocity *= 0.7f;
break;
case BossState.Charge:
ChargeRpc();
break;
case BossState.ChargeFinish:
SetState( BossState.Default );
break;
case BossState.JumpPrepare:
PrepareJumpRpc();
_timeSinceJumping = 0f;
_delayUntilNextJump = Game.Random.Float( _nextJumpDelayMin, _nextJumpDelayMax ) * Utils.Map( HpPercent, 1f, 0f, Utils.Select( Manager.Instance.Difficulty, 2.3f, 2.1f, 2f ), 1f ) * Utils.Select( Manager.Instance.Difficulty, 1.75f, 1.4f, 1f );
_delayUntilNextJumpAway += Game.Random.Float( 2f, 3f );
_prepareJumpTime = Game.Random.Float( 0.3f, 0.6f );
_jumpTargetPos = Manager.Instance.ClampPosToBounds( TargetUnit.Position2D + TargetUnit.Velocity * Game.Random.Float( 0f, 4f ) + Utils.GetRandomVector() * Game.Random.Float( 0f, 250f ) );
if ( (_jumpTargetPos - Position2D).LengthSquared > MathF.Pow( _maxJumpDist, 2f ) )
_jumpTargetPos = Position2D + (_jumpTargetPos - Position2D).Normal * _maxJumpDist;
_jumpTargetPos = Manager.Instance.ClampPosToBounds( _jumpTargetPos );
IsAttacking = false;
break;
case BossState.Jump:
//SetState( BossState.Default );
JumpRpc(
_jumpTargetPos,
height: Game.Random.Float( 250f, 300f ),
lifetime: Utils.Map( (_jumpTargetPos - Position2D).Length, 0f, _maxJumpDist, 1f, 2f, EasingType.SineIn ) * Game.Random.Float( 0.85f, 1.1f )
);
break;
case BossState.JumpFinish:
break;
case BossState.JumpAwayPrepare:
PrepareJumpRpc();
_timeSinceJumpingAway = 0f;
_delayUntilNextJumpAway = Game.Random.Float( _nextJumpAwayDelayMin, _nextJumpAwayDelayMax ) * Utils.Map( HpPercent, 1f, 0f, 2f, 1f ) * Utils.Select( Manager.Instance.Difficulty, 1.25f, 1.1f, 0.95f );
_delayUntilNextJump += 2f;
_prepareJumpTime = Game.Random.Float( 0.3f, 0.6f );
_jumpTargetPos = Manager.Instance.GetRandomSpawnPos( buffer: 20f );
int numTries = 0;
while ( numTries < 40 )
{
if ( (_jumpTargetPos - TargetUnit.Position2D).LengthSquared > MathF.Pow( Utils.Map( numTries, 0, 40, 1500f, 800f ), 2f ) )
break;
var jumpAwayPos = Manager.Instance.GetRandomSpawnPos( buffer: 20f );
if ( (jumpAwayPos - TargetUnit.Position2D).LengthSquared > (_jumpTargetPos - TargetUnit.Position2D).LengthSquared )
_jumpTargetPos = jumpAwayPos;
numTries++;
}
_jumpTargetPos = Manager.Instance.ClampPosToBounds( _jumpTargetPos );
IsAttacking = false;
break;
case BossState.JumpAway:
//SetState( BossState.Default );
JumpRpc(
_jumpTargetPos,
height: Game.Random.Float( 350f, 400f ),
lifetime: Utils.Map( (_jumpTargetPos - Position2D).Length, 0f, 2000f, 1f, 2.5f, EasingType.SineIn ) * Game.Random.Float( 0.85f, 1.1f )
);
_moveClockwise = !_moveClockwise;
_circleTargetTime = Game.Random.Float( 1f, 10f );
break;
case BossState.BarragePrepare:
_barrageDelayTimer = Game.Random.Float( _barrageDelayMin, _barrageDelayMax ) * Utils.Select( Manager.Instance.Difficulty, 1.45f, 1f, 0.925f );
StartBarrageRpc();
break;
case BossState.Barrage:
BarrageRpc();
break;
case BossState.BarrageFinish:
SetState( BossState.Default );
break;
case BossState.InvinciblePrepare:
StartInvincibleRpc();
break;
case BossState.Invincible:
FinishInvincibleRpc();
SetState( BossState.Default );
break;
case BossState.ChainPrepare:
_chainDelayTimer = Game.Random.Float( _chainDelayMin, _chainDelayMax ) * Utils.Select( Manager.Instance.Difficulty, 3f, 1.1f, 0.95f );
StartChainRpc();
_isRetreating = false;
break;
case BossState.ChainThrow:
ThrowChainRpc();
break;
case BossState.ChainFinish:
Velocity = Vector2.Zero;
SetState( BossState.Default );
break;
case BossState.SwordPrepare:
_swordDelayTimer = Game.Random.Float( _swordDelayMin, _swordDelayMax ) * Utils.Select( Manager.Instance.Difficulty, 1.3f, 1f, 0.925f );
StartSwordRpc();
break;
case BossState.Sword:
SwordRpc();
break;
case BossState.SwordFinish:
SetState( BossState.Default );
break;
}
}
protected override float GetMoveSpeedFactor()
{
var leftFootStart = 0.30f;
var leftFootEnd = 0.70f;
var rightFootStart = 0.70f;
var rightFootEnd = 0.25f;
var progress = ModelRenderer.SceneModel.CurrentSequence.TimeNormalized;
if ( progress > rightFootStart || progress < rightFootEnd )
{
var totalProgress = 1f + rightFootEnd;
var offsetProgress = progress < rightFootEnd
? 1f + progress
: progress;
return Utils.Map( offsetProgress, rightFootStart, totalProgress, 0f, 1f, EasingType.QuadOut );
}
else if ( progress > leftFootStart && progress < leftFootEnd )
{
return Utils.Map( progress, leftFootStart, leftFootEnd, 0f, 1f, EasingType.QuadOut );
}
return 0f;
}
protected override Vector2 GetTargetOffset()
{
var offset = TargetUnit.Velocity * (0.5f + Utils.FastSin( TimeSinceSpawn * 3f ) * 0.5f) * (TargetUnit.Position2D - Position2D).Length * 0.012f;
//if ( !IsShooting && !_isRetreating )
// offset += _personalTargetOffset + new Vector2( Utils.FastSin( TimeSinceSpawn * 0.7f ), Utils.FastSin( TimeSinceSpawn * 1.1f ) ) * 50f;
return offset;
}
protected override void HandleRotation()
{
if ( State == BossState.Charge )
{
if ( !HitstopActive )
WorldRotation = Rotation.Lerp( WorldRotation, Rotation.From( new Angles( 0f, -Utils.GetAngleDegreesFromVector( _chargeDir ), 0f ) ), _chargeRotateSpeed * Time.Delta * TimeScale );
}
else
{
if ( !HasTarget || State != BossState.Default || !ShouldCircleTarget )
{
base.HandleRotation();
return;
}
Vector2 toTarget = TargetPos - Position2D;
toTarget = new Vector2( toTarget.y, -toTarget.x );
WorldRotation = Rotation.Lerp( WorldRotation, Rotation.LookAt( (Vector3)toTarget ), _personalTurnSpeed * Time.Delta * TimeScale );
}
}
public override void Die( Vector2 dir, float force, Player player, DamageType damageType )
{
base.Die( dir, force, player, damageType );
Manager.Instance.PlaySfxNearby( "boss.die", Position2D, pitch: Game.Random.Float( 0.75f, 0.8f ), volume: 1.3f, maxDist: 2500f );
if ( IsProxy )
return;
Manager.Instance.BossDied();
}
protected override void DropLoot( Player player )
{
// todo: drop crown?
}
public override void Flash( float time, UnitFlashType flashType )
{
if ( flashType == UnitFlashType.EnemyDmg )
flashType = UnitFlashType.BossDmg;
base.Flash( time, flashType );
}
void HandleRetreating()
{
// todo: don't retreat if at stage bounds (player walking you into the edge of arena)
if ( _isRetreating )
{
_retreatTimer -= Time.Delta;
if ( _retreatTimer <= 0f )
_isRetreating = false;
}
}
public override void StartAttacking()
{
base.StartAttacking();
if ( IsProxy )
return;
_isRetreating = false;
_timeSinceRetreat = 0f;
}
protected override void Damage( float damage, Player player, DamageType damageType, Vector3 hitPos, Vector2 force, bool isCrit = false, bool shouldFlinch = true, DamageResultFlags damageFlags = DamageResultFlags.None )
{
shouldFlinch = false;
base.Damage( damage, player, damageType, hitPos, force, isCrit, shouldFlinch, damageFlags );
if ( IsProxy || IsDying )
return;
if ( (_timeSinceRetreat > Game.Random.Float( 3f, 10f ) * Utils.Map( HpPercent, 1f, 0f, 1.5f, 0.8f )) && Game.Random.Float( 0f, 1f ) < Utils.Map( HpPercent, 1f, 0f, 0f, 0.5f ) )
{
_isRetreating = true;
_retreatTimer = Game.Random.Float( RETREAT_TIME_MIN, RETREAT_TIME_MAX );
_timeSinceRetreat = 0f;
Vector2 impulseDir;
if( player.IsValid() )
{
impulseDir = (Position2D - player.Position2D).LengthSquared > Manager.TOUCH_DIST_REQUIRED_SQR
? (Position2D - player.Position2D).Normal
: Utils.GetRandomVector();
}
else
{
impulseDir = force.Normal;
}
if ( impulseDir.LengthSquared > 0f )
Velocity += impulseDir * Game.Random.Float( 50f, 200f ) * TimeScale;
}
}
public override void OnStun()
{
base.OnStun();
PlayFlinchAnim();
SetState( BossState.Default );
}
protected override void PlayFlinchAnim()
{
}
[Rpc.Broadcast( NetFlags.Reliable )]
public void EnterDefaultStateRpc()
{
Weight = _baseWeight;
CanAnimate = true;
PlayWalkAnim();
//AnimationHelper.DuckLevel = 0f;
SetPlaybackRate( 1f );
//AnimationHelper.Sitting = CitizenAnimationHelper.SittingStyle.None;
//AnimationHelper.SpecialMove = CitizenAnimationHelper.SpecialMoveStyle.None;
//AnimationHelper.HoldType = CitizenAnimationHelper.HoldTypes.None;
//AnimationHelper.WithLook( WorldRotation.Forward );
}
protected override void SpawnGibs( Vector2 dir, float force, DamageType damageType )
{
base.SpawnGibs( dir, force, damageType );
SpawnGoreGib(
$"{GibFolder}/crown",
localPos: new Vector3( 0f, 0, 60f ) * GibOffsetMultiplier,
localRot: new Angles( 0f, 0f, 0f ),
scaleMultiplier: GibScaleMultiplier,
dir,
force,
Color.White,
damageType
);
}
public override void Celebrate( bool victory )
{
SetState( BossState.Default );
AnimVelocity = Vector2.Zero;
base.Celebrate( victory );
}
}