Enemy subclass for a miniboss type called MinibossZoner. Implements movement, jump/evade state machine, zone radius that shrinks with health, animations and effects for jump, and overrides damage to ignore hits from outside its zone.
using System;
using Sandbox;
public class MinibossZoner : Enemy
{
public override EnemyType EnemyType => EnemyType.MinibossZoner;
public override string GibFolder => "miniboss_zoner";
public override float OverrideGibChance => 1f;
public override int ExtraDeathBloodSprayAmount => 25;
public override float GetMaxHealth() => MinibossBaseHealth;
public override Vector3 SpawnScale => new Vector3( 1.75f );
public override bool ShowHealthbar => true;
public override float HealthbarOffset => 100f;
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;
[Property] public Decal Decal { get; set; }
public const float ZONE_SCALE_FACTOR = 0.13f;
public float ZoneRadius { get; set; }
public float ZoneRadiusMax { get; set; }
public float ZoneRadiusMin { get; set; }
public override float ParticleYPosOverride => 0.6f;
public override float StunParticleYPosOverride => 1.1f;
// State machine
private enum ZonerState { Default, JumpPrepare, Jump }
private ZonerState State { get; set; } = ZonerState.Default;
// Jump
private float _prepareJumpTime;
private TimeSince _timeSinceJumping;
private float _delayUntilNextJump;
private Vector2 _jumpTargetPos;
private float _nextJumpDelayMin;
private float _nextJumpDelayMax;
private float _maxJumpDist;
// Evade
private TimeSince _timeSinceEvade;
private float _evadeDelay;
private float _evadeDelayMin;
private float _evadeDelayMax;
private float _evadeVelocityMin;
private float _evadeVelocityMax;
public override bool CanAttack => base.CanAttack && State == ZonerState.Default;
public override bool CanMove => base.CanMove && State == ZonerState.Default;
public override bool CanTurn => base.CanTurn && State == ZonerState.Default;
protected override void OnStart()
{
base.OnStart();
CoinValueMin = 11;
CoinValueMax = 21;
CoinChance = 1f;
PushStrength = 9000f;
Weight = 1.5f;
_personalSpeedScale = 1.75f;
_personalSpeedFreq = 12f;
AnimSpeedModifier = 0.7f;
ZoneRadiusMax = Utils.Select( Manager.Instance.Difficulty, 175f, 150f, 140f );
ZoneRadiusMin = Utils.Select( Manager.Instance.Difficulty, 130f, 90f, 80f );
ZoneRadius = ZoneRadiusMax;
Decal.Size = new Vector2( ZoneRadius * ZONE_SCALE_FACTOR );
if ( IsProxy )
return;
AggroRange = 85f;
DetectTargetRange = 850f;
LoseTargetRange = 1300f;
LoseTargetTime = 7f;
MeleeDamage = Utils.Select( Manager.Instance.Difficulty, 8f, 10f, 12f );
DamageTargetDelay = 0.8f;
Acceleration = 100f;
AccelerationAttacking = 120f;
Deceleration = 0.45f;
DecelerationAttacking = 0.75f;
_personalTurnSpeed = 4.5f;
_evadeDelayMin = 1.5f;
_evadeDelayMax = 6.5f;
_evadeDelay = Game.Random.Float( _evadeDelayMin, _evadeDelayMax );
_evadeVelocityMin = 120f;
_evadeVelocityMax = 350f;
_nextJumpDelayMin = 6.0f;
_nextJumpDelayMax = 14.0f;
_delayUntilNextJump = Game.Random.Float( _nextJumpDelayMin, _nextJumpDelayMax );
_maxJumpDist = 650f;
}
protected override void OnUpdate()
{
base.OnUpdate();
if ( IsProxy )
return;
if ( !IsStunned && !IsDying )
HandleState();
}
private void HandleState()
{
switch ( State )
{
case ZonerState.Default:
// Evade: perpendicular dodge when player is close and facing us
if ( _timeSinceEvade > _evadeDelay * (1f / TimeScale) &&
TargetUnit != null &&
(TargetUnit.Position2D - Position2D).LengthSquared < MathF.Pow( 150f, 2f ) )
{
Vector2 forwardDir = (Vector2)WorldRotation.Forward;
var dot = Vector2.Dot( forwardDir, (Vector2)TargetUnit.WorldRotation.Forward );
if ( dot < -0.92f )
{
Vector2 toTarget = (TargetUnit.Position2D - Position2D).Normal;
Vector2 evadeDir = new Vector2( toTarget.y, -toTarget.x ) *
(Game.Random.Float( 0f, 1f ) < 0.5f ? 1f : -1f);
Velocity += evadeDir * Game.Random.Float( _evadeVelocityMin, _evadeVelocityMax );
_evadeDelay = Game.Random.Float( _evadeDelayMin, _evadeDelayMax ) * TimeScale;
_timeSinceEvade = 0f;
}
}
// Jump trigger
if ( HasTarget && !IsInTheAir &&
_timeSinceJumping > _delayUntilNextJump * (IsAttacking ? 2f : 1f) )
SetZonerState( ZonerState.JumpPrepare );
break;
case ZonerState.JumpPrepare:
var dir = (_jumpTargetPos - Position2D).Normal;
WorldRotation = Rotation.Lerp( WorldRotation, Rotation.LookAt( dir ),
10f * Time.Delta * TimeScale );
if ( _timeSinceChangeState > _prepareJumpTime )
SetZonerState( ZonerState.Jump );
break;
}
}
private void SetZonerState( ZonerState state )
{
State = state;
_timeSinceChangeState = 0f;
switch ( state )
{
case ZonerState.Default:
EnterDefaultStateRpc();
break;
case ZonerState.JumpPrepare:
PrepareJumpRpc();
_timeSinceJumping = 0f;
_delayUntilNextJump = Game.Random.Float( _nextJumpDelayMin, _nextJumpDelayMax );
_delayUntilNextJump *= Utils.Map(
HpPercent, 1f, 0f,
Utils.Select( Manager.Instance.Difficulty, 1.4f, 1.2f, 1f ),
Utils.Select( Manager.Instance.Difficulty, 1f, 0.9f, 0.85f ) );
_prepareJumpTime = Game.Random.Float( 0.4f, 0.75f );
IsAttacking = false;
_jumpTargetPos = TargetUnit.IsValid()
? TargetUnit.Position2D + TargetUnit.Velocity * Game.Random.Float( 0f, 4f ) +
Utils.GetRandomVector() * Game.Random.Float( 0f, 150f )
: Position2D + Utils.GetRandomVector() * Game.Random.Float( 50f, 150f );
if ( (_jumpTargetPos - Position2D).LengthSquared > MathF.Pow( _maxJumpDist, 2f ) )
_jumpTargetPos = Position2D + (_jumpTargetPos - Position2D).Normal * _maxJumpDist;
_jumpTargetPos = Manager.Instance.ClampPosToBounds( _jumpTargetPos );
break;
case ZonerState.Jump:
SetZonerState( ZonerState.Default );
var height = Game.Random.Float( 70f, 130f );
var time = Utils.Map( (_jumpTargetPos - Position2D).Length, 0f, _maxJumpDist,
0.75f, 1.3f, EasingType.SineIn ) *
Game.Random.Float( 0.85f, 1.1f );
JumpRpc( _jumpTargetPos, height, time );
break;
}
}
[Rpc.Broadcast]
public void PrepareJumpRpc()
{
SetAnim( "JumpPrepare" );
CanAnimate = false;
}
[Rpc.Broadcast]
public void EnterDefaultStateRpc()
{
CanAnimate = true;
PlayWalkAnim();
}
protected override void Jump( Vector2 targetPos, float height, float lifetime )
{
base.Jump( targetPos, height, lifetime );
GameObject.Clone( "prefabs/effects/cloud.prefab",
new CloneConfig { StartEnabled = true,
Transform = new Transform( WorldPosition.WithZ( 10f ) ) } );
Manager.Instance.PlaySfxNearby( "jump_whoosh", Position2D,
pitch: Game.Random.Float( 1.3f, 1.45f ), volume: 0.8f, maxDist: 380f );
}
public override void JumpFinish()
{
base.JumpFinish();
Manager.Instance.PlaySfxNearby( "jump_thud", Position2D,
pitch: Game.Random.Float( 0.82f, 0.92f ), volume: 0.7f, maxDist: 280f );
if ( IsProxy )
return;
var dir = (Position2D - JumpStartPos).Normal;
Velocity += dir * Game.Random.Float( 50f, 150f ) * TimeScale;
_timeSinceDamageTarget = 999f;
}
public override void OnStun()
{
base.OnStun();
SetZonerState( ZonerState.Default );
}
protected override Vector2 GetTargetOffset()
{
return TargetUnit.Velocity * (1f + Utils.FastSin( TimeSinceSpawn * 0.4f ) * 0.6f);
}
protected override float GetMoveSpeedFactor()
{
return 1f;
}
protected override void Damage( float damage, Player player, DamageType damageType, Vector3 hitPos, Vector2 force, bool isCrit = false, bool shouldFlinch = true, DamageResultFlags damageFlags = DamageResultFlags.None )
{
if ( player.IsValid() )
{
var distSqr = (player.Position2D - Position2D).LengthSquared;
//Log.Info( $"Distance to player: {(player.Position2D - Position2D).Length}, ZoneRadius: {ZoneRadius}" );
if ( distSqr > MathF.Pow( ZoneRadius, 2f ) )
{
damage = 0f;
isCrit = false;
force = Vector2.Zero;
shouldFlinch = false;
// todo: flash zone decal
}
base.Damage( damage, player, damageType, hitPos, force, isCrit, shouldFlinch, damageFlags );
}
else
{
base.Damage( damage, player, damageType, hitPos, force, isCrit, shouldFlinch, damageFlags );
}
// todo: don't get ignited, frozen, poisoned, etc by bullets shot from outside the zone
}
protected override void OnAdjustHealth( float amount )
{
base.OnAdjustHealth( amount );
ZoneRadius = Utils.Map( HpPercent, 1f, 0f, ZoneRadiusMax, ZoneRadiusMin );
Decal.Size = new Vector2( ZoneRadius * ZONE_SCALE_FACTOR );
Acceleration = Utils.Map( HpPercent, 1f, 0f, 120f, 140f );
AccelerationAttacking = Acceleration * 1.3f;
}
}