Enemy subclass MinibossSpiker2 derived from Spiker. It configures miniboss-specific stats, visuals, behavior timers and AI parameters, overrides spawn/health/healthbar properties, and customizes targeting and shooting position selection to avoid lava and predict player movement.
using System;
using Sandbox;
public class MinibossSpiker2 : Spiker
{
public override EnemyType EnemyType => EnemyType.MinibossSpiker2;
public override string GibFolder => "miniboss_spiker_2";
public override float OverrideGibChance => 1f;
public override int ExtraDeathBloodSprayAmount => 25;
protected override float MinibossHealthScale => 1.2f;
public override float GetMaxHealth() => MinibossBaseHealth * MinibossHealthScale;
public override Vector3 SpawnScale => new Vector3( 1.8f );
public override float SpawnZPos => -180f;
public override bool ShowHealthbar => true;
public override float HealthbarOffset => 130f;
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 float ParticleYPosOverride => 0.4f;
public override float StunParticleYPosOverride => 0.7f;
protected override void OnStart()
{
base.OnStart();
CoinValueMin = 10;
CoinValueMax = 18;
CoinChance = 1f;
PushStrength = 6000f;
Weight = 1.4f;
_personalSpeedScale = Game.Random.Float( 0.9f, 1.1f );
_personalSpeedFreq = Game.Random.Float( 9f, 11f );
_projectileName = "spiker_head_miniboss_2";
_crackParticleColor = new Color( 0.02f, 0.02f, 0.08f );
if ( IsProxy )
return;
AggroRange = 60f;
DetectTargetRange = 950f;
LoseTargetRange = 1300f;
LoseTargetTime = 5.5f;
MeleeDamage = Utils.Select( Manager.Instance.Difficulty, 11f, 14f, 15f );
DamageTargetDelay = 0.75f;
_personalTurnSpeed = Game.Random.Float( 3f, 6f );
Acceleration = 140f * Utils.Select( Manager.Instance.Difficulty, 0.9f, 1f, 1.05f );
AccelerationAttacking = 160f * Utils.Select( Manager.Instance.Difficulty, 0.9f, 1f, 1.05f );
Deceleration = 1.7f;
DecelerationAttacking = 1.5f;
_shootDelayMin = 4f;
_shootDelayMax = 8f;
_shootRange = 750f;
_maxExtraHeadDist = 27f;
_shootReverseDelay = 1f;
_digDelayMin = 3f;
_digDelayMax = 12f;
_digRange = 1200f;
_digTime = 1.5f;
_digOffsetMin = 200f;
_digOffsetMax = 800f;
_shootDelayTimer = Game.Random.Float( _shootDelayMin, _shootDelayMax );
_digDelayTimer = Game.Random.Float( _digDelayMin, _digDelayMax );
_moveClockwise = Game.Random.Int( 0, 1 ) == 0;
}
protected override void OnUpdate()
{
base.OnUpdate();
//Gizmo.Draw.Color = Color.White;
//Gizmo.Draw.Text( $"_numTimesShot: {_numTimesShot} _numShotsTotal: {_numShotsTotal}\n_timeSinceShoot: {_timeSinceShoot}", new global::Transform( WorldPosition ) );
}
protected override Vector2 GetShootTargetPos( Player closestPlayer )
{
var targetPos = GetRandomTargetPos( closestPlayer );
int NUM_TRIES = 40;
int count = 0;
while ( Manager.Instance.IsInLava( targetPos ) && count < NUM_TRIES )
{
targetPos = GetRandomTargetPos( closestPlayer, count );
count++;
}
return targetPos;
}
Vector2 GetRandomTargetPos( Player closestPlayer, int currentTries = 0 )
{
var targetPos = closestPlayer.IsValid()
? closestPlayer.Position2D + closestPlayer.Velocity * Game.Random.Float( 0f, 1.75f ) + Utils.GetRandomVector() * 120f * Utils.Map( currentTries, 0, 40, 1f, 4f )
: Position2D + Utils.GetRandomVector() * Game.Random.Float( 200f, 340f ) * Utils.Map( currentTries, 0, 40, 1f, 4f );
return Manager.Instance.ClampPosToBounds( targetPos, buffer: 10f );
}
}