Enemy subclass Spiker. Controls spawning, movement, shooting spiker head projectiles, digging underground behavior, state machine transitions, animations and networked RPCs for multiplayer visuals and sounds.
using System;
using Sandbox;
public class Spiker : Enemy
{
public override EnemyType EnemyType => EnemyType.Spiker;
public override float GetMaxHealth()
{
switch ( Manager.Instance.Difficulty )
{
case 0: default: return 75f;
case 1: return 80f;
case 2: return 80f;
}
}
public override Vector3 SpawnScale => new Vector3( 1.25f );
public override int ExtraDeathBloodSprayAmount => 15;
protected float _shootDelayTimer;
protected float _shootDelayMin;
protected float _shootDelayMax;
protected float _shootReverseDelay;
protected float _shootRange;
protected Vector2 _cracksPos;
protected string _projectileName;
protected Color _crackParticleColor;
protected float _maxExtraHeadDist;
protected float _digDelayTimer;
protected float _digDelayMin;
protected float _digDelayMax;
protected float _digRange;
protected float _digOffsetMin;
protected float _digOffsetMax;
protected TimeSince _timeSinceDigging;
protected float _digTime;
protected Vector2 _digPos;
protected float _waitUndergroundTime;
public override bool CanAttack => base.CanAttack && State == SpikerState.Default;
public override bool CanMove => base.CanMove && State == SpikerState.Default;
public override bool CanTurn => base.CanTurn && State == SpikerState.Default;
public override bool CanBeStunned => base.CanBeStunned && !(State == SpikerState.DigStart || State == SpikerState.DigWaitUnderground );
protected bool _cantBePushed;
protected bool _moveClockwise;
public override float SpawnZPos => -120f;
protected int _numTimesShot;
protected int _numShotsTotal;
protected virtual bool ShouldCircleTarget => !IsAttacking || (Utils.FastSin( TimeSinceSpawn * 2f * _personalSpeedScale ) < 0.2f);
protected enum SpikerState
{
Default,
ShootStart,
ShootSpawnCracks,
Shoot,
ShootReverse,
DigStart,
DigWaitUnderground,
DigFinish,
}
protected SpikerState State { get; private set; } = SpikerState.Default;
protected override void OnStart()
{
base.OnStart();
CoinValueMin = 3;
CoinValueMax = 4;
CoinChance = 1f;
PushStrength = 7500f;
Weight = 1.5f;
_personalSpeedScale = Game.Random.Float( 0.95f, 1.05f );
_personalSpeedFreq = Game.Random.Float( 7f, 9f );
_fullMeleeAttackAnimSpeed = 1.5f;
_projectileName = "spiker_head";
_crackParticleColor = new Color( 0.02f, 0.02f, 0.01f );
if ( IsProxy )
return;
AggroRange = 65f;
DetectTargetRange = 650f;
LoseTargetRange = 950f;
LoseTargetTime = 5f;
MeleeDamage = Utils.Select( Manager.Instance.Difficulty, 11f, 12f, 13f );
DamageTargetDelay = 0.75f;
_personalTurnSpeed = Game.Random.Float( 3f, 6f );
Acceleration = 180f;
AccelerationAttacking = 220f;
Deceleration = 2.3f;
DecelerationAttacking = 2.1f;
_shootDelayMin = 2f;
_shootDelayMax = 7f;
_shootRange = 500f;
_maxExtraHeadDist = 14f;
_shootReverseDelay = 0.4f;
_digDelayMin = 5f;
_digDelayMax = 10f;
_digRange = 700f;
_digTime = 1.5f;
_digOffsetMin = 200f;
_digOffsetMax = 400f;
_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( $"{ModelRenderer.SceneModel.CurrentSequence.Name}\nprogress: {ModelRenderer.SceneModel.CurrentSequence.TimeNormalized.ToString( "N2" )}\nspeed: {GetMoveSpeedFactor().ToString( "N2" )}", new global::Transform( WorldPosition ) );
if ( Manager.Instance.IsGameOver )
return;
if ( IsProxy )
return;
if ( !IsSpawning && !IsStunned )
HandleState();
}
protected void HandleState()
{
switch ( State )
{
case SpikerState.Default:
if ( TargetUnit.IsValid() && !IsInTheAir && _timeSinceChangeState > 0.5f )
{
var targetDistSqr = (TargetUnit.Position2D - Position2D).LengthSquared;
if ( targetDistSqr < MathF.Pow( _shootRange, 2f ) )
{
_shootDelayTimer -= Time.Delta * TimeScale;
if ( _shootDelayTimer < 0f )
{
_numTimesShot = 0;
_numShotsTotal = GetNumShotsTotal();
SetState( SpikerState.ShootStart );
}
}
if ( targetDistSqr < MathF.Pow( _digRange, 2f ) )
{
_digDelayTimer -= Time.Delta * TimeScale;
if ( _digDelayTimer < 0f )
SetState( SpikerState.DigStart );
}
}
break;
case SpikerState.ShootStart:
if ( _timeSinceChangeState > GetShootSpawnCracksDelay() )
SetState( SpikerState.ShootSpawnCracks );
break;
case SpikerState.ShootSpawnCracks:
ClearVelocity();
if ( _timeSinceChangeState > 0.7f )
SetState( SpikerState.Shoot );
break;
case SpikerState.Shoot:
ClearVelocity();
if( _numTimesShot >= _numShotsTotal )
{
if ( _timeSinceChangeState > 1.5f )
SetState( SpikerState.ShootReverse );
}
else
{
if ( _timeSinceChangeState > 0.6f )
SetState( SpikerState.ShootStart);
}
break;
case SpikerState.ShootReverse:
ClearVelocity();
if ( _timeSinceChangeState > _shootReverseDelay )
SetState( SpikerState.Default );
break;
case SpikerState.DigStart:
var digTime = 1.5f;
var zPos = Utils.Map( _timeSinceChangeState, 0f, digTime, 0f, SpawnZPos );
WorldPosition = WorldPosition.WithZ( zPos );
SpawnProgress = Utils.Map( _timeSinceChangeState, 0f, digTime, 1f, 0f );
if ( _timeSinceChangeState > digTime )
SetState( SpikerState.DigWaitUnderground);
break;
case SpikerState.DigWaitUnderground:
if ( _timeSinceChangeState > _waitUndergroundTime )
SetState( SpikerState.DigFinish );
break;
}
}
protected void SetState( SpikerState state )
{
State = state;
_timeSinceChangeState = 0f;
switch ( state )
{
case SpikerState.Default:
EnterDefaultStateRpc();
break;
case SpikerState.ShootStart:
_shootDelayTimer = Game.Random.Float( _shootDelayMin, _shootDelayMax );
StartShootingRpc();
break;
case SpikerState.ShootSpawnCracks:
var targetPlayer = TargetUnit.IsValid() && TargetUnit is Player ? TargetUnit as Player : Manager.Instance.GetClosestPlayer( Position2D );
var targetPos = targetPlayer.IsValid()
? GetShootTargetPos( targetPlayer )
: Position2D + Utils.GetRandomVector() * Game.Random.Float( 100f, 400f );
Manager.Instance.AvoidObstacles( ref targetPos, radius: 22f, andSpikerHeads: true );
_cracksPos = Manager.Instance.ClampPosToBounds( targetPos, buffer: 10f );
SpawnCracksRpc( _cracksPos );
break;
case SpikerState.Shoot:
var headPos = _cracksPos;
var player = TargetUnit.IsValid() && TargetUnit is Player ? TargetUnit as Player : Manager.Instance.GetClosestPlayer( Position2D );
if ( player.IsValid() )
{
// extra distance to try to hit player
headPos += (player.Position2D - headPos).Normal * Math.Min( (player.Position2D - headPos).Length, _maxExtraHeadDist );
}
//var zPos = -200f;
var zPos = 0f;
var headGo = GameObject.Clone( $"prefabs/{_projectileName}.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( new Vector3( headPos.x, headPos.y, zPos ), new Angles( 0f, Game.Random.Float( -60f, -120f ), 0f ) ) } );
var head = headGo.Components.Get<SpikerHead>( true );
head.Shooter = this;
head.ModelRenderer.Tint = ModelRenderer.Tint;
headGo.NetworkSpawn();
Manager.Instance.SpawnSpikerHeadArea( headPos, 30f, 1.3f );
_numTimesShot++;
break;
case SpikerState.ShootReverse:
//EnterDefaultStateRpc();
break;
case SpikerState.DigStart:
_digDelayTimer = Game.Random.Float( _digDelayMin, _digDelayMax );
StartDiggingRpc();
break;
case SpikerState.DigWaitUnderground:
_waitUndergroundTime = Game.Random.Float( 0.5f, 1f );
break;
case SpikerState.DigFinish:
SetState( SpikerState.Default );
Vector2 pos = TargetUnit.IsValid()
? TargetUnit.Position2D + TargetUnit.Velocity * Game.Random.Float( 0f, 1.5f ) + Utils.GetRandomVector() * Game.Random.Float( _digOffsetMin, _digOffsetMax )
: Position2D + Utils.GetRandomVector() * Game.Random.Float( _digOffsetMin, _digOffsetMax );
FinishDiggingRpc( Manager.Instance.ClampPosToBounds( pos, buffer: 10f ) );
WorldPosition = new Vector3( pos.x, pos.y, SpawnZPos );
Transform.ClearInterpolation();
_moveClockwise = !_moveClockwise;
break;
}
}
protected virtual float GetShootSpawnCracksDelay()
{
return 0.5f;
}
protected virtual int GetNumShotsTotal()
{
return 1;
}
void ClearVelocity()
{
Velocity = Vector2.Zero;
ResetRepelVelocity();
ExplosionVelocity = Vector2.Zero;
}
protected override float GetMoveSpeedFactor()
{
var leftFootStart = 0.2f;
var leftFootEnd = 0.60f;
var rightFootStart = 0.60f;
var rightFootEnd = 1.25f;
var progress = ModelRenderer.SceneModel.CurrentSequence.TimeNormalized;
if ( progress > rightFootStart || progress < rightFootEnd)
{
var totalProgress = 1f + rightFootEnd;
var offsetProgress = progress < rightFootEnd
? 1f + progress
: progress;
return Utils.Map( offsetProgress, rightFootStart, totalProgress, 0f, 1f, EasingType.Linear );
}
else if ( progress > leftFootStart && progress < leftFootEnd )
{
return Utils.Map( progress, leftFootStart, leftFootEnd, 0f, 1f, EasingType.Linear );
}
return 0f;
}
protected override void HandleRotation()
{
if( !HasTarget || !ShouldCircleTarget )
{
base.HandleRotation();
return;
}
Vector2 toTarget = TargetPos - Position2D;
if ( (TargetPos - Position2D).LengthSquared < MathF.Pow( 600f, 2f ) )
toTarget = Vector2.Lerp( toTarget, new Vector2( toTarget.y, -toTarget.x ) * (_moveClockwise ? -1f : 1f), Utils.Map( (TargetPos - Position2D).LengthSquared, MathF.Pow( 600f, 2f ), MathF.Pow( 200f, 2f ), 0f, 1f ) );
WorldRotation = Rotation.Lerp( WorldRotation, Rotation.LookAt( (Vector3)toTarget), _personalTurnSpeed * Time.Delta * TimeScale );
}
public override void StartAttacking()
{
base.StartAttacking();
if ( _timeSinceStartAttackingSfx > Game.Random.Float( 0.8f, 1.3f ) )
{
Manager.Instance.PlaySfxNearby( "monster.scream1", Position2D, pitch: Game.Random.Float( 1f, 1.2f ), volume: 1.3f, maxDist: 300f );
_timeSinceStartAttackingSfx = 0f;
}
}
[Rpc.Broadcast]
protected void StartShootingRpc()
{
StartShooting();
}
protected virtual void StartShooting()
{
CanAnimate = false;
SetAnim( "Shoot" );
SetPlaybackRate( 0.7f );
}
protected virtual Vector2 GetShootTargetPos( Player targetPlayer )
{
return targetPlayer.IsValid()
? targetPlayer.Position2D + targetPlayer.Velocity * Game.Random.Float( 0.2f, 2f ) + new Vector2( Game.Random.Float( -1f, 1f ), Game.Random.Float( -1f, 1f ) ) * 40f
: Position2D + Utils.GetRandomVector() * Game.Random.Float( 100f, 400f );
}
[Rpc.Broadcast]
protected void SpawnCracksRpc( Vector2 pos )
{
Manager.Instance.PlaySfxNearby( "spike.prepare", pos, pitch: Game.Random.Float( 1f, 1.1f ), volume: 0.85f, maxDist: 250f );
Manager.Instance.SpawnSpikerHeadCracksRpc( pos, _crackParticleColor );
Manager.Instance.SpawnSpikerHeadArea( pos, 30f, GetShootSpawnCracksDelay() + 0.15f );
}
//[Rpc.Broadcast]
//protected void ReverseShootingRpc()
//{
// SetAnim( "RPG_2H_Pose_Standing_Idle_01" );
//}
[Rpc.Broadcast]
public void StartDiggingRpc()
{
Manager.Instance.PlaySfxNearby( "zombie.dirt", Position2D, pitch: Game.Random.Float( 0.6f, 0.8f ), volume: 0.7f, maxDist: 320f );
GameObject.Clone( "prefabs/effects/enemy_spawn_clouds.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( WorldPosition.WithZ( 15f ) ) } );
SetAnim( "Dig" );
SetPlaybackRate( 1f );
CanAnimate = false;
}
[Rpc.Broadcast]
protected void FinishDiggingRpc( Vector2 pos )
{
Manager.Instance.PlaySfxNearby( "zombie.dirt", pos, pitch: Game.Random.Float( 0.6f, 0.8f ), volume: 0.7f, maxDist: 320f );
GameObject.Clone( "prefabs/effects/enemy_spawn_clouds.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( new Vector3( pos.x, pos.y, 15f ) ) } );
IsSpawning = true;
TimeSinceSpawn = 0f;
SpawnProgress = 0f;
ShouldSpawnInstantly = false;
PlaySpawnAnim();
CanAnimate = true;
SetPlaybackRate( 1f );
}
public override void OnStun()
{
base.OnStun();
PlayFlinchAnim();
SetState( SpikerState.Default );
}
protected override void Jump( Vector2 targetPos, float height, float lifetime )
{
SetState( SpikerState.Default );
base.Jump( targetPos, height, lifetime );
}
[Rpc.Broadcast]
public void EnterDefaultStateRpc()
{
CanAnimate = true;
PlayWalkAnim();
}
protected override void PlayFlinchAnim()
{
}
}