Game/Profile.cs
using System;

/// <summary>
/// Everything the puppet needs to draw a gladiator. Gear values are item indexes (-1 = nothing worn).
/// </summary>
public struct Look
{
	public int Race;
	public int Skin;
	public int Hair;        // 0 = bald, 1.. = S&S2 hairstyles
	public int Beard;       // 0 = clean shaven
	public int HairColour;
	public int Cloth;
	public int Helm, Chest, Shoulders, Gauntlets, Thighs, Greaves, Boots, Shield;
	public int Weapon;      // 0 = fists

	public static Look Default => new()
	{
		Helm = -1, Chest = -1, Shoulders = -1, Gauntlets = -1, Thighs = -1, Greaves = -1, Boots = -1, Shield = -1,
		HairColour = 1
	};

	public string SkinHex
	{
		get
		{
			var skins = Catalog.Races[Math.Clamp( Race, 0, Catalog.Races.Length - 1 )].Skins;
			return skins[Math.Clamp( Skin, 0, skins.Length - 1 )];
		}
	}
	public string HairHex => Catalog.HairColours[Math.Clamp( HairColour, 0, Catalog.HairColours.Length - 1 )];
	public string ClothHex => Catalog.ClothColours[Math.Clamp( Cloth, 0, Catalog.ClothColours.Length - 1 )];
	public string RaceName => Catalog.Races[Math.Clamp( Race, 0, Catalog.Races.Length - 1 )].Name;

	public int Get( GearSlot s ) => s switch
	{
		GearSlot.Helm => Helm,
		GearSlot.Chest => Chest,
		GearSlot.Shoulders => Shoulders,
		GearSlot.Gauntlets => Gauntlets,
		GearSlot.Thighs => Thighs,
		GearSlot.Greaves => Greaves,
		GearSlot.Boots => Boots,
		_ => Shield
	};

	public void Set( GearSlot s, int item )
	{
		switch ( s )
		{
			case GearSlot.Helm: Helm = item; break;
			case GearSlot.Chest: Chest = item; break;
			case GearSlot.Shoulders: Shoulders = item; break;
			case GearSlot.Gauntlets: Gauntlets = item; break;
			case GearSlot.Thighs: Thighs = item; break;
			case GearSlot.Greaves: Greaves = item; break;
			case GearSlot.Boots: Boots = item; break;
			default: Shield = item; break;
		}
	}

	/// <summary>Armour from everything worn.</summary>
	public int ArmourPoints()
	{
		int total = 0;
		foreach ( var s in Catalog.Slots )
		{
			var g = Catalog.GearItem( s, Get( s ) );
			if ( g is not null ) total += g.Points;
		}
		return total;
	}
}

public class WeaponEnchant
{
	public int Weapon { get; set; }
	public Enchant Type { get; set; }
	public int Tier { get; set; }
}

public class Profile
{
	public const string SaveFile = "gladiator.json";
	public const int CurrentVersion = 2;

	public int SaveVersion { get; set; } = CurrentVersion;
	public string Name { get; set; } = "Gladiator";
	public int Level { get; set; } = 1;
	public int Gold { get; set; } = 60;

	/// <summary>Every stat starts at this, with <see cref="StartPoints"/> to spend on top (like the Flash game).</summary>
	public const int StartStat = 1;
	public const int StartPoints = 9;
	public const int StatCount = 8;

	public int StatPoints { get; set; } = StartPoints;
	/// <summary>Everyone starts as a slave: dungeon duels until <see cref="Catalog.DuelsToFreedom"/> are won.</summary>
	public bool Enslaved { get; set; } = true;
	public int DuelWins { get; set; }
	public int Wins { get; set; }
	public int Losses { get; set; }

	public int Strength { get; set; } = StartStat;
	public int Attack { get; set; } = StartStat;
	public int Defence { get; set; } = StartStat;
	public int Vitality { get; set; } = StartStat;
	public int Agility { get; set; } = StartStat;
	public int Stamina { get; set; } = StartStat;
	public int Charisma { get; set; } = StartStat;
	public int Magicka { get; set; } = StartStat;

	public int Race { get; set; }
	public int Skin { get; set; }
	public int Hair { get; set; }
	public int Beard { get; set; }
	public int HairColour { get; set; } = 1;
	public int Cloth { get; set; }

	public int Weapon { get; set; } = 1;
	public List<int> OwnedWeapons { get; set; } = new() { 0, 1 };
	/// <summary>Worn item per slot (index into the slot's list), -1 = nothing.</summary>
	public int[] Gear { get; set; } = NewGear();
	/// <summary>"slot:index" of everything bought.</summary>
	public List<string> OwnedGear { get; set; } = new() { "1:0" };
	public List<WeaponEnchant> Enchants { get; set; } = new();
	public List<int> Spells { get; set; } = new();
	public int[] Potions { get; set; } = new int[3];

