things/Spear.cs

A Thing-derived Spear entity used in-game. It manages model, size scaling based on Damage, movement including wind, collision with enemies and obstacles, applies damage and spawning impact particles, and destroys itself after lifetime or on obstacle hit.

NetworkingFile Access
using System;
using Sandbox;

public class Spear : Thing
{
	[Property] public ModelRenderer Model { get; set; }

	public Player Shooter { get; set; }

	public float BaseZPos { get; set; }

	private List<Thing> _hitThings;

	public float Damage { get; set; }
	public float DamagePerPierce { get; set; }
	public float Lifetime { get; set; }

	private bool _sizeDirty;

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

		Radius = 5f;

		if ( IsProxy )
			return;

		CollideWithTags.Add( "enemy" );
		CollideWithTags.Add( "obstacle" );

		_hitThings = new();

		_sizeDirty = true;
	}

	public void Init()
	{
		
	}

	void DetermineSize()
	{
		var damage = Damage;
		var scale = damage < 30f
			? Utils.Map( damage, 0f, 30f, 0.9f, 1f, EasingType.QuadOut )
			: Utils.Map( damage, 30f, 150f, 1f, 1.4f, EasingType.QuadIn );

		Radius = 5f * scale;

		WorldScale = new Vector3( scale );
		
		//_pfakeshadow.Set("Size", 9f * Scale);

		_sizeDirty = false;
	}

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

		if ( IsProxy )
			return;

		//Gizmo.Draw.Color = Color.White;
		//Gizmo.Draw.Text( $"\n\n\n{Stats[BulletStat.NumPiercing]}", new global::Transform( WorldPosition ) );

		if ( _sizeDirty )
			DetermineSize();

		var zPos = Utils.Map( TimeSinceSpawn, 0f, Lifetime, BaseZPos, 0f, EasingType.QuadIn );

		if ( Manager.Instance.IsWindActive )
			Velocity += Manager.Instance.GlobalWindForce * 0.75f * Time.Delta;

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

		if ( TimeSinceSpawn > Lifetime )
		{
			var scaleMultiplier = Utils.Map( Damage, 1f, 5f, 0.5f, 1f, EasingType.Linear ) * Utils.Map( Damage, 5f, 30f, 1f, 1.5f, EasingType.Linear );
			Manager.Instance.SpawnBulletImpactParticlesRpc( WorldPosition.WithZ(10f), Vector3.Up, Color.White, scaleMultiplier );

			Remove();

			return;
		}
	}

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

		if ( !Shooter.IsValid() || _hitThings.Contains( other ) )
			return;

		if ( other is Enemy enemy )
		{
			//if ( enemy.IsDying || (enemy.IsSpawning && enemy.SpawnProgress < 0.7f) )
			if ( enemy.IsSpawning && enemy.SpawnProgress < 0.7f )
				return;

			Vector2 dir = Velocity.Normal;

			//if ( Stats[BulletStat.Force] > 0f )
			//	enemy.AddVelocity( Vector2.Lerp( Velocity2D.Normal, dir, 0.3f ) * Stats[BulletStat.Force] * (1f / enemy.Weight) * Utils.Map( dmg, 0f, 5f, 0.1f, 1f ) );

			var shouldFlinch =  Damage < enemy.MaxHealth * 0.05f ? false : true;
			var force = dir * Damage * 0.1f;
			enemy.DamageRpc( Damage, Shooter, DamageType.Spear, WorldPosition, force, isCrit: false, shouldFlinch );

			// todo: needs to use GetDamageMultiplier

			_hitThings.Add( enemy );

			Damage += DamagePerPierce;

			WorldRotation = WorldRotation.RotateAroundAxis( Vector3.Forward, Game.Random.Float( -40f, -25f ) );

			_sizeDirty = true;
		}
		else if ( other is Obstacle obstacle )
		{
			var normal = (Position2D - obstacle.Position2D).LengthSquared > Manager.TOUCH_DIST_REQUIRED_SQR
				? (Position2D - obstacle.Position2D).Normal
				: Utils.GetRandomVector();

			var scaleMultiplier = Utils.Map( Damage, 1f, 40f, 0.5f, 1f, EasingType.Linear ) * Utils.Map( Damage, 40f, 100f, 1f, 1.25f, EasingType.Linear );
			Manager.Instance.SpawnBulletImpactParticlesRpc( WorldPosition, normal, Color.White, scaleMultiplier );

			Remove();
		}
	}

	public void Remove()
	{
		GameObject.Destroy();
	}
}