Arena/Gladiator.cs
using System;

public enum Mood { Neutral, Happy, Grin, Upset, Hurt }

public enum PopupKind { Damage, Miss, Gain, Text, Enchant }

public sealed class Gladiator : Component
{
	public const float WalkSpeed = 420f;
	public const float LeapTime = 0.6f;
	public const float LeapHeight = 170f;

	public class Popup
	{
		public string Text;
		public PopupKind Kind;
		public string Colour;
		public RealTimeSince Age;
		public float OffsetX;
	}

	[Sync( SyncFlags.FromHost )] public string DisplayName { get; set; }
	/// <summary>Shown on the VS screen, e.g. "Fearful prisoner".</summary>
	[Sync( SyncFlags.FromHost )] public string Title { get; set; }
	[Sync( SyncFlags.FromHost )] public bool IsAI { get; set; }
	[Sync( SyncFlags.FromHost )] public Guid ControllerId { get; set; }
	[Sync( SyncFlags.FromHost )] public int Level { get; set; } = 1;

	[Sync( SyncFlags.FromHost )] public int Strength { get; set; } = 3;
	[Sync( SyncFlags.FromHost )] public int Attack { get; set; } = 3;
	[Sync( SyncFlags.FromHost )] public int Defence { get; set; } = 3;
	[Sync( SyncFlags.FromHost )] public int Vitality { get; set; } = 3;
	[Sync( SyncFlags.FromHost )] public int Agility { get; set; } = 3;
	[Sync( SyncFlags.FromHost )] public int Stamina { get; set; } = 3;
	[Sync( SyncFlags.FromHost )] public int Charisma { get; set; } = 3;
	[Sync( SyncFlags.FromHost )] public int Magicka { get; set; } = 1;

	[Sync( SyncFlags.FromHost )] public int Health { get; set; }
	[Sync( SyncFlags.FromHost )] public int Energy { get; set; }
	[Sync( SyncFlags.FromHost )] public int Magic { get; set; }
	[Sync( SyncFlags.FromHost )] public int ArmourPoints { get; set; }
	[Sync( SyncFlags.FromHost )] public int MaxArmour { get; set; }
	/// <summary>Duels: armour broken = beaten.</summary>
	[Sync( SyncFlags.FromHost )] public bool Broken { get; set; }
	/// <summary>Turns left frozen solid.</summary>
	[Sync( SyncFlags.FromHost )] public int Frozen { get; set; }
	[Sync( SyncFlags.FromHost )] public int Step { get; set; }
	[Sync( SyncFlags.FromHost )] public float PowerRoll { get; set; } = 0.3f;

	[Sync( SyncFlags.FromHost )] public int Race { get; set; }
	[Sync( SyncFlags.FromHost )] public int Skin { get; set; }
	[Sync( SyncFlags.FromHost )] public int Hair { get; set; }
	[Sync( SyncFlags.FromHost )] public int Beard { get; set; }
	[Sync( SyncFlags.FromHost )] public int HairColour { get; set; }
	[Sync( SyncFlags.FromHost )] public int Cloth { get; set; }
	[Sync( SyncFlags.FromHost )] public int Helm { get; set; } = -1;
	[Sync( SyncFlags.FromHost )] public int Chest { get; set; } = -1;
	[Sync( SyncFlags.FromHost )] public int Shoulders { get; set; } = -1;
	[Sync( SyncFlags.FromHost )] public int Gauntlets { get; set; } = -1;
	[Sync( SyncFlags.FromHost )] public int Thighs { get; set; } = -1;
	[Sync( SyncFlags.FromHost )] public int Greaves { get; set; } = -1;
	[Sync( SyncFlags.FromHost )] public int Boots { get; set; } = -1;
	[Sync( SyncFlags.FromHost )] public int Shield { get; set; } = -1;
	[Sync( SyncFlags.FromHost )] public int WeaponId { get; set; }
	[Sync( SyncFlags.FromHost )] public Enchant Enchant { get; set; }
	[Sync( SyncFlags.FromHost )] public int EnchantTier { get; set; }

	/// <summary>Spells known, one bit per <see cref="Spell"/>.</summary>
	[Sync( SyncFlags.FromHost )] public int SpellBits { get; set; }
	[Sync( SyncFlags.FromHost )] public int HealthPotions { get; set; }
	[Sync( SyncFlags.FromHost )] public int EnergyPotions { get; set; }
	[Sync( SyncFlags.FromHost )] public int MagicPotions { get; set; }

	public int MaxHealth => Catalog.MaxHealth( Vitality );
	public int MaxEnergy => Catalog.MaxEnergy( Stamina );
	public int MaxMagic => Catalog.MaxMagicka( Magicka );
	public bool IsDead => Health <= 0 || Broken;
	public bool IsLocalPlayer => !IsAI && ControllerId == Connection.Local.Id;
	public WeaponDef Weapon => Catalog.Weapons[Math.Clamp( WeaponId, 0, Catalog.Weapons.Length - 1 )];
	public bool Knows( Spell s ) => (SpellBits & (1 << (int)s)) != 0;

