things/enemies/Sword.cs

An Enemy subclass representing a falling sword hazard. It controls spawn behavior, movement while spawning, despawn/shake effects, spawn/death sounds, and optional gib spawning; it disables normal enemy interactions like attacking or being targeted.

NetworkingFile Access
using System;
using System.Numerics;
using Sandbox;

public class Sword : Enemy
{
	public override EnemyType EnemyType => EnemyType.Sword;
	public override float GetMaxHealth()
	{
		return 3000f;
	}

	public override Vector3 SpawnScale => new Vector3( 3f );
	public override float SpawnTime => 0.25f + SPAWN_AIM_TIME;

	public override float SpawnZPos => 750f;
	public override float GroundZPos => 10f;

	//public override bool CanHaveTarget => false;
	public override bool CanAttack => false;
	public override bool CanTurn => false;
	public override bool CanBeBackstabbed => false;
	public override bool CountsAsKill => false;
	public override bool CanMove => false;
	public override bool IsInanimate => true;
	public override bool CanBeTargeted => false;
	public override bool ShouldCreateSpawnClouds => false;
	public override bool CanDamageByTouch => false;
	public override bool CanCombust => false;

	public bool IsInSpawnDamagePhase => (IsSpawning && TimeSinceSpawn > SPAWN_AIM_TIME) || _timeSinceFinishSpawn < 0.2f;

	protected string _debrisName;

	private bool _isDespawning;
	private TimeSince _timeSinceDespawn;
	private bool _hasShaked;
	private float _leaveTime;

	private GameObject _warningSwordEffect;
	private const float SPAWN_AIM_TIME = 1.5f;

	private TimeSince _timeSinceFinishSpawn;

	private float _spawnSpeed;
	private float _spawnSpeedFactorClose;
	private float _spawnSpeedFactorFar;
	private float _spawnDeceleration;

	private bool _hasPlayedSpawnSfx;

