things/SpikerHead.cs

A Thing subclass representing a spiking head hazard. It handles lifetime, plays a sound shortly after spawn, configures collision with players, and applies damage/force to players it touches during the active phase.

Networking
using System;
using Sandbox;

public class SpikerHead : Thing
{
	[Property] public SkinnedModelRenderer ModelRenderer { get; set; }
	public float Damage { get; set; }
	public float Lifetime { get; set; }

	public Enemy Shooter { get; set; }

	public float BaseZPos { get; set; }

	public override Vector3 SpawnScale => new Vector3( 1.25f );

	private List<Unit> _hitUnits = new();

	private protected float _hitForce;
	private protected float _depthBottom;
	private protected float _depthTop;

	private bool _hasPlayedSfx;

	// todo: TimeScale can apply to this if shooter is frozen?

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

		Lifetime = 1.7f;

		if ( IsProxy )
			return;

		CollideWithTags.Add( "player" );

		_hitForce = 240f;

		_depthBottom = -150f;
		_depthTop = -120f;

		Damage = Utils.Select( Manager.Instance.Difficulty, 7f, 9f, 12f );
	}

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

		//Gizmo.Draw.Color = Color.White;
		//Gizmo.Draw.LineSphere( WorldPosition.WithZ( 0f ), (Collider as SphereCollider).Radius * WorldScale.x );

		if (!_hasPlayedSfx && TimeSinceSpawn > 0.4f)
		{
			Manager.Instance.PlaySfxNearby( "spike.thrust", Position2D, pitch: Game.Random.Float( 1.15f, 1.3f ), volume: 1f, maxDist: 350f );
			_hasPlayedSfx = true;
		}

		if ( IsProxy )
			return;

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

		//var zPos = WorldPosition.z;

		//if ( TimeSinceSpawn < Lifetime * 0.4f )
		//	zPos = Utils.Map( TimeSinceSpawn, 0f, Lifetime * 0.4f, _depthBottom, _depthTop, EasingType.QuadIn );
		//else if ( TimeSinceSpawn > Lifetime * 0.6f )
		//	zPos = Utils.Map( TimeSinceSpawn, Lifetime * 0.6f, Lifetime, _depthTop, _depthBottom, EasingType.QuadOut );

		//WorldPosition = WorldPosition.WithZ( zPos );

		//if( TimeSinceSpawn > Lifetime * 0.4f && TimeSinceSpawn < Lifetime * 0.6f )
		//{
		//	WorldScale = SpawnScale * Utils.MapReturn( TimeSinceSpawn, Lifetime * 0.4f, Lifetime * 0.6f, 1f, 1.2f );
		//}
		//else
		//{
		//	WorldScale = SpawnScale;
		//}
		
		if ( TimeSinceSpawn > Lifetime )
			GameObject.Destroy();
	}

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

		if ( TimeSinceSpawn < Lifetime * 0.35f || TimeSinceSpawn > Lifetime * 0.65f )
			return;

		if ( _hitUnits.Contains( other ) )
			return;

		if ( other is Player player )
		{
			if ( !player.IsDead )
			{
				var dir = (player.Position2D - Position2D).Normal;
				var hitPos = player.Position2D - dir * player.Radius;
				player.DamageRpc( Damage, damageType: DamageType.SpikerHead, hitPos, dir, upwardAmount: Game.Random.Float(0.5f, 1.5f), _hitForce, ragdollForce: _hitForce * 0.01f, Shooter, enemyType: EnemyType.Spiker );
				
				_hitUnits.Add( player );
			}
		}
	}
}