	public int Potions( Potion p ) => p switch { Potion.Health => HealthPotions, Potion.Energy => EnergyPotions, _ => MagicPotions };

	public void UsePotion( Potion p )
	{
		switch ( p )
		{
			case Potion.Health: HealthPotions--; break;
			case Potion.Energy: EnergyPotions--; break;
			default: MagicPotions--; break;
		}
	}

	public Look Look => new()
	{
		Race = Race, Skin = Skin, Hair = Hair, Beard = Beard, HairColour = HairColour, Cloth = Cloth,
		Helm = Helm, Chest = Chest, Shoulders = Shoulders, Gauntlets = Gauntlets, Thighs = Thighs, Greaves = Greaves,
		Boots = Boots, Shield = Shield, Weapon = WeaponId
	};

	public void SetLook( Look l )
	{
		Race = l.Race; Skin = l.Skin; Hair = l.Hair; Beard = l.Beard; HairColour = l.HairColour; Cloth = l.Cloth;
		Helm = l.Helm; Chest = l.Chest; Shoulders = l.Shoulders; Gauntlets = l.Gauntlets; Thighs = l.Thighs;
		Greaves = l.Greaves; Boots = l.Boots; Shield = l.Shield; WeaponId = l.Weapon;
		MaxArmour = l.ArmourPoints();
	}

	public void ResetVitals()
	{
		Health = MaxHealth;
		Energy = MaxEnergy;
		Magic = MaxMagic;
		ArmourPoints = MaxArmour;
		Broken = false;
		Frozen = 0;
	}

	public void RollPower() => PowerRoll = Random.Shared.Float( 0.2f, 0.4f );

	// ---- client side presentation ----

	public float DisplayX { get; private set; } = float.NaN;
	public float Lift { get; private set; }
	public bool Walking { get; private set; }
	public List<Popup> Popups { get; } = new();

	string oneShot;
	RealTimeSince oneShotTime;
	RealTimeSince loopTime = 0;
	RealTimeSince deadTime;
	bool wasDead;
	bool leaping;
	float leapFrom;
	RealTimeSince leapTime;

	Mood mood;
	RealTimeUntil moodUntil;

	public Mood CurrentMood => IsDead ? Mood.Hurt : !moodUntil ? mood : (Health < MaxHealth * 0.3f || (MaxArmour > 0 && ArmourPoints < MaxArmour * 0.3f)) ? Mood.Upset : Mood.Neutral;

	public (string Anim, float Time) CurrentAnim
	{
		get
		{
			if ( IsDead ) return ("die", (float)deadTime);
			if ( oneShot is not null && PuppetRig.Anims.TryGetValue( oneShot, out var a ) && oneShotTime < a.Length ) return (oneShot, (float)oneShotTime);
			if ( Walking ) return ("walk", (float)loopTime);

			var fight = Scene.Get<ArenaFight>();
			if ( fight.IsValid() && fight.State == FightState.Over && fight.Winner == this ) return ("win", (float)loopTime);
			return ("idle", (float)loopTime);
		}
	}

	protected override void OnUpdate()
	{
		var fight = Scene.Get<ArenaFight>();
		if ( !fight.IsValid() ) return;

		float target = fight.StepToX( Step );
		if ( float.IsNaN( DisplayX ) ) DisplayX = target;

		if ( IsDead && !wasDead ) deadTime = 0;
		wasDead = IsDead;

		if ( leaping )
		{
			float t = Math.Clamp( leapTime / LeapTime, 0f, 1f );
			DisplayX = leapFrom + (target - leapFrom) * t;
			Lift = MathF.Sin( t * MathF.PI ) * LeapHeight;
			Walking = false;
			if ( t >= 1f ) { leaping = false; Lift = 0; }
		}
		else
		{
			float diff = target - DisplayX;
			float move = Math.Clamp( diff, -WalkSpeed * Time.Delta, WalkSpeed * Time.Delta );
			DisplayX += move;
			Walking = MathF.Abs( diff ) > 2f;
		}

		Popups.RemoveAll( p => p.Age > 1.6f );
	}

	[Rpc.Broadcast( NetFlags.HostOnly )]
	public void PlayAnim( string anim )
	{
		oneShot = anim;
		oneShotTime = 0;
	}

	[Rpc.Broadcast( NetFlags.HostOnly )]
	public void PlayLeap()
	{
		leaping = true;
		leapFrom = DisplayX;
		leapTime = 0;
		oneShot = "leap";
		oneShotTime = 0;
	}

	[Rpc.Broadcast( NetFlags.HostOnly )]
	public void SetMood( Mood newMood, float seconds )
	{
		mood = newMood;
		moodUntil = seconds;
	}

	[Rpc.Broadcast( NetFlags.HostOnly )]
	public void ShowPopup( string text, PopupKind kind, string colour )
	{
		Popups.Add( new Popup { Text = text, Kind = kind, Colour = colour, Age = 0, OffsetX = Popups.Count * 70f } );
	}
}