Units/AutoWeapon.cs
using System;

namespace PlanetMeat;

[Group( "Planet Meat - Units" ), Title( "AutoWeapon" ), Icon( "search" )]
public sealed class AutoWeapon : Component, IUpgradeableAttack, IAttackBlame, IEnemyAttackReceiver
{
	public const float FAST_RECHECK = 0.2f;

	[Property] public float AimRotationSpeed = 5.0f;

	[Property] public int BaseDamage { get; set; } = 5;
	[Property] public string DamageImprovement { get; set; } = "";
	public ValueAggregator<int> AggregateDamage => field ??= ValueAggregator<int>.FromImprovement( DamageImprovement, BaseDamage );

	[Property] public float BaseRecharge { get; set; } = 1.0f;
	[Property] public string RechargeImprovement { get; set; } = "";
	public ValueAggregator<float> AggregateRecharge => field ??= ValueAggregator<float>.FromImprovement( RechargeImprovement, BaseRecharge );

	[Property] public bool IsMultishotter { get; set; } = false;
	[Property] public string MultishotRateImprovement { get; set; } = "";
	public ValueAggregator<float> AggregateMultishotRate => field ??= ValueAggregator<float>.FromImprovement( MultishotRateImprovement );
	private float MultishotProgress { get; set; } = 0.0f;
	private int _bakedMultishotShots = 0;
	private bool TrackMultishots { get; set; }

	[Property] public string MultishotAmountImprovement { get; set; } = "";
	public ValueAggregator<int> AggregateMultishotAmount => field ??= ValueAggregator<int>.FromImprovement( MultishotAmountImprovement );

	[Property] public string MultishotDamageImprovement { get; set; } = "";
	public ValueAggregator<float> AggregateMultishotDamage => field ??= ValueAggregator<float>.FromImprovement( RechargeImprovement );

	public float ShotDelay => 1.0f / this.GetAttackRecharge();
	private float untilShot = 0.0f;

	[Property] public string Ident { get; set; } = "";
	[Property] public Animator Animator { get; set; }
	[Property] private GameObject ModelGameObject { get; set; }
	[Property] private GameObject MuzzleGameObject { get; set; }
	[Property] private GameObject MuzzleEffect { get; set; }
	[Property] private GameObject TracerEffect { get; set; }

	public int ShotsFired { get; set; } = 0;

	public Targeter Targeter => field ??= this.FindNearestUpwards<Targeter>();
	private Targetable LatestTarget { get; set; } = null;
	private Vector3 LatestValidAimPosition { get; set; }

	protected override void OnStart()
	{
		base.OnStart();
		if ( Targeter is not null )
			untilShot = FAST_RECHECK;
		else
			Log.Warning( $"{this} is an AutoWeapon with no Targeter" );

		TrackMultishots = IsMultishotter && Research.Current.HasTech( "multishot" );
		if ( TrackMultishots )
			ApplyMultishotAmount();
	}

	public void ApplyMultishotAmount()
	{
		Targeter?.NumTargets = _bakedMultishotShots = 1 + AggregateMultishotAmount.Total;
	}

	protected override void OnUpdate()
	{
		base.OnUpdate();
		if ( Agenda.Current.Phase != Agenda.Phases.Wave || Targeter.Status.Killed )
			return;

		if ( untilShot > -0.01f )
		{
			untilShot -= Time.Delta;
			while ( untilShot < 0.01f )
			{
				var targets = Targeter.NextTargets;
				if ( targets.Count > 0 )
				{
					var numShots = 1;
					if ( TrackMultishots )
					{
						MultishotProgress += AggregateMultishotRate.Total;
						if ( MultishotProgress > 0.99f )
							numShots = _bakedMultishotShots;
					}

					Shoot( targets, numShots );
					LatestTarget = targets[0];
					untilShot += ShotDelay;
				}
				else
				{
					LatestTarget = null;
					untilShot = MathF.Min( FAST_RECHECK, ShotDelay );
				}
			}
		}

		if ( ModelGameObject is GameObject model && LatestTarget is Targetable t )
		{
			if ( t.IsValid )
				LatestValidAimPosition = t.BullseyeWorld;

			model.WorldRotation = Rotation.Lerp(
				model.WorldRotation,
				Rotation.LookAt( LatestValidAimPosition - model.WorldPosition ),
				AimRotationSpeed * Time.Delta
			);
		}
	}

	private void ShootEffects( GameObject muzzle )
	{
		if ( muzzle is not null )
			MuzzleEffect?.Clone( new CloneConfig { Parent = muzzle, Transform = global::Transform.Zero, StartEnabled = true } );
		Animator?.Attack();
	}

	private DamageInfo CreateDamage( float dmg )
	{
		var d = new DamageInfo( dmg, Targeter.Status.GameObject, GameObject );
		d.SetFriendlyFire( Targeter.CanTargetFriends );
		return d;
	}
	private DamageInfo CreateDamage() => CreateDamage( this.GetAttackDamage() );

	private void ShootTarget( Targetable targ, in DamageInfo damage, GameObject muzzle )
	{
		var tracer = TracerEffect?.Clone( new CloneConfig { Transform = (muzzle?.WorldTransform ?? WorldTransform).WithScale( 1.0f ), StartEnabled = true } );
		tracer?.GetComponentInChildren<ITargetedEffect>()?.SetTarget( targ.NextBullseye );
		targ.OnDamage( in damage );
		ShotsFired++;
	}

	private void Shoot( Targetable targ )
	{
		var muzzle = MuzzleGameObject;
		ShootEffects( muzzle );
		ShootTarget( targ, CreateDamage(), muzzle );
	}

	private void Shoot( List<Targetable> targs, int numShots )
	{
		if ( targs.Count < numShots )
			numShots = targs.Count;
		if ( numShots < 1 )
			return;

		if ( numShots == 1 )
		{
			Shoot( targs[0] );
		}
		else
		{
			MultishotProgress -= 1.0f;

			var muzzle = MuzzleGameObject;
			ShootEffects( muzzle );
			var dmg = CreateDamage( this.GetAttackDamage() * AggregateMultishotDamage.Total / numShots );
			for ( int i = 0; i < numShots; i++ )
				ShootTarget( targs[i], in dmg, muzzle );
		}
	}
}