things/enemies/SpikerElite.cs
using Sandbox;
using System;
public class SpikerElite : Enemy
{
private TimeSince _damageTime;
private const float DAMAGE_TIME = 0.75f;
private float _shootDelayTimer;
private const float SHOOT_DELAY_MIN = 2f;
private const float SHOOT_DELAY_MAX = 4.5f;
public bool IsShooting { get; private set; }
private float _shotTimer;
private const float SHOOT_TIME = 4f;
private bool _hasShot;
private TimeSince _prepareStartTime;
private bool _hasReversed;
private bool _moveClockwise;
public static int SpikerEliteNum { get; set; }
private float _perpendicularMaxDist;
private float _digDelayTimer;
private const float DIG_DELAY_MIN = 4f;
private const float DIG_DELAY_MAX = 13f;
public bool IsDigging { get; private set; }
private TimeSince _timeSinceStartDigging;
private const float DIG_TIME = 1.2f;
protected override void OnAwake()
{
//OffsetY = -0.82f;
ShadowScale = 1.65f;
ShadowFullOpacity = 0.8f;
ShadowOpacity = 0f;
Scale = 1.9f;
base.OnAwake();
//AnimSpeed = 3f;
//Sprite.Texture = Texture.Load("textures/sprites/spiker_elite.vtex");
//Sprite.Size = new Vector2( 1f, 1f ) * Scale;
PushStrength = 12f;
Deceleration = 2.57f;
DecelerationAttacking = 2.35f;
AggroRange = 0.45f;
Radius = 0.28f;
Health = 220f;
if ( Manager.Instance.Difficulty < 0 )
Health = 180f;
MaxHealth = Health;
DamageToPlayer = 16f;
CoinValueMin = 7;
CoinValueMax = 15;
CoinChance = 0.85f;
if ( IsProxy )
return;
CollideWith.Add( typeof( Enemy ) );
CollideWith.Add( typeof( Player ) );
_damageTime = DAMAGE_TIME;
_shootDelayTimer = Game.Random.Float( SHOOT_DELAY_MIN, SHOOT_DELAY_MAX );
_digDelayTimer = Game.Random.Float( DIG_DELAY_MIN, DIG_DELAY_MAX );
_moveClockwise = SpikerEliteNum % 2 == 0;
SpikerEliteNum++;
_perpendicularMaxDist = Game.Random.Float( 4.5f, 7.5f );
}
protected override void UpdatePosition( float dt )
{
base.UpdatePosition( dt );
var targetPos = Target.IsValid() ? Target.Position2D : (IsCharmed ? Manager.Instance.Player.Position2D : Position2D);
if ( IsShooting )
{
Velocity *= (1f - dt * (IsAttacking ? DecelerationAttacking : Deceleration));
if ( !_hasShot && _prepareStartTime > 1f )
{
CreateSpike();
_hasShot = true;
}
if ( !_hasReversed && _prepareStartTime > 3f )
{
_hasReversed = true;
Sprite.PlayAnimation( "shoot_reverse" );
}
Velocity *= (1f - dt * 4f);
_shotTimer -= dt;
if ( _shotTimer < 0f )
{
FinishShooting();
return;
}
}
else if ( IsDigging )
{
Velocity *= (1f - dt * 6f);
if ( _timeSinceStartDigging > DIG_TIME )
{
Vector2 pos;
if ( Target.IsValid() )
{
pos = targetPos + Target.Velocity * Game.Random.Float( 0f, 2f ) + Utils.GetRandomVector() * Game.Random.Float( 1.5f, 5.5f );
if ( (Position2D - pos).LengthSquared < MathF.Pow( 0.5f, 2f ) )
pos = Position2D + Utils.GetRandomVector() * Game.Random.Float( 3f, 5f );
}
else
{
pos = Position2D + Game.Random.Float( 4f, 5f );
}
FinishDigging( Manager.Instance.ClampToBounds( pos ) );
}
else
{
float progress = Utils.Map( _timeSinceStartDigging, 0f, DIG_TIME, 0f, 1f );
//ZPos = Utils.Map( progress, 0f, 1f, 0f, DIG_DEPTH );
//PlaybackRate = Utils.Map( progress, 0f, 1f, 0f, 1f ) * _personalSpeedScale;
float shadowOpacity = Utils.Map( progress, 0f, 1f, ShadowOpacity, 0f, EasingType.QuadIn );
ShadowSprite.Tint = Color.Black.WithAlpha( shadowOpacity );
VfxOpacity = Utils.Map( progress, 0f, 1f, 1f, 0f, EasingType.QuadIn );
//Gizmo.Draw.Color = Color.White;
//Gizmo.Draw.Text( $"{progress}", new global::Transform( (Vector3)Position2D + new Vector3( 0f, -0.4f, 0f ) ) );
IgnoreCollision = progress > 0.3f;
if ( _spawnCloudTime > (0.3f / TimeScale) )
{
var cloud = Manager.Instance.SpawnCloud( Position2D + new Vector2( 0f, 0.05f ) );
cloud.Velocity = new Vector2( Game.Random.Float( -1f, 1f ), Game.Random.Float( -1f, 1f ) ).Normal * Game.Random.Float( 0.2f, 0.6f );
_spawnCloudTime = Game.Random.Float( 0f, 0.15f );
}
}
return;
}
else
{
Vector2 toTarget = (targetPos - Position2D).Normal;
if ( Manager.Instance.Difficulty >= 1 )
{
if ( (targetPos - Position2D).LengthSquared < MathF.Pow( _perpendicularMaxDist, 2f ) )
toTarget = Vector2.Lerp( toTarget, new Vector2( toTarget.y, -toTarget.x ) * (_moveClockwise ? -1f : 1f), Utils.Map( (targetPos - Position2D).LengthSquared, MathF.Pow( _perpendicularMaxDist, 2f ), MathF.Pow( 1.5f, 2f ), 0f, 1f ) );
}
Velocity += toTarget * 1.0f * dt * (IsFeared ? -1f : 1f);
}
float speed = 0.5f * (IsAttacking ? 1.3f : 0.7f) + Utils.FastSin( MoveTimeOffset + Time.Now * (IsAttacking ? 12f : 4.5f) ) * (IsAttacking ? 0.66f : 0.35f);
if ( Manager.Instance.Difficulty < 0 )
speed *= 0.85f;
WorldPosition += (Vector3)Velocity * speed * dt;
if ( !IsShooting && !IsDigging && (!IsAttacking || IsCharmed) && !Manager.Instance.IsGameOver )
{
var target_dist_sqr = (targetPos - Position2D).LengthSquared;
var range = MathF.Pow( 7.5f, 2f );
if ( Manager.Instance.Difficulty < 0 )
range *= 0.7f;
if ( target_dist_sqr < range )
{
_shootDelayTimer -= dt;
if ( _shootDelayTimer < 0f )
StartShooting();
}
if ( target_dist_sqr < MathF.Pow( 14f, 2f ) && Manager.Instance.Difficulty > 0 )
{
_digDelayTimer -= dt;
if ( _digDelayTimer < 0f )
StartDigging();
}
}
}
protected override void UpdateSprite( Thing target )
{
if ( Sprite.CurrentAnimation.Name.Contains( "shoot" ) || IsDigging ) return;
base.UpdateSprite( target );
}
public void StartShooting()
{
_shotTimer = SHOOT_TIME;
IsShooting = true;
CanAttack = false;
CanAttackAnim = false;
CanTurn = false;
_hasShot = false;
_hasReversed = false;
_prepareStartTime = 0f;
Velocity *= 0.25f;
DontChangeAnimSpeed = true;
AnimSpeed = 1f;
BroadcastShootAnim();
ShouldUpdateAfterGameOver = true;
}
void BroadcastShootAnim()
{
Sprite.PlayAnimation( "shoot" );
}
public void CreateSpike()
{
var target_pos = Target.IsValid()
? Target.Position2D + Target.Velocity * Game.Random.Float( 0.1f, 3f ) + new Vector2( Game.Random.Float( -1f, 1f ), Game.Random.Float( -1f, 1f ) ) * 1.2f
: Position2D + Utils.GetRandomVector() * Game.Random.Float( 3.5f, 6f );
var BUFFER = 0.125f;
var spike = Manager.Instance.SpawnEnemySpikeElite(
new Vector2( Math.Clamp( target_pos.x, Manager.Instance.BOUNDS_MIN.x + BUFFER, Manager.Instance.BOUNDS_MAX.x - BUFFER ), Math.Clamp( target_pos.y, Manager.Instance.BOUNDS_MIN.y + BUFFER, Manager.Instance.BOUNDS_MAX.y - BUFFER ) ),
target: Target
);
if(IsCharmed)
{
spike.BecomeCharmed();
}
Manager.Instance.PlaySfxNearby( "spike.prepare", target_pos, pitch: Game.Random.Float( 0.95f, 1.05f ), volume: 1.5f, maxDist: 5f );
}
public void FinishShooting()
{
_shootDelayTimer = Game.Random.Float( SHOOT_DELAY_MIN, SHOOT_DELAY_MAX ) * (Manager.Instance.Difficulty < 0 ? 2.5f : 1f);
IsShooting = false;
CanAttack = true;
CanAttackAnim = true;
CanTurn = true;
DontChangeAnimSpeed = false;
BroadcastIdleAnim();
ShouldUpdateAfterGameOver = false;
if ( Manager.Instance.IsGameOver && !Manager.Instance.ShouldUpdateThings )
Celebrate();
}
void BroadcastIdleAnim()
{
Sprite.PlayAnimation( AnimIdlePath );
}
public override void Colliding( Thing other, float percent, float dt )
{
base.Colliding( other, percent, dt );
if ( other is Enemy enemy && !enemy.IsDying )
{
var spawnFactor = Utils.Map( enemy.TimeSinceSpawn, 0f, enemy.SpawnTime, 0f, 1f, EasingType.QuadIn );
Velocity += (Position2D - enemy.Position2D).Normal * Utils.Map( percent, 0f, 1f, 0f, 1f ) * enemy.PushStrength * (1f + enemy.TempWeight) * spawnFactor * dt;
if ( IsAttacking && IsCharmed != enemy.IsCharmed && _damageTime > (DAMAGE_TIME / TimeScale) )
{
var dmg = DamageToPlayer;
if ( IsCharmed )
dmg *= CharmDamageDealtMultiplier;
enemy.Damage( dmg, null, addVel: Vector2.Zero, addTempWeight: 0f, isCrit: false, DamageType.Melee );
enemy.Target = this;
_damageTime = 0f;
Manager.Instance.PlaySfxNearby( "zombie.attack.player", Position2D, pitch: Utils.Map( enemy.Health, enemy.MaxHealth, 0f, 0.95f, 1.15f, EasingType.QuadIn ), volume: 0.6f, maxDist: 4.5f );
}
}
// todo: move collision check to player instead to prevent laggy hits?
else if ( other is Player player )
{
if ( !player.IsDead )
{
Velocity += (Position2D - player.Position2D).Normal * Utils.Map( percent, 0f, 1f, 0f, 1f ) * player.Stats[PlayerStat.PushStrength] * (1f + player.TempWeight) * dt;
if ( IsAttacking && _damageTime > (DAMAGE_TIME / TimeScale) && !IsCharmed )
{
float dmg = player.CheckDamageAmount( DamageToPlayer, DamageType.Melee );
if ( !player.IsInvulnerable && !player.IsTimePausedForChoosing )
{
Manager.Instance.PlaySfxNearby( "zombie.attack.player", Position2D, pitch: Utils.Map( player.Health, player.Stats[PlayerStat.MaxHp], 0f, 0.95f, 1.15f, EasingType.QuadIn ), volume: 1f, maxDist: 5.5f );
player.Damage( dmg );
if ( dmg > 0f )
OnDamagePlayer( player, dmg );
}
_damageTime = 0f;
}
}
}
}
public override void Damage( float damage, Player player, Vector2 addVel, float addTempWeight, bool isCrit = false, DamageType damageType = DamageType.PlayerBullet )
{
base.Damage( damage, player, addVel, addTempWeight, isCrit, damageType );
if ( Game.Random.Float( 0f, 1f ) < Utils.Map( damage, 1f, 20f, 0.1f, 0.7f ) )
_digDelayTimer *= Game.Random.Float( 0.6f, 0.95f );
}
public void StartDigging()
{
IsDigging = true;
_timeSinceStartDigging = 0f;
CanAttack = false;
CanAttackAnim = false;
CanTurn = false;
Velocity *= 0.5f;
Sprite.PlayAnimation( "dig" );
Manager.Instance.PlaySfxNearby( "zombie.dirt", Position2D, pitch: Game.Random.Float( 0.5f, 0.55f ), volume: 0.6f, maxDist: 7.5f );
//SS2Game.PlaySfx( "zombie.dirt", Position, pitch: Game.Random.Float( 0.6f, 0.8f ), volume: 0.7f );
//SS2Game.Current.DustCloudClient( Position2D );
ShouldUpdateAfterGameOver = true;
}
void FinishDigging( Vector2 pos )
{
Position2D = pos;
//WorldPosition = ((Vector3)Position2D).WithZ( ZPos );
Transform.ClearInterpolation();
IsDigging = false;
CanAttack = true;
CanAttackAnim = true;
CanTurn = true;
IgnoreCollision = false;
//SetAnim( "Attack1" );
_moveClockwise = !_moveClockwise;
_digDelayTimer = Game.Random.Float( DIG_DELAY_MIN, DIG_DELAY_MAX );
//SS2Game.PlaySfx( "zombie.dirt", Position, pitch: Game.Random.Float( 0.6f, 0.8f ), volume: 0.7f );
//SS2Game.Current.DustCloudClient( Position2D );
IsSpawning = true;
//_hasDug = true;
TimeSinceSpawn = 0f;
//SpawnProgress = 0f;
//ShadowRadiusModifier = 1.5f;
//ShadowOpacityModifier = 0f;
Manager.Instance.PlaySfxNearby( "zombie.dirt", Position2D, pitch: Game.Random.Float( 0.85f, 0.9f ), volume: 0.6f, maxDist: 7.5f );
ShadowSprite.Tint = Color.Black.WithAlpha( 0f );
VfxOpacity = 0f;
}
public override void Celebrate()
{
base.Celebrate();
if ( IsShooting || IsDigging )
return;
CelebrateAsync();
}
async void CelebrateAsync()
{
await Task.Delay( Game.Random.Int( 0, 500 ) );
Sprite.PlaybackSpeed = Game.Random.Float( 3.5f, 4.5f );
Sprite.PlayAnimation( "cheer_start" );
await Task.Delay( Game.Random.Int( 400, 500 ) );
Sprite.PlaybackSpeed = Game.Random.Float( 1.5f, 5f );
Sprite.PlayAnimation( "cheer" );
}
protected override void FinishSpawning()
{
base.FinishSpawning();
ShouldUpdateAfterGameOver = false;
if ( Manager.Instance.IsGameOver && !Manager.Instance.ShouldUpdateThings )
Celebrate();
}
}