things/enemies/RunnerElite.cs
using Sandbox;
public class RunnerElite : Enemy
{
private TimeSince _damageTime;
private const float DAMAGE_TIME = 0.5f;
public override float FullOpacity => Manager.Instance.Difficulty < 0 ? 0.2f : 0.1f;
public override float HeightVariance => 0.02f;
public override float WidthVariance => 0.01f;
protected bool _isJumping;
private Vector2 _jumpStartPos;
private Vector2 _jumpTargetPos;
private TimeSince _timeSinceBeginJump;
private float _jumpTime;
private float _jumpHeight;
private bool _isPreparingJump;
private float _prepareJumpTime;
private TimeSince _timeSincePrepareJump;
private TimeSince _timeSinceJumping;
private float _delayUntilNextJump;
public float MoveSpeed { get; set; }
protected float JUMP_DELAY_MIN = 1f;
protected float JUMP_DELAY_MAX = 8f;
protected override void OnAwake()
{
//OffsetY = -0.54f;
ShadowScale = 1.3f;
ShadowFullOpacity = Manager.Instance.Difficulty < 0 ? 0.4f : 0.2f;
ShadowOpacity = 0f;
Scale = 1.3f;
base.OnAwake();
//AnimSpeed = 2f;
//Sprite.Texture = Texture.Load("textures/sprites/runner.vtex");
//Sprite.Size = new Vector2( 1f, 1f ) * Scale;
PushStrength = 10f;
Deceleration = 0.27f;
DecelerationAttacking = 0.1f;
AggroRange = 2.5f;
Radius = 0.29f;
Health = 120f;
if ( Manager.Instance.Difficulty < 0 )
Health = 105f;
MaxHealth = Health;
DamageToPlayer = 13f;
CoinValueMin = 2;
CoinValueMax = 4;
CoinChance = 0.75f;
Sprite.PlayAnimation( AnimSpawnPath );
Sprite.Tint = Color.White.WithAlpha( FullOpacity );
if ( IsProxy )
return;
CollideWith.Add( typeof( Enemy ) );
CollideWith.Add( typeof( Player ) );
//ShadowScale = 0.95f;
_damageTime = DAMAGE_TIME;
_delayUntilNextJump = Game.Random.Float( JUMP_DELAY_MIN, JUMP_DELAY_MAX );
MoveSpeed = 1f;
}
protected override void UpdatePosition( float dt )
{
base.UpdatePosition( dt );
var targetPos = Target.IsValid() ? Target.Position2D : (IsCharmed ? Manager.Instance.Player.Position2D : Position2D);
var targetVel = Target.IsValid() ? Target.Velocity : (IsCharmed ? Manager.Instance.Player.Velocity : Vector2.Zero);
if ( Manager.Instance.Difficulty >= 1 && !_isPreparingJump && !_isJumping && _timeSinceJumping > _delayUntilNextJump * (IsAttacking ? 2f : 1f) && !Manager.Instance.IsGameOver )
{
PrepareJump( targetPos + targetVel * Game.Random.Float( 0f, 4f ) + Utils.GetRandomVector() * Game.Random.Float( 0f, 1f ) );
}
if ( _isPreparingJump )
{
if ( Manager.Instance.IsGameOver )
_isPreparingJump = false;
if ( _timeSincePrepareJump > _prepareJumpTime )
{
Jump();
}
Velocity *= (1f - dt * 1.75f);
WorldPosition += (Vector3)Velocity * dt;
return;
}
if ( _isJumping )
{
if ( _timeSinceBeginJump > _jumpTime )
{
FinishJump();
}
else
{
float progress = Utils.Map( _timeSinceBeginJump, 0f, _jumpTime, 0f, 1f, EasingType.Linear );
Position2D = Vector2.Lerp( _jumpStartPos, _jumpTargetPos, progress );
Sprite.LocalPosition = new Vector3( 0f, Utils.MapReturn( progress, 0f, 1f, 0f, _jumpHeight, EasingType.SineOut ), 0f );
float shadowScale = Utils.MapReturn( progress, 0f, 1f, ShadowScale, ShadowScale * 0.5f, EasingType.SineOut );
ShadowSprite.LocalScale = new Vector3( shadowScale * Globals.SPRITE_SCALE, shadowScale * Globals.SPRITE_SCALE, 1f );
float shadowOpacity = Utils.MapReturn( progress, 0f, 1f, ShadowOpacity, ShadowOpacity * 0.5f, EasingType.SineOut );
ShadowSprite.Tint = Color.Black.WithAlpha( shadowOpacity );
}
return;
}
Velocity += (targetPos - Position2D).Normal * dt * (IsFeared ? -1f : 1f);
float speed = ( IsAttacking ? 0.9f : 0.2f) + Utils.FastSin( MoveTimeOffset + Time.Now * (IsAttacking ? 14f : 5.5f) ) * (IsAttacking ? 0.8f : 0.2f);
speed *= MoveSpeed;
if ( Manager.Instance.Difficulty < 0 )
speed *= 0.75f;
//else if ( Manager.Instance.Difficulty >= 1 )
// speed *= 1.5f;
WorldPosition += (Vector3)Velocity * speed * dt;
}
protected override void UpdateSprite( Thing target )
{
if ( _isPreparingJump || _isJumping )
return;
base.UpdateSprite( target );
}
public override void StartAttacking()
{
base.StartAttacking();
Manager.Instance.PlaySfxNearby( "runner.bark", Position2D, pitch: Game.Random.Float( 0.7f, 0.75f ), volume: 1f, maxDist: 4f );
}
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( "runner.bite", Position2D, pitch: Utils.Map( enemy.Health, enemy.MaxHealth, 0f, 0.9f, 0.95f, EasingType.QuadIn ), volume: 0.6f, maxDist: 4.5f );
DealDamage();
}
}
// 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( "runner.bite", Position2D, pitch: Utils.Map( player.Health, player.Stats[PlayerStat.MaxHp], 0f, 0.9f, 0.95f, EasingType.QuadIn ), volume: 1f, maxDist: 5.5f );
player.Damage( dmg );
if( dmg > 0f )
OnDamagePlayer( player, dmg );
}
_damageTime = 0f;
DealDamage();
}
}
}
}
public virtual void DealDamage()
{
}
public override void StartDying( Player player )
{
Sprite.Tint = Sprite.Tint.WithAlpha( 1f );
base.StartDying( player );
}
public void PrepareJump( Vector2 targetPos )
{
var BUFFER = 0.3f;
targetPos = new Vector2( MathX.Clamp( targetPos.x, Manager.Instance.BOUNDS_MIN.x + BUFFER, Manager.Instance.BOUNDS_MAX.x - BUFFER ), MathX.Clamp( targetPos.y, Manager.Instance.BOUNDS_MIN.y + BUFFER, Manager.Instance.BOUNDS_MAX.y - BUFFER ) );
float MAX_DIST = Game.Random.Float( 3.7f, 4.1f );
if ( (targetPos - Position2D).Length > MAX_DIST )
targetPos = Position2D + (targetPos - Position2D).Normal * MAX_DIST;
_jumpTargetPos = targetPos;
_jumpTime = Utils.Map( (targetPos - Position2D).Length, 1.5f, MAX_DIST, 0.65f, 1.15f, EasingType.SineIn ) * Game.Random.Float( 0.85f, 1.1f );
_jumpHeight = Utils.Map( _jumpTime, 0.7f, 3f, 1.8f, 3f ) * Game.Random.Float( 0.9f, 1.1f );
_isPreparingJump = true;
_timeSincePrepareJump = 0f;
_prepareJumpTime = Game.Random.Float( 0.2f, 0.6f );
IsAttacking = false;
CanTurn = false;
CanAttack = false;
CanAttackAnim = false;
FlipX = targetPos.x > Position2D.x;
Sprite.PlayAnimation( "jump_prepare" );
}
public void Jump()
{
if ( Manager.Instance.IsGameOver )
return;
_isPreparingJump = false;
_isJumping = true;
_jumpStartPos = Position2D;
_timeSinceBeginJump = 0f;
IgnoreCollision = true;
ShouldUpdateAfterGameOver = true;
Sprite.PlayAnimation( "jump" );
Manager.Instance.PlaySfxNearby( "jump_whoosh", Position2D, pitch: Game.Random.Float( 1.65f, 1.7f ), volume: 0.4f, maxDist: 8f );
}
void FinishJump()
{
_isJumping = false;
Position2D = _jumpTargetPos;
Sprite.LocalPosition = Vector3.Zero;
IgnoreCollision = false;
CanTurn = true;
CanAttack = true;
CanAttackAnim = true;
ShouldUpdateAfterGameOver = false;
if ( Manager.Instance.IsGameOver && !Manager.Instance.ShouldUpdateThings )
Celebrate();
_timeSinceJumping = 0f;
_delayUntilNextJump = Game.Random.Float( JUMP_DELAY_MIN, JUMP_DELAY_MAX );
ShadowSprite.LocalScale = new Vector3( ShadowScale * Globals.SPRITE_SCALE, ShadowScale * Globals.SPRITE_SCALE, 1f );
ShadowSprite.Tint = Color.Black.WithAlpha( ShadowOpacity );
Manager.Instance.PlaySfxNearby( "jump_thud", Position2D, pitch: Game.Random.Float( 0.95f, 1f ), volume: 0.6f, maxDist: 6f );
for ( int i = 0; i < Game.Random.Int( 2, 4 ); i++ )
{
SpawnCloudClient( Position2D, Utils.GetRandomVector() * Game.Random.Float( 0.3f, 1.5f ) );
}
if ( Target.IsValid() )
Velocity += (Target.Position2D - Position2D).Normal * Game.Random.Float( -0.2f, 2f );
}
protected virtual void SpawnLandingClouds()
{
for ( int i = 0; i < Game.Random.Int( 2, 4 ); i++ )
{
SpawnCloudClient( Position2D, Utils.GetRandomVector() * Game.Random.Float( 0.3f, 1.5f ) );
}
}
public override void Celebrate()
{
base.Celebrate();
if ( _isJumping )
return;
CelebrateAsync();
}
async void CelebrateAsync()
{
await Task.Delay( Game.Random.Int( 0, 500 ) );
Sprite.PlaybackSpeed = Game.Random.Float( 2f, 4f );
Sprite.Tint = Color.White.WithAlpha( FullOpacity * 2f );
Sprite.PlayAnimation( "cheer" );
}
}