	public static Profile Current { get; set; }

	/// <summary>A slave starts in rags (the first chest piece) with a wooden club.</summary>
	static int[] NewGear()
	{
		var g = new int[Catalog.Slots.Length];
		for ( int i = 0; i < g.Length; i++ ) g[i] = -1;
		g[(int)GearSlot.Chest] = 0;
		return g;
	}

	public Look Look
	{
		get
		{
			var l = new Look { Race = Race, Skin = Skin, Hair = Hair, Beard = Beard, HairColour = HairColour, Cloth = Cloth, Weapon = Weapon };
			foreach ( var s in Catalog.Slots ) l.Set( s, Gear[(int)s] );
			return l;
		}
	}

	public void ApplyLook( Look l )
	{
		Race = l.Race; Skin = l.Skin; Hair = l.Hair; Beard = l.Beard; HairColour = l.HairColour; Cloth = l.Cloth;
	}

	public int GetStat( int i ) => i switch
	{
		0 => Strength, 1 => Attack, 2 => Defence, 3 => Vitality, 4 => Agility, 5 => Stamina, 6 => Charisma, _ => Magicka
	};

	public void AddStat( int i, int amount )
	{
		switch ( i )
		{
			case 0: Strength += amount; break;
			case 1: Attack += amount; break;
			case 2: Defence += amount; break;
			case 3: Vitality += amount; break;
			case 4: Agility += amount; break;
			case 5: Stamina += amount; break;
			case 6: Charisma += amount; break;
			default: Magicka += amount; break;
		}
	}

	public static readonly string[] StatNames = { "Strength", "Attack", "Defence", "Vitality", "Agility", "Stamina", "Charisma", "Magicka" };

	/// <summary>The order the Flash game lists them in.</summary>
	public static readonly int[] StatOrder = { 0, 4, 1, 2, 3, 6, 5, 7 };

	public static readonly string[] StatHelp =
	{
		"Strength: +2 damage per point. Unlocks heavier weapons.",
		"Attack: better chance to hit.",
		"Defence: reduces the damage you take.",
		"Vitality: +10 max health per point.",
		"Agility: move further, dodge more, act first.",
		"Stamina: +10 max energy per point. Resting recovers more.",
		"Charisma: stronger taunts, happier crowds, cheaper shops.",
		"Magicka: +10 magic per point, stronger spells, and lets you learn better ones."
	};

	public WeaponEnchant EnchantFor( int weapon ) => Enchants.FirstOrDefault( x => x.Weapon == weapon );

	public bool Owns( GearSlot slot, int index ) => OwnedGear.Contains( $"{(int)slot}:{index}" );

	public int ArmourPoints() => Look.ArmourPoints();

	public static bool HasSave => FileSystem.Data.FileExists( SaveFile );

	public void Save() => FileSystem.Data.WriteJson( SaveFile, this );

	public static Profile Load()
	{
		try
		{
			var p = FileSystem.Data.ReadJson<Profile>( SaveFile );
			if ( p is null ) return null;

			// saves from before the S&S2 art: keep the career, reset looks and gear
			if ( p.SaveVersion < CurrentVersion )
			{
				p.Hair = 0; p.Beard = 0; p.Race = 0; p.Skin = 0; p.HairColour = 1; p.Cloth = 0;
				p.Gear = NewGear();
				p.OwnedGear = new() { "1:0" };
				p.Weapon = 1;
				p.OwnedWeapons = new() { 0, 1 };
				p.Enchants = new();
				if ( p.Magicka < StartStat ) p.Magicka = StartStat;
				p.SaveVersion = CurrentVersion;
			}

			if ( p.Gear is null || p.Gear.Length != Catalog.Slots.Length ) p.Gear = NewGear();
			p.OwnedWeapons ??= new() { 0, 1 };
			p.OwnedGear ??= new();
			p.Enchants ??= new();
			p.Spells ??= new();
			if ( p.Potions is null || p.Potions.Length != 3 ) p.Potions = new int[3];
			p.Weapon = Math.Clamp( p.Weapon, 0, Catalog.Weapons.Length - 1 );
			p.Hair = Math.Clamp( p.Hair, 0, Catalog.HairCount );
			p.Beard = Math.Clamp( p.Beard, 0, Catalog.BeardCount );
			return p;
		}
		catch ( Exception e )
		{
			Log.Warning( $"Couldn't load save: {e.Message}" );
			return null;
		}
	}
}