Enemy AI class MinibossSniper, derived from Spitter. It configures miniboss stats, movement, shooting and blink teleport behavior, handles damage and death effects, and spawns projectile attacks.
using System;
using Sandbox;
public class MinibossSniper : Spitter
{
public override EnemyType EnemyType => EnemyType.MinibossSniper;
public override string GibFolder => "miniboss_sniper";
public override float OverrideGibChance => 1f;
public override int ExtraDeathBloodSprayAmount => 25;
protected override float MinibossHealthScale => 1.15f;
public override float GetMaxHealth() => MinibossBaseHealth * MinibossHealthScale;
public override Vector3 SpawnScale => new Vector3( 1.45f );
public override bool ShowHealthbar => true;
public override float HealthbarOffset => 95f;
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;
protected int _numTimesShot;
protected int _numShotsTotal;
//private float _currShootDelay;
protected override void OnStart()
{
base.OnStart();
CoinValueMin = 9;
CoinValueMax = 18;
CoinChance = 1f;
PushStrength = 6000f;
Weight = 1.4f;
_personalSpeedScale = 1f;
_personalSpeedFreq = Game.Random.Float( 9f, 11f );
if ( IsProxy )
return;
DetectTargetRange = 3000f;
LoseTargetRange = 1400f;
LoseTargetTime = 6f;
MeleeDamage = Utils.Select( Manager.Instance.Difficulty, 11f, 12f, 13f );
DamageTargetDelay = 0.6f;
_personalTurnSpeed = Game.Random.Float( 12f, 12f );
Acceleration = 210f * Utils.Select( Manager.Instance.Difficulty, 0.9f, 1f, 1.05f );
AccelerationAttacking = 225f * Utils.Select( Manager.Instance.Difficulty, 0.9f, 1f, 1.05f );
Deceleration = 2f;
DecelerationAttacking = 1.8f;
_shootDelayMin = Utils.Select( Manager.Instance.Difficulty, 6.5f, 4f, 3.5f );
_shootDelayMax = Utils.Select( Manager.Instance.Difficulty, 14f, 12.5f, 11f );
_shootRange = 3000f;
_shootDelayTimer = 1f;
_prepareShootTime = 0.7f;
_shootTime = 0.3f;
_blinkDelayMin = 5f;
_blinkDelayMax = 13f;
_blinkPrepareDelay = 0.45f;
_blinkRange = 950f;
_blinkDelayTimer = Game.Random.Float( _blinkDelayMin, _blinkDelayMax );
_personalRetreatRange = Game.Random.Float( 640f, 660f );
_personalStopRetreatRange = Game.Random.Float( 870f, 890f );
}
protected override Vector2 GetTargetOffset()
{
var dist = (TargetUnit.Position2D - Position2D).Length;
var offset = TargetUnit.Velocity * (0.5f + Utils.FastSin( TimeSinceSpawn * 2.6f ) * 0.5f) * dist * 0.012f;
// don't lead shots if target is very close and heading toward us
if ( dist < 110f )
{
var dot = Vector2.Dot( (Position2D - TargetUnit.Position2D).Normal, TargetUnit.Velocity.Normal );
if ( dot > 0.65f )
offset = Vector2.Zero;
}
if ( State == SpitterState.Default && !_isRetreating )
offset += new Vector2( Utils.FastSin( TimeSinceSpawn * 0.73f ), Utils.FastSin( TimeSinceSpawn * 0.64f ) ) * 85f;
return offset;
}
//protected override void PlayShootAnim()
//{
// SetAnim( Game.Random.Float( 0f, 1f ) < 0.5f ? "HoldItem_RH_Throw_Strong" : "HoldItem_LH_Throw_Strong" );
// SetPlaybackRate( 0.5f );
//}
protected override void BeginShooting()
{
base.BeginShooting();
_numTimesShot = 0;
_numShotsTotal = 3;
}
protected override void SetStateShoot()
{
ShootRpc();
_numTimesShot++;
//_currShootDelay = Game.Random.Float( 0.1f, 0.5f );
}
protected override void HandleStateShoot()
{
if ( _numTimesShot >= _numShotsTotal )
{
if ( _timeSinceChangeState > 0.6f )
SetState( SpitterState.ShootFinish );
}
else
{
if ( _timeSinceChangeState > 0.4f )
SetState( SpitterState.ShootPrepare );
}
}
protected override void StartShooting()
{
base.StartShooting();
SetPlaybackRate( 1.35f );
}
protected override void Shoot()
{
Manager.Instance.PlaySfxNearby( "spitter.shoot", Position2D, pitch: Game.Random.Float( 1.4f, 1.5f ), volume: 0.65f, maxDist: 850f );
if ( IsProxy )
return;
var dir = (Vector2)WorldRotation.Forward;
var minutes = Manager.Instance.ElapsedTime / 60f;
var projectileType = Manager.Instance.GetRandomEnemyProjectileType( minutes );
var currDir = Utils.RotateVector( dir, Game.Random.Float( -7f, 7f ) );
var pos = Position2D + currDir * 45f;
var speed = 290f * Game.Random.Float( 0.95f, 1.05f ) * Utils.Map( HpPercent, 1f, 0f, 1f, 1.5f, EasingType.SineIn );
Manager.Instance.SpawnEnemyProjectile( pos, currDir, shooter: this, enemyType: this.EnemyType, startVel: 300f, projectileType, lifetimeModifier: 3f );
}
protected override Vector2 GetBlinkTargetPos()
{
// todo: blink to a corner far away from target player
Vector2 blinkPos = GetRandomBlinkPos();
int numTries = 0;
while ( numTries < 10 )
{
if ( Manager.Instance.IsInBounds( blinkPos ) )
break;
blinkPos = GetRandomBlinkPos();
numTries++;
}
return Manager.Instance.ClampPosToBounds( blinkPos );
}
Vector2 GetRandomBlinkPos()
{
var targetPos = TargetUnit.Position2D + Utils.GetRandomVector() * Game.Random.Float( 500f, 1000f );
int numTries = 0;
while ( numTries < 20 )
{
if ( (TargetUnit.Position2D - targetPos).LengthSquared > MathF.Pow( Utils.Map( numTries, 0, 20, 800f, 400f ), 2f ) )
break;
targetPos = TargetUnit.Position2D + Utils.GetRandomVector() * Game.Random.Float( 500f, 1000f );
numTries++;
}
return targetPos;
}
protected override void Damage( float damage, Player player, DamageType damageType, Vector3 hitPos, Vector2 force, bool isCrit = false, bool shouldFlinch = true, DamageResultFlags damageFlags = DamageResultFlags.None )
{
base.Damage( damage, player, damageType, hitPos, force, isCrit, shouldFlinch, damageFlags );
if ( IsProxy || IsDying )
return;
if ( State == SpitterState.Default && _timeSinceBlinking > Game.Random.Float( 4f, 8f ) && Game.Random.Float( 0f, 1f ) < Utils.Map( HpPercent, 1f, 0f, 0f, 1f ) )
SetState( SpitterState.BlinkPrepare );
}
public override void Die( Vector2 dir, float force, Player player, DamageType damageType )
{
base.Die( dir, force, player, damageType );
Manager.Instance.PlaySfxNearbyRpc( "puddle_splat", Position2D, pitch: Game.Random.Float( 1.05f, 1.1f ), volume: 1.2f, maxDist: 350f );
Manager.Instance.SpawnAcidPuddle( Position2D, lifetime: Game.Random.Float( 24f, 30f ), damage: 5f, scale: Game.Random.Float( 1.35f, 1.8f ), new Color( 0.5f, 0.3f, 0.6f ), new Color( 0.5f, 0.5f, 0.7f ), playerSource: null, enemySource: this, enemyType: this.EnemyType );
}
}