Enemy subclass for a miniboss that throws fireballs and explodes, it configures stats, handles state machine for preparing/shooting fireballs, spawns projectile prefabs, and creates an explosion and ground fire on death.
using System;
using System.Collections.Generic;
using Sandbox;
public class MinibossExploder : Exploder
{
public override EnemyType EnemyType => EnemyType.MinibossExploder;
public override string GibFolder => "miniboss_exploder";
public override float OverrideGibChance => 1f;
public override int ExtraDeathBloodSprayAmount => 25;
protected override float MinibossHealthScale => 1.25f;
public override float GetMaxHealth() => MinibossBaseHealth * MinibossHealthScale;
public override Vector3 SpawnScale => new Vector3( 1.6f );
public override bool ShowHealthbar => true;
public override float HealthbarOffset => 105f;
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;
public override bool CanAttack => base.CanAttack && State == MinibossExploderState.Default;
public override bool CanAccelerate => State == MinibossExploderState.Default;
protected float _shootDelayTimer;
protected float _shootDelayMin;
protected float _shootDelayMax;
protected float _shootRange;
public override float ParticleYPosOverride => 0.7f;
public override float StunParticleYPosOverride => 1.1f;
protected enum MinibossExploderState
{
Default,
ShootPrepare,
Shoot,
ShootFinish,
}
protected MinibossExploderState State { get; private set; } = MinibossExploderState.Default;
protected override void OnStart()
{
base.OnStart();
CoinValueMin = 10;
CoinValueMax = 18;
CoinChance = 1f;
PushStrength = 9000f;
Weight = 1.8f;
_personalSpeedScale = 1f;
_personalSpeedFreq = Game.Random.Float( 6f, 7f );
_personalExplosionRadius = 150f;
_personalExplodeTime = 2f;
if ( IsProxy )
return;
DetectTargetRange = 900f;
LoseTargetRange = 1200f;
LoseTargetTime = 6f;
MeleeDamage = Utils.Select( Manager.Instance.Difficulty, 12f, 14f, 15f );
DamageTargetDelay = 0.7f;
_personalTurnSpeed = 4f;
Acceleration = 160f;
AccelerationAttacking = 170f;
Deceleration = 1.25f;
DecelerationAttacking = 0.8f;
_shootDelayMin = 2f;
_shootDelayMax = 9f;
_shootRange = 650f;
_shootDelayTimer = Game.Random.Float( _shootDelayMin, _shootDelayMax );
}
protected override void OnUpdate()
{
base.OnUpdate();
//Gizmo.Draw.Color = Color.White;
//Gizmo.Draw.Text( $"{State}\nVelocity: {Velocity}\nGetMoveSpeedFactor(): {GetMoveSpeedFactor()}", new global::Transform( WorldPosition ) );
if ( IsProxy )
return;
if ( IsExploding || Manager.Instance.IsGameOver )
return;
if ( !IsStunned && !IsDying )
HandleState();
}
protected override float GetMoveSpeedFactor()
{
return 1f;
}
protected void HandleState()
{
switch ( State )
{
case MinibossExploderState.Default:
if ( TargetUnit.IsValid() && !IsInTheAir )
{
var targetDistSqr = (TargetUnit.Position2D - Position2D).LengthSquared;
if ( targetDistSqr < MathF.Pow( _shootRange, 2f ) )
{
_shootDelayTimer -= Time.Delta * TimeScale;
if ( _shootDelayTimer < 0f && targetDistSqr < MathF.Pow( _shootRange * 0.95f, 2f ) )
SetState( MinibossExploderState.ShootPrepare );
}
}
break;
case MinibossExploderState.ShootPrepare:
if ( _timeSinceChangeState > 0.7f )
SetState( MinibossExploderState.Shoot );
break;
case MinibossExploderState.Shoot:
if ( _timeSinceChangeState > 0.7f )
SetState( MinibossExploderState.ShootFinish );
break;
}
}
protected void SetState( MinibossExploderState state )
{
State = state;
_timeSinceChangeState = 0f;
switch ( state )
{
case MinibossExploderState.Default:
EnterDefaultStateRpc();
break;
case MinibossExploderState.ShootPrepare:
StartShootingRpc();
_shootDelayTimer = Game.Random.Float( _shootDelayMin, _shootDelayMax ) * Utils.Map( HpPercent, 1f, 0f, 1f, 0.6f, EasingType.SineIn );
break;
case MinibossExploderState.Shoot:
ShootRpc();
var dir = (Vector2)WorldRotation.Forward;
var pos = Position2D + dir * 20f;
var targetPos = TargetUnit.IsValid()
? (Position2D + (TargetUnit.Position2D - Position2D).Normal * MathF.Min( (TargetUnit.Position2D - Position2D).Length, 350f ) + TargetUnit.Velocity * Game.Random.Float( 0f, 2.2f ) + Utils.GetRandomVector() * Game.Random.Float( 30f, 60f ))
: Position2D + Utils.GetRandomVector() * Game.Random.Float( 100f, 300f );
targetPos = Manager.Instance.ClampPosToBounds( targetPos );
SpawnFireball( pos, targetPos );
break;
case MinibossExploderState.ShootFinish:
//Velocity = Vector2.Zero;
SetState( MinibossExploderState.Default );
break;
}
}
[Rpc.Broadcast]
protected void StartShootingRpc()
{
CanAnimate = false;
PlayShootAnim();
// todo: new sfx
Manager.Instance.PlaySfxNearby( "spitter.prepare", Position2D, pitch: Game.Random.Float( 0.9f, 0.95f ), volume: 0.6f, maxDist: 400f );
}
protected virtual void PlayShootAnim()
{
SetAnim( "Attack" );
SetPlaybackRate( 0.3f );
CanAnimate = false;
}
[Rpc.Broadcast]
protected void ShootRpc()
{
// todo: new sfx
Manager.Instance.PlaySfxNearby( "spitter.shoot", Position2D, pitch: Game.Random.Float( 0.9f, 0.95f ), volume: 0.85f, maxDist: 450f );
}
void SpawnFireball( Vector2 pos, Vector2 targetPos )
{
SpawnFireballWarning( targetPos );
var zPos = 80f;
var fireballGo = GameObject.Clone( "prefabs/enemyProjectiles/tossed_fireball.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( new Vector3( pos.x, pos.y, zPos ) ) } );
var fireball = fireballGo.Components.Get<TossedFireball>( true );
fireball.Shooter = this;
fireball.Damage = Utils.Select( Manager.Instance.Difficulty, 9f, 10f, 13f );
fireball.Lifetime = Game.Random.Float( 1.7f, 2.5f );
fireball.StartPos2D = pos;
fireball.TargetPos2D = Manager.Instance.ClampPosToBounds( targetPos );
fireballGo.NetworkSpawn();
}
[Rpc.Broadcast]
public void SpawnFireballWarning( Vector2 targetPos )
{
GameObject.Clone( "prefabs/effects/warning_fireball.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( new Vector3( targetPos.x, targetPos.y, 0f ) ) } );
}
public override void Explode()
{
var repelRadius = _explosionRadius * 1.35f;
var force = 900f;
Manager.Instance.CreateExplosionRpc( Position2D, _explosionRadius, _explosionDamage, repelRadius, force, playerSource: _playerWhoKilledUs, enemySource: this, enemyType: this.EnemyType, new Color(1f, 0f, 0.75f), options: RepelOptions.RepelPlayers | RepelOptions.DamagePlayers | RepelOptions.RepelEnemies | RepelOptions.RepelItems );
for ( int i = 0; i < Game.Random.Int( 2, 4 ); i++ )
{
var dir = Utils.GetRandomVector();
var pos = Position2D + dir * Game.Random.Float( 10f, 20f );
var targetPos = pos + dir * Game.Random.Float( 100f, 500f );
SpawnFireball( pos, targetPos );
}
DieRpc( dir: Vector2.Zero, force: 4f, player: _playerWhoKilledUs, damageType: DamageType.Explosion );
}
public override void Die( Vector2 dir, float force, Player player, DamageType damageType )
{
base.Die( dir, force, player, damageType );
Manager.Instance.SpawnFireGroundRpc( Position2D, player: null, enemySource: null, enemyType: EnemyType.MinibossExploder, damage: 8f, lifetime: Game.Random.Float( 12f, 15f ), spreadChance: 0.5f, canStack: false, scale: 2.5f, colorA: Color.Magenta, colorB: Color.Red, hurtPlayers: true, hurtEnemies: false );
}
public override void OnStun()
{
base.OnStun();
PlayFlinchAnim();
SetState( MinibossExploderState.Default );
}
[Rpc.Broadcast]
public void EnterDefaultStateRpc()
{
CanAnimate = true;
PlayWalkAnim();
}
}