	protected override void OnStart()
	{
		base.OnStart();

		CoinValueMin = 0;
		CoinValueMax = 0;
		CoinChance = 0f;

		PushStrength = 20000f;
		Weight = 17f;

		_debrisName = "barrel_debris";

		MeleeDamage = Utils.Select( Manager.Instance.Difficulty, 9f, 13f, 14f );
		DamageTargetDelay = 0.3f;

		DetectTargetRange = 2000f;
		LoseTargetRange = 3000f;

		_warningSwordEffect = GameObject.Clone( "prefabs/effects/warning_sword.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( new Vector3( Position2D.x, Position2D.y, 5f ) ) } );

		if ( IsProxy )
			return;

		_spawnSpeed = Game.Random.Float( 80f, 500f );
		_spawnSpeedFactorClose = Game.Random.Float( 0.1f, 0.7f );
		_spawnSpeedFactorFar = Game.Random.Float( 0.5f, 1.5f );
		_spawnDeceleration = Game.Random.Float( 0.5f, 1.2f );

		Deceleration = 4.2f;

		_leaveTime = Game.Random.Float( 4.5f, 5f );
	}

	protected override void HandleSpawning()
	{
		if ( TimeSinceSpawn > SpawnTime )
		{
			FinishSpawning();
		}
		else
		{
			SpawnProgress = Utils.Map( TimeSinceSpawn, 0f, SpawnTime, 0f, 1f );

			if ( TimeSinceSpawn < SPAWN_AIM_TIME )
			{
				ModelRenderer.Tint = Color.White.WithAlpha( Utils.Map( TimeSinceSpawn, 0f, 0.5f, 0f, 1f ) );

				if ( !IsProxy && !HasTarget )
					CheckForTarget();

				if ( !IsProxy && HasTarget )
				{
					Velocity += (TargetUnit.Position2D - Position2D).Normal * _spawnSpeed * Time.Delta * Utils.Map( (TargetUnit.Position2D - Position2D).LengthSquared, 0f, 200f * 200f, _spawnSpeedFactorClose, _spawnSpeedFactorFar );
					Velocity *= Math.Max( 1f - Time.Delta * _spawnDeceleration, 0f );

					Position2D += Velocity * Time.Delta;

					//WorldPosition = new Vector3( TargetUnit.Position2D.x, TargetUnit.Position2D.y, WorldPosition.z );
				}

				_warningSwordEffect.WorldPosition = new Vector3( WorldPosition.x, WorldPosition.y, _warningSwordEffect.WorldPosition.z );
 			}
			else
			{
				Velocity = Vector2.Zero;

				WorldPosition = WorldPosition.WithZ( Utils.Map( TimeSinceSpawn, SPAWN_AIM_TIME, SpawnTime, SpawnZPos, GroundZPos, EasingType.ExpoIn ) );

				if( !_hasPlayedSpawnSfx )
				{
					Manager.Instance.PlaySfxNearby( "sword_fall", Position2D, pitch: Game.Random.Float(1f, 1.1f), volume: 1f, maxDist: 600f );
					_hasPlayedSpawnSfx = true;
				}
			}
		}
	}

	protected override void OnUpdate()
	{
		base.OnUpdate();

		//Gizmo.Draw.Color = Color.White;
		//Gizmo.Draw.Text( $"{WorldPosition.z}", new global::Transform( WorldPosition ) );

		if ( IsSpawning )
			return;

		if ( IsProxy )
			return;

		Velocity *= Math.Max( 1f - Time.Delta * Deceleration * Manager.Instance.GlobalFrictionModifier, 0f );

		WorldPosition += (Vector3)Velocity * Time.Delta;

		if( _isDespawning )
		{
			var despawnTime = 0.45f;
			if( _timeSinceDespawn > despawnTime )
			{
				Remove();
			}
			else
			{
				WorldPosition = WorldPosition.WithZ( Utils.Map( _timeSinceDespawn, 0f, despawnTime, GroundZPos, 1500f, EasingType.QuadOut) );
				ModelRenderer.Tint = Color.White.WithAlpha( Utils.Map( _timeSinceDespawn, 0f, despawnTime, 1f, 0f, EasingType.QuadIn ) );
			}
		}
		else
		{
			var leaveTime = _leaveTime;
			if( TimeSinceSpawn > leaveTime )
			{
				_isDespawning = true;
				_timeSinceDespawn = 0f;

				Manager.Instance.ShakeCamsNearby( Position2D, radius: 300f, maxStrength: Game.Random.Float( 4f, 4.5f ), time: 0.1f );
			}

			var shakeTime = 0.3f;
			if (!_hasShaked && TimeSinceSpawn > leaveTime - shakeTime )
			{
				_hasShaked = true;
				ShakeRpc( startStrength: 0f, endStrength: 1f, time: shakeTime, easingType: EasingType.SineInOut );
			}
		}
	}

	public override void Colliding( Thing other, float percent, float dt )
	{
		base.Colliding( other, percent, dt );
	}

	public override void Flinch( float time, Vector2 dir )
	{
		base.Flinch( time, dir );

		ModelRenderer.LocalRotation = new Angles(Game.Random.Float(-3f, 3f), 0f, 180f + Game.Random.Float(-3f, 3f));
	}

	public override void StopFlinching()
	{
		base.StopFlinching();

		ModelRenderer.LocalRotation = new Angles(0f, 0f, 180f );
	}

	public override void SetAnim( string name, bool forceRestart = false )
	{
		return;
	}

	protected override void DropLoot( Player player )
	{
		// do nothing
	}

	protected override void SpawnGibs( Vector2 dir, float force, DamageType damageType )
	{
		// todo:

		//GameObject.Clone( $"prefabs/effects/{_debrisName}.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( WorldPosition.WithZ( Game.Random.Float( 40f, 60f ) ), Rotation.Identity ) } );

		//force = Game.Random.Float( 1f, 1.3f );

		//for ( int i = 0; i < 3; i++ )
		//{
		//	var offsetDir = Utils.GetRandomVector();
		//	var pos = WorldPosition + (Vector3)offsetDir * Radius;
		//	pos = pos.WithZ( Game.Random.Float( 15f, 30f ) );
		//	var velocity = (pos - WorldPosition).Normal * Game.Random.Float( 100f, 250f );
		//	SpawnGib( "gib_barrel_01", pos.WithZ( Game.Random.Float( 25f, 40f ) ), velocity + new Vector3( dir.x * Game.Random.Float( -5f, 25f ), dir.y * Game.Random.Float( -5f, 25f ), Game.Random.Float( 30f, 150f ) ), force, chance: 0.5f );
		//}

		//for ( int i = 0; i < 5; i++ )
		//{
		//	var offsetDir = Utils.GetRandomVector();
		//	var pos = WorldPosition + (Vector3)offsetDir * Radius;
		//	pos = pos.WithZ( Game.Random.Float( 15f, 30f ) );
		//	var velocity = (pos - WorldPosition).Normal * Game.Random.Float( 100f, 250f );
		//	SpawnGib( "gib_barrel_12", pos.WithZ( Game.Random.Float( 25f, 40f ) ), velocity + new Vector3( dir.x * Game.Random.Float( -5f, 25f ), dir.y * Game.Random.Float( -5f, 25f ), Game.Random.Float( 30f, 150f ) ), force, chance: 0.5f );
		//}
	}

	protected void SpawnGib( string name, Vector3 pos, Vector3 velocity, float force, float chance = 1f )
	{
		chance *= Utils.Map( Manager.Instance.NumGibs, 0, 100, 1f, 0f, EasingType.SineIn );

		if ( Game.Random.Float( 0f, 1f ) > chance )
			return;

		var gib = GameObject.Clone( $"prefabs/gibs/barrel/{name}.prefab", new global::Transform( pos, new Angles( Game.Random.Float( -5f, 5f ), Game.Random.Float(0f, 360f), Game.Random.Float( -5f, 5f ) ) ) );
		var rigidBody = gib.GetComponent<Rigidbody>();
		var horizontalForceMultiplier = IsExploding ? 2f : 1f;
		var verticalForceMultiplier = IsExploding ? 0.5f : 1f;
		rigidBody.Velocity = new Vector3( velocity.x * horizontalForceMultiplier, velocity.y * horizontalForceMultiplier, velocity.z * verticalForceMultiplier ) * force;
		rigidBody.AngularVelocity = new Vector3( Game.Random.Float( -15f, 15f ), Game.Random.Float( -15f, 15f ), Game.Random.Float( -15f, 15f ) ) * force;

		var gibFader = gib.GetComponent<GibFader>();
		gibFader.Color = Color.White;
		gibFader.Lifetime = Game.Random.Float( 0.5f, 3.5f );
	}

	protected override void PlayDeathSfx( Vector2 pos )
	{
		Manager.Instance.PlaySfxNearby( "chest.break", Position2D, pitch: Game.Random.Float( 1.15f, 1.2f ), volume: 1.4f, maxDist: 450f );
	}

	protected override void FinishSpawning()
	{
		base.FinishSpawning();

		SpawnLandingClouds();

		Manager.Instance.PlaySfxNearby( "slam", Position2D, pitch: Game.Random.Float( 1.8f, 2.2f ), volume: 1f, maxDist: 600f );

		Manager.Instance.ShakeCamsNearby( Position2D, radius: 400f, maxStrength: Game.Random.Float( 5f, 6f ), time: Game.Random.Float( 0.2f, 0.25f ) );

		if ( !IsProxy )
			Manager.Instance.RequestTimeScale( startTimeScale: 0f, endTimeScale: 1f, duration: 0.15f, priority: 10 );

		_timeSinceFinishSpawn = 0f;
	}

	public void SpawnLandingClouds()
	{
		int numClouds = Game.Random.Int( 3, 7 );
		var startAngle = Game.Random.Float( 0f, 360f );
		var increment = 360f / numClouds;
		for ( int i = 0; i < numClouds; i++ )
		{
			var offsetDir = Utils.GetVector2FromAngleDegrees( startAngle );
			var pos = WorldPosition + (Vector3)offsetDir * Game.Random.Float( 0.5f, 1f );
			pos = pos.WithZ( Game.Random.Float( 8f, 13f ) );

			Vector2 velocity = ((Vector2)pos - Position2D).Normal * Game.Random.Float( 500f, 800f );
			var deceleration = Game.Random.Float( 7f, 10f );

			Manager.Instance.SpawnCloud( pos, velocity, deceleration, lifetime: Game.Random.Float( 1f, 2f ), bright: true );

			startAngle += increment * Game.Random.Float( 0.8f, 1.2f );
		}
	}
}