Game/GameManager.cs
using System;

public enum GameScreen { Title, Create, Intro, Town, Weaponsmith, Armoury, Magic, Church, Stats, Arena, Fitting }

/// <summary>
/// Owns the player's save and which screen is showing.
/// </summary>
public sealed class GameManager : Component
{
	public GameScreen Screen { get; set; } = GameScreen.Title;
	public GameScreen StatsReturn { get; set; } = GameScreen.Town;
	public Profile Player => Profile.Current;
	public string Notice { get; set; }
	/// <summary>Heading on the stats screen after a fight ("You have risen to gladiator level 2.").</summary>
	public string Heading { get; set; }

	public static GameManager Instance { get; private set; }

	protected override void OnAwake()
	{
		Instance = this;
		// the 3D places live next to the game manager
		GameObject.GetOrAddComponent<World3D>();
		Fit.Load();
	}

	public void NewGame()
	{
		Profile.Current = new Profile();
		Screen = GameScreen.Create;
	}

	public void Continue()
	{
		var p = Profile.Load();
		if ( p is null ) return;
		Profile.Current = p;
		Screen = GameScreen.Town;
	}

	public void FinishCreate( string name, Look look )
	{
		var p = Player;
		p.Name = string.IsNullOrWhiteSpace( name ) ? "Gladiator" : name.Trim();
		p.ApplyLook( look );
		p.Save();
		// a new gladiator starts as a slave: short intro, then the town
		Screen = GameScreen.Intro;
	}

	public void FinishIntro()
	{
		Notice = null;
		Screen = GameScreen.Town;
	}

	public void OpenFitting()
	{
		Profile.Current ??= Profile.Load() ?? new Profile();
		Screen = GameScreen.Fitting;
	}

	// ---------------- character creation ----------------

	/// <summary>Take a point back out of a stat while creating the gladiator (never below the starting 1).</summary>
	public void UnspendStat( int stat )
	{
		var p = Player;
		if ( p is null || p.GetStat( stat ) <= Profile.StartStat ) return;
		p.AddStat( stat, -1 );
		p.StatPoints++;
	}

	public void Go( GameScreen s )
	{
		Notice = null;
		if ( s == GameScreen.Stats )
		{
			StatsReturn = Screen == GameScreen.Stats ? GameScreen.Town : Screen;
			Heading = null;
		}
		// the town's arena gate: a slave only gets dungeon duels
		if ( s == GameScreen.Arena )
		{
			EnterFight();
			return;
		}
		Screen = s;
	}

	public void QuitToTitle()
	{
		Player?.Save();
		Scene.Get<ArenaFight>()?.Clear();
		Screen = GameScreen.Title;
	}

	// ---------------- fights ----------------

	public FightMode NextFight => Player is null || Player.Enslaved ? FightMode.Duel : FightMode.Arena;

	public void EnterFight()
	{
		var fight = Scene.Get<ArenaFight>();
		if ( !fight.IsValid() || Player is null ) return;

		Screen = GameScreen.Arena;
		fight.StartFight( Player, NextFight );
	}

	public void LeaveArena()
	{
		var fight = Scene.Get<ArenaFight>();
		if ( !fight.IsValid() ) return;

		bool won = fight.Winner.IsValid() && fight.Winner.IsLocalPlayer;
		bool duel = fight.IsDuel;
		var p = Player;

		// potions drunk are gone
		if ( fight.Left.IsValid() )
		{
			p.Potions[0] = fight.Left.HealthPotions;
			p.Potions[1] = fight.Left.EnergyPotions;
			p.Potions[2] = fight.Left.MagicPotions;
		}

		int gold, points;
		Heading = null;
		if ( won )
		{
			gold = duel ? 25 + p.Level * 10 : 60 + p.Level * 30 + fight.Crowd * p.Level / 4;
			points = 5;
			p.Level++;
			p.Wins++;
			Heading = $"You have risen to gladiator level {p.Level}.";
			if ( duel )
			{
				p.DuelWins++;
				if ( p.DuelWins >= Catalog.DuelsToFreedom && p.Enslaved )
				{
					p.Enslaved = false;
					Heading = "The crowd has heard of you. You've earned your place in the arena!";
				}
			}
			p.Gold += gold;
			Notice = $"Victory! +{gold} gold and {points} skill points.";
		}
		else
		{
			p.Losses++;
			if ( duel )
			{
				// losing a duel costs 5-15% of your gold: the quicker you lost, the more
				float frac = Math.Clamp( 0.15f - (fight.Turns - 4) * (0.10f / 16f), 0.05f, 0.15f );
				gold = (int)MathF.Round( p.Gold * frac );
				p.Gold -= gold;
				points = 1;
				Notice = $"You lost the duel and {gold} gold ({MathF.Round( frac * 100 )}%) to the guards' bets. +{points} skill point.";
			}
			else
			{
				gold = 20 + p.Level * 8;
				points = 2;
				p.Gold += gold;
				Notice = $"Defeat... +{gold} gold for your trouble and {points} skill points.";
			}
		}

		p.StatPoints += points;
		p.Save();
		fight.Clear();

		StatsReturn = GameScreen.Town;
		Screen = GameScreen.Stats;
	}

	// ---------------- stats ----------------

	public void SpendStat( int stat )
	{
		var p = Player;
		if ( p.StatPoints <= 0 ) return;
		p.AddStat( stat, 1 );
		p.StatPoints--;
		p.Save();
	}

	// ---------------- shops ----------------

	public int Price( int basePrice ) => Catalog.Discount( basePrice, Player.Charisma );

	public string BuyWeapon( int id )
	{
		var p = Player;
		var w = Catalog.Weapons[id];

		if ( p.OwnedWeapons.Contains( id ) )
		{
			p.Weapon = id;
			p.Save();
			return $"{w.Name} equipped.";
		}

		if ( p.Strength < w.Strength ) return $"{w.Name}\nNeeds {w.Strength} strength.\nThis item is not available.";
		int price = Price( w.Price );
		if ( p.Gold < price ) return $"The {w.Name} costs {price} gold. Come back when you have it.";

		p.Gold -= price;
		p.OwnedWeapons.Add( id );
		p.Weapon = id;
		p.Save();
		return $"A fine choice! The {w.Name} is yours.";
	}

	public string BuyGear( GearSlot slot, int index )
	{
		var p = Player;
		var g = Catalog.GearItem( slot, index );
		if ( g is null ) return "";

		if ( p.Owns( slot, index ) )
		{
			p.Gear[(int)slot] = p.Gear[(int)slot] == index ? -1 : index;
			p.Save();
			return p.Gear[(int)slot] == index ? $"{g.Name} equipped." : $"{g.Name} taken off.";
		}

		if ( p.Level < g.Level ) return $"{g.Name}\nRequired level {g.Level}\nThis item is not available.";
		int price = Price( g.Price );
		if ( p.Gold < price ) return $"{g.Name} costs {price} gold. You can't afford it.";

		p.Gold -= price;
		p.OwnedGear.Add( $"{(int)slot}:{index}" );
		p.Gear[(int)slot] = index;
		p.Save();
		return $"{g.Name}, a perfect fit!";
	}

	public string BuyEnchant( Enchant type, int tier )
	{
		var p = Player;
		if ( p.Weapon <= 0 ) return "I can't enchant your fists, dear. Buy a weapon first.";

		var current = p.EnchantFor( p.Weapon );
		if ( current is not null && current.Type == type && current.Tier >= tier ) return "Your weapon already holds that magic.";

		int price = Price( Catalog.EnchantPrice( tier ) );
		if ( p.Gold < price ) return $"That costs {price} gold. You don't have enough.";

		p.Gold -= price;
		p.Enchants.RemoveAll( x => x.Weapon == p.Weapon );
		p.Enchants.Add( new WeaponEnchant { Weapon = p.Weapon, Type = type, Tier = tier } );
		p.Save();

		var (min, max) = Catalog.EnchantRange( tier );
		return $"Done! Your {Catalog.Weapons[p.Weapon].Name} now deals +{min}-{max} {type.ToString().ToLower()} damage.";
	}

	public string BuySpell( Spell s )
	{
		var p = Player;
		var info = Catalog.SpellInfo( s );
		if ( p.Spells.Contains( (int)s ) ) return $"You already know {info.Name}.";
		if ( p.Magicka < info.MinMagicka ) return $"{info.Name}\nNeeds {info.MinMagicka} magicka.\nYour mind isn't ready for it yet.";
		int price = Price( info.Price );
		if ( p.Gold < price ) return $"{info.Name} costs {price} gold.";

		p.Gold -= price;
		p.Spells.Add( (int)s );
		p.Save();
		return $"The words of {info.Name} are yours. Use them wisely.";
	}

	public string BuyPotion( Potion potion )
	{
		var p = Player;
		int price = Price( Catalog.PotionPrice( potion ) );
		if ( p.Potions[(int)potion] >= 5 ) return "You can't carry any more of those, my child.";
		if ( p.Gold < price ) return $"A {Catalog.PotionName( potion ).ToLower()} costs {price} gold.";

		p.Gold -= price;
		p.Potions[(int)potion]++;
		p.Save();
		return $"Bless you. A {Catalog.PotionName( potion ).ToLower()} for the fight ahead.";
	}
}