Units/Targetable.cs
using System;

namespace PlanetMeat;

[Group( "Planet Meat - Units" ), Title( "Targetable" ), Icon( "center_focus_strong" )]
public sealed class Targetable : Component, Component.IDamageable, ITargeterStatus
{
	[Property] public string Ident { get; set; } = "";
	[Property] public ITargeterStatus.Teams Team { get; set; } = ITargeterStatus.Teams.Enemy;
	public bool IsPlayerControlled => Team == ITargeterStatus.Teams.Player;

	[Property] public Vector3 BullseyeCenter { get; set; }
	[Property] public float BullseyeRadius { get; set; } = 16.0f;
	public Vector3 BullseyeWorld => WorldTransform.PointToWorld( BullseyeCenter );
	public Vector3 NextBullseye => BullseyeWorld + Game.Random.VectorInSphere( BullseyeRadius );

	[Property] public int BaseMaxHp { get; set; } = 100;
	[Property] public string MaxHpImprovement { get; set; }
	public ValueAggregator<int> AggregateMaxHp => field ??= ValueAggregator<int>.FromImprovement( MaxHpImprovement, BaseMaxHp );
	public int MaxHp => AggregateMaxHp.Total;

	public float BaseResistPerc { get; set; } = 0.0f;
	[Property] public string ResistPercImprovement { get; set; }
	public ValueAggregator<float> AggregateResistPerc => field ??= ValueAggregator<float>.FromImprovement( ResistPercImprovement, BaseResistPerc );
	public float ResistPercentage => AggregateResistPerc.Total;

	public int BaseResistFlat { get; set; } = 0;
	[Property] public string ResistFlatImprovement { get; set; }
	public ValueAggregator<int> AggregateResistFlat => field ??= ValueAggregator<int>.FromImprovement( ResistFlatImprovement, BaseResistFlat );
	public int ResistFlat => AggregateResistFlat.Total;

	public float BaseRegen { get; set; } = 0.0f;
	[Property] public string RegenImprovement { get; set; }
	public ValueAggregator<float> AggregateRegen => field ??= ValueAggregator<float>.FromImprovement( RegenImprovement, BaseRegen );
	public float Regen => AggregateRegen.Total;
	private float RegenProgress { get; set; } = 0.0f;

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

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

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

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

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

	[Property] public GameObject BlameThorns { get; set; }
	public bool HasThorns { get; private set; }
	[Property] public GameObject BlamePhoenix { get; set; }
	public bool HasPhoenix { get; private set; }
	public int PhoenixChargesRemaining => AggregatePhoenixCharges.Total - _timesPhoenixed;

	private int _timesPhoenixed = 0;

	[Property] public string SoundOnHit { get; set; } = "";

	public int CurrentHp
	{
		get
		{
			if ( field > MaxHp || (field < 0 && !Killed) )
				field = MaxHp;
			return field;
		}
		set
		{
			if ( value > MaxHp )
				value = MaxHp;

			field = value;
			Killed = value <= 0;
		}
	} = -1;

	public bool Killed
	{
		get;
		private set
		{
			if ( field != value )
			{
				field = value;

				if ( value )
				{
					foreach ( var listener in GameObject.Components.GetAll<ITargetableListener>() )
						listener.OnKilled();
				}
			}
		}
	} = false;

	protected override void OnStart()
	{
		base.OnStart();
		var hasThornsTech = Research.Current.HasTech( "thorns" );
		HasPhoenix = hasThornsTech && (BlamePhoenix?.IsValid ?? false);
		HasThorns = hasThornsTech && (BlameThorns?.IsValid ?? false);
	}

	protected override void OnUpdate()
	{
		base.OnUpdate();
		if ( CurrentHp < MaxHp && Agenda.Current.Phase == Agenda.Phases.Wave )
		{
			RegenProgress += Regen * MaxHp * Time.Delta;
			var hpRegen = (int)RegenProgress;
			if ( hpRegen > 0 )
			{
				RegenProgress -= hpRegen;
				CurrentHp += hpRegen;
			}
		}
	}

	private static void IncrementScopedStats( string statIdent, string targIdent = "", string blameIdent = "", double amount = 1.0 )
	{
		var stats = Career.Current;
		stats.IncrementStat( statIdent + "_all", amount );

		var hasTarg = targIdent != "";
		if ( hasTarg )
			stats.IncrementStat( statIdent + "_" + targIdent, amount );

		if ( blameIdent != "" )
		{
			stats.IncrementStat( statIdent + "_by_" + blameIdent, amount );

			if ( hasTarg )
				stats.IncrementStat( statIdent + "_" + targIdent + "_by_" + blameIdent, amount );
		}
	}
	private void IncrementScopedEnemyStats( string statIdent, string blameIdent, double amount = 1.0 ) => IncrementScopedStats( statIdent, Ident, blameIdent, amount );

	public void OnDamage( in DamageInfo damage )
	{
		if ( damage.IsTargetValid( this ) )
		{
			var rawDmg = damage.Damage;
			if ( rawDmg > 0.0f )
			{
				var finalDmg = Math.Max( 1, (int)Math.Ceiling( (1.0f - ResistPercentage) * rawDmg - ResistFlat ) );
				if ( finalDmg >= CurrentHp && !damage.IsRetaliation() && _timesPhoenixed < AggregatePhoenixCharges.Total && HasPhoenix )
				{
					_timesPhoenixed++;
					foreach ( var effect in Components.GetAll<EffectOnKilled>( FindMode.EnabledInSelfAndDescendants ) )
						effect.PerformEffects();

					CurrentHp = (int)Math.Ceiling( MaxHp * AggregatePhoenixRegen.Total );
					var phoenixDmg = new DamageInfo( AggregatePhoenixDmg.Total, GameObject, BlamePhoenix );
					phoenixDmg.SetRetaliation( true );
					phoenixDmg.Explode( this, AggregatePhoenixRange.Total );
				}
				else
				{
					var overkill = finalDmg > CurrentHp ? finalDmg - CurrentHp : 0;
					CurrentHp -= finalDmg;

					if ( SoundOnHit != "" )
						Sound.Play( SoundOnHit );

					if ( HasThorns && !damage.IsRetaliation() && damage.FindAttackingTargetable() is Targetable attacker && !this.IsFriendlyTo( attacker ) )
					{
						var thornsDmg = new DamageInfo( AggregateThornsReflect.Total * finalDmg, GameObject, BlameThorns );
						thornsDmg.SetRetaliation( true );
						attacker.OnDamage( in thornsDmg );
					}

					var blameIdent = damage.FindBlameIdent();
					if ( blameIdent == "" )
						Log.Warning( $"Missing blame ident for damage from {damage.Attacker} using {damage.Weapon}" );

					if ( Team == ITargeterStatus.Teams.Enemy )
					{
						IncrementScopedEnemyStats( "damage", blameIdent, finalDmg );
						IncrementScopedEnemyStats( "overkill", blameIdent, overkill );

						foreach ( var l in Lifesteal.All )
							l.StealDamage( this, finalDmg - overkill );

						if ( Killed )
						{
							foreach ( var l in Lifesteal.All )
								l.KillHeal( this );

							IncrementScopedEnemyStats( "kills", blameIdent );
							if ( blameIdent != "" && Catalogue.Current.Categories.ContainsKey( blameIdent ) )
								Career.Current.IncrementStat( "wep_kills" );
						}
					}
					else
					{
						IncrementScopedStats( "hurt", "", blameIdent, finalDmg );
						if ( Killed && blameIdent != "" )
							Career.Current.IncrementStat( "defeated_by_" + blameIdent );
					}
				}
			}
		}
	}

	public void Instakill( string blameIdent = "" )
	{
		if ( !Killed )
		{
			CurrentHp = 0;

			if ( blameIdent != "" )
			{
				if ( Team == ITargeterStatus.Teams.Enemy )
					IncrementScopedEnemyStats( "kills", blameIdent );
				else
					Career.Current.IncrementStat( "defeated_by_" + blameIdent );
			}
		}
	}
	public void Instakill( DamageInfo dmg ) => Instakill( dmg.FindBlameIdent() );

	protected override void DrawGizmos()
	{
		base.DrawGizmos();
		Gizmo.Draw.Color = Color.Yellow;
		Gizmo.Draw.LineSphere( BullseyeCenter, BullseyeRadius );
	}
}