Enemy subclass for a spitter miniboss. It configures stats, behaviors, projectiles, spawning visuals, shooting patterns, blinking teleport, and death effects specific to the miniboss variant of Spitter.
using Sandbox;
using System;
using System.Collections.Generic;
public class MinibossSpitter : Spitter
{
public override EnemyType EnemyType => EnemyType.MinibossSpitter;
public override string GibFolder => "miniboss_spitter";
public override float OverrideGibChance => 1f;
public override int ExtraDeathBloodSprayAmount => 25;
protected override float MinibossHealthScale => 1.1f;
public override float GetMaxHealth() => MinibossBaseHealth * MinibossHealthScale;
public override Vector3 SpawnScale => new Vector3( 1.3f );
public override bool ShowHealthbar => true;
public override float HealthbarOffset => 90f;
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 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 = 900f;
LoseTargetRange = 1400f;
LoseTargetTime = 6f;
MeleeDamage = Utils.Select( Manager.Instance.Difficulty, 9f, 12f, 13f );
DamageTargetDelay = 0.6f;
_personalTurnSpeed = Game.Random.Float( 5f, 7f );
Acceleration = 250f * Utils.Select( Manager.Instance.Difficulty, 0.9f, 1f, 1.05f );
AccelerationAttacking = 285f * Utils.Select( Manager.Instance.Difficulty, 0.9f, 1f, 1.05f );
Deceleration = 2f;
DecelerationAttacking = 1.8f;
_shootDelayMin = 3f;
_shootDelayMax = 7f;
_shootRange = 500f;
_shootDelayTimer = Game.Random.Float( _shootDelayMin, _shootDelayMax );
_prepareShootTime = 0.9f;
_shootTime = 0.6f;
_blinkDelayMin = 4f;
_blinkDelayMax = 14f;
_blinkPrepareDelay = 0.45f;
_blinkRange = 1200f;
_blinkDelayTimer = Game.Random.Float( _blinkDelayMin, _blinkDelayMax );
_personalRetreatRange = Game.Random.Float( 185f, 250f );
_personalStopRetreatRange = Game.Random.Float( 300f, 370f );
}
protected override void OnUpdate()
{
base.OnUpdate();
}
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 StartShooting()
{
base.StartShooting();
//SetPlaybackRate( 1.43f );
SetPlaybackRate( 1f );
}
protected override void Shoot()
{
Manager.Instance.PlaySfxNearby( "spitter.shoot", Position2D, pitch: Game.Random.Float( 1.4f, 1.5f ), volume: 0.85f, maxDist: 450f );
if ( IsProxy )
return;
var dir = (Vector2)WorldRotation.Forward;
var numBullets = MathX.FloorToInt( Utils.Map( HpPercent, 1f, 0f, 1f, Utils.Select( Manager.Instance.Difficulty, 2f, 3f, 4f ), EasingType.SineIn ) ) + Game.Random.Int( 0, Utils.Select( Manager.Instance.Difficulty, 1, 2, 3 ) );
var spread = Game.Random.Float( 30f, Utils.Map( numBullets, 1, 7, 70f, 140f) );
float currAngleOffset = -spread * 0.5f;
float increment = numBullets > 1 ? (spread / (float)(numBullets - 1)) : 0f;
var minutes = Manager.Instance.ElapsedTime / 60f;
var projectileType = Manager.Instance.GetRandomEnemyProjectileType( minutes * Utils.Select( Manager.Instance.Difficulty, 0.8f, 1f, 1.25f ) );
// todo: less numProjectiles when Cursed type
for ( int i = 0; i < numBullets; i++ )
{
var currDir = Utils.RotateVector( dir, currAngleOffset + increment * i );
bool isHoming = Game.Random.Float( 0f, 1f ) < 0.35f;
if ( isHoming )
{
var pos = Position2D + dir * 40f + currDir * 10f + Utils.GetRandomVector() * Game.Random.Float( 0f, 4f );
Manager.Instance.SpawnEnemyHomingProjectile( pos, currDir, shooter: this, enemyType: this.EnemyType, startVel: Game.Random.Float( 50f, 70f ), projectileType, lifetimeModifier: Game.Random.Float(1f, 1.4f) );
}
else
{
var pos = Position2D + dir * 40f + currDir * 10f;
Manager.Instance.SpawnEnemyProjectile( pos, currDir, shooter: this, enemyType: this.EnemyType, startVel: 150f, projectileType, lifetimeModifier: Game.Random.Float( 1f, 1.3f ) );
}
}
}
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 );
var damage = Utils.Select( Manager.Instance.Difficulty, 3f, 5f, 7f );
Manager.Instance.SpawnAcidPuddle( Position2D, lifetime: Game.Random.Float( 24f, 30f ), damage, scale: Game.Random.Float( 1.35f, 1.8f ), new Color( 0.5f, 0.2f, 0.3f ), new Color( 0.5f, 0.3f, 0.4f ), playerSource: null, enemySource: this, enemyType: this.EnemyType );
}
protected override Vector2 GetBlinkTargetPos()
{
Vector2 blinkPos = TargetUnit.IsValid()
? TargetUnit.Position2D + TargetUnit.Velocity * Game.Random.Float( 0f, 1.5f ) + Utils.GetRandomVector() * Game.Random.Float( 200f, 440f )
: Position2D + Utils.GetRandomVector() * Game.Random.Float( 200f, 500f );
return Manager.Instance.ClampPosToBounds( blinkPos );
}
protected override void Jump( Vector2 targetPos, float height, float lifetime )
{
SetState( SpitterState.Default );
base.Jump( targetPos, height, lifetime );
}
}