World/World3D.cs
using System;

/// <summary>
/// Shows the 3D place for the current screen (see <see cref="StageSet"/> in scenes/arena.scene), puts the
/// camera on that place's camera spot, and stands the gladiators in it as lit paper puppets.
/// </summary>
public sealed class World3D : Component
{
	public static World3D Instance { get; private set; }

	/// <summary>World units per rig pixel: a gladiator is about 70 units tall.</summary>
	public const float U = 0.16f;

	/// <summary>Sprites are seen from the front only when turned round; flip this if all the art shows up mirrored.</summary>
	public static bool TurnSprites = true;

	[Property, Title( "Turn sprites around (untick if the art looks mirrored)" )]
	public bool TurnSpritesAround { get => TurnSprites; set => TurnSprites = value; }

	/// <summary>Fights: how close the camera gets to the two fighters (world units).</summary>
	[Property] public float FightZoomNear { get; set; } = 210f;
	[Property] public float FightZoomFar { get; set; } = 430f;

	// what a screen can override on the gladiator it shows (character creation, fitting room, intro)
	public Look? HeroLook { get; set; }
	public string HeroAnim { get; set; }
	public Mood HeroMood { get; set; } = Mood.Neutral;

	public StageSet CurrentSet { get; private set; }
	public SpritePuppet Hero { get; private set; }
	public SpritePuppet LeftFighter { get; private set; }
	public SpritePuppet RightFighter { get; private set; }

	string activeKey;
	GameScreen lastScreen = (GameScreen)(-1);
	GameObject puppets;
	readonly List<Cutout> chains = new();
	float follow, zoom;

	protected override void OnAwake() => Instance = this;

	static GameManager Gm => GameManager.Instance;

	/// <summary>Which place a screen uses (fights: "Duel" in the dungeon, "Arena" in the arena).</summary>
	public string PlaceKey
	{
		get
		{
			if ( !Gm.IsValid() ) return "Title";
			if ( Gm.Screen == GameScreen.Arena )
			{
				var f = Scene.Get<ArenaFight>();
				return f.IsValid() && f.Mode == FightMode.Duel ? "Duel" : "Arena";
			}
			return Gm.Screen.ToString();
		}
	}

	protected override void OnStart()
	{
		puppets = new GameObject( true, "Puppets" );
		puppets.Flags |= GameObjectFlags.NotSaved;
		Hero = NewPuppet( "Hero" );
		LeftFighter = NewPuppet( "Left fighter" );
		RightFighter = NewPuppet( "Right fighter" );
		RightFighter.Flip = true;
		Hero.LookSource = () => HeroLook ?? Profile.Current?.Look ?? DefaultHero;
	}

	SpritePuppet NewPuppet( string name ) => new GameObject( puppets, true, name ).AddComponent<SpritePuppet>();

	/// <summary>The gladiator on the title screen.</summary>
	public static Look DefaultHero
	{
		get
		{
			var l = Look.Default;
			l.Hair = 2; l.Beard = 1; l.HairColour = 2;
			l.Chest = 8; l.Shoulders = 6; l.Gauntlets = 6; l.Greaves = 6; l.Boots = 6; l.Shield = 6;
			l.Weapon = 22;
			return l;
		}
	}

	protected override void OnUpdate()
	{
		if ( !Gm.IsValid() ) return;

		// a screen's own look/animation for the gladiator only lasts while that screen is up
		if ( Gm.Screen != lastScreen )
		{
			lastScreen = Gm.Screen;
			HeroLook = null;
			HeroAnim = null;
			HeroMood = Mood.Neutral;
		}

		string key = PlaceKey;
		if ( key != activeKey )
		{
			activeKey = key;
			var sets = Scene.GetAllComponents<StageSet>().ToList();
			var set = sets.FirstOrDefault( s => s.Shows( key ) );
			foreach ( var s in sets ) s.Show( s == set );
			CurrentSet = set;
			zoom = 0;
			if ( !set.IsValid() ) Log.Warning( $"No StageSet in the scene shows \"{key}\"" );
		}

		var cam = Scene.Camera;
		if ( !CurrentSet.IsValid() || !cam.IsValid() )
		{
			HidePuppets();
			return;
		}

		// ---- camera on the place's spot for this screen ----
		var screen = Gm.Screen;
		var fight = Scene.Get<ArenaFight>();
		bool versus = screen == GameScreen.Arena && fight.IsValid() && fight.State == FightState.Intro;
		var camSpot = (versus ? CurrentSet.Spot( "Camera:Versus" ) : null) ?? CurrentSet.Spot( $"Camera:{screen}" ) ?? CurrentSet.Spot( "Camera" );
		if ( !camSpot.IsValid() ) return;

		var rot = camSpot.WorldRotation;
		var right = rot.Right.WithZ( 0 ).Normal;
		var toCam = (-rot.Forward).WithZ( 0 ).Normal;
		var camPos = camSpot.WorldPosition;
		cam.FieldOfView = camSpot.FieldOfView;
		cam.BackgroundColor = CurrentSet.Background;

		// ---- the gladiators ----
		bool fighting = screen == GameScreen.Arena && fight.IsValid() && fight.Left.IsValid() && fight.Right.IsValid();
		if ( fighting )
		{
			var spot = CurrentSet.Spot( "Fight" );
			var centre = spot.IsValid() ? spot.WorldPosition : Vector3.Zero;
			Place( LeftFighter, fight.Left, centre, right, toCam );
			Place( RightFighter, fight.Right, centre, right, toCam );
			Hero.Enabled = false;

			// follow the fight: slide sideways to the middle, move in as they close up
			float a = Lateral( fight.Left.DisplayX ), b = Lateral( fight.Right.DisplayX );
			if ( !float.IsNaN( a ) && !float.IsNaN( b ) )
			{
				float baseDist = Vector3.Dot( centre - camPos, rot.Forward );
				float want = versus ? FightZoomNear * 0.75f : Math.Clamp( 150f + MathF.Abs( b - a ) * 0.7f, FightZoomNear, FightZoomFar );
				float k = Math.Clamp( Time.Delta * 3f, 0f, 1f );
				if ( zoom <= 0 ) { zoom = want; follow = (a + b) * 0.5f; }
				zoom += (want - zoom) * k;
				follow += ((a + b) * 0.5f - follow) * k;
				// the place's camera spot is the "far" view; only move closer, never further back.
				// The camera stays level; it comes down as it moves in so the feet stay at the same height on screen.
				float forward = MathF.Max( 0, baseDist - zoom );
				float height = camPos.z - centre.z;
				camPos += right * follow + rot.Forward * forward;
				if ( baseDist > 1f ) camPos = camPos.WithZ( centre.z + height * (baseDist - forward) / baseDist );
			}
		}
		else
		{
			LeftFighter.Enabled = RightFighter.Enabled = false;
			LeftFighter.Fighter = RightFighter.Fighter = null;

			string role = screen switch
			{
				GameScreen.Title => "Hero",
				GameScreen.Intro => "Chained",
				GameScreen.Fitting => "Fitting",
				_ => "Player"
			};
			var spot = CurrentSet.Spot( role ) ?? (role == "Fitting" ? CurrentSet.Spot( "Player" ) : null);
			if ( spot.IsValid() && screen is not (GameScreen.Weaponsmith or GameScreen.Armoury or GameScreen.Magic or GameScreen.Church) )
			{
				Hero.Enabled = true;
				Hero.Feet = spot.WorldPosition;
				Hero.ScreenRight = right;
				Hero.ToCamera = toCam;
				Hero.Size = spot.Size;
				Hero.Anim = HeroAnim ?? (screen == GameScreen.Title ? "win" : screen == GameScreen.Intro ? "chained" : "idle");
				Hero.FixedMood = HeroMood;
			}
			else Hero.Enabled = false;
		}

		cam.WorldPosition = camPos;
		cam.WorldRotation = rot;

		UpdateChains( screen == GameScreen.Intro && Hero.Enabled, toCam );
	}

	void Place( SpritePuppet p, Gladiator g, Vector3 centre, Vector3 right, Vector3 toCam )
	{
		p.Enabled = true;
		p.Fighter = g;
		p.Size = 1f;
		p.ScreenRight = right;
		p.ToCamera = toCam;
		if ( !float.IsNaN( g.DisplayX ) )
			p.Feet = centre + right * Lateral( g.DisplayX ) + Vector3.Up * Lateral( g.Lift );
	}

	void HidePuppets()
	{
		if ( Hero.IsValid() ) Hero.Enabled = false;
		if ( LeftFighter.IsValid() ) LeftFighter.Enabled = false;
		if ( RightFighter.IsValid() ) RightFighter.Enabled = false;
		UpdateChains( false, Vector3.Forward );
	}

	/// <summary>Fight positions (stage pixels) to world units.</summary>
	public static float Lateral( float stagePx ) => stagePx / ArenaView.BaseScale * U;

	/// <summary>A fighter position (stage pixels, as the fight uses them) in the world.</summary>
	public Vector3 FighterWorld( float displayX, float lift )
	{
		var spot = CurrentSet.IsValid() ? CurrentSet.Spot( "Fight" ) : null;
		var centre = spot.IsValid() ? spot.WorldPosition : Vector3.Zero;
		var cam = Scene.Camera;
		var right = cam.IsValid() ? cam.WorldRotation.Right.WithZ( 0 ).Normal : Vector3.Right;
		return centre + right * Lateral( displayX ) + Vector3.Up * Lateral( lift );
	}

	/// <summary>A world point on the 1920x1080 UI stage.</summary>
	public Vector2 ToStage( Vector3 world )
	{
		var cam = Scene.Camera;
		if ( !cam.IsValid() || Screen.Width <= 0 ) return new Vector2( 960, 540 );
		var px = cam.PointToScreenPixels( world );
		float s = MathF.Min( Screen.Width / 1920f, Screen.Height / 1080f );
		var offset = new Vector2( (Screen.Width - 1920f * s) / 2f, (Screen.Height - 1080f * s) / 2f );
		return (px - offset) / s;
	}

	/// <summary>The intro: chains from the ceiling down to the shackles on the gladiator's wrists.</summary>
	void UpdateChains( bool on, Vector3 toCam )
	{
		if ( !on )
		{
			foreach ( var c in chains ) c.GameObject.Enabled = false;
			return;
		}

		int n = 0;
		foreach ( var hand in new[] { "farm_b", "farm_f" } )
		{
			// the wrist: down the forearm from the elbow joint
			var wrist = Hero.BoneWorld( hand == "farm_b" ? "shield" : "weapon" );
			var shackle = Chain( n++, "ss/p/shackle.png", 9f );
			shackle.GameObject.WorldPosition = wrist - Vector3.Up * 3f + toCam * 4f;
			for ( float z = wrist.z + 4f; z < wrist.z + 220f; z += 9f )
			{
				var link = Chain( n++, "ss/p/chain.png", 6f );
				link.GameObject.WorldPosition = new Vector3( wrist.x, wrist.y, z ) + toCam * 3.5f;
			}
		}
		for ( int i = n; i < chains.Count; i++ ) chains[i].GameObject.Enabled = false;
	}

	Cutout Chain( int index, string image, float width )
	{
		while ( chains.Count <= index )
		{
			var go = new GameObject( puppets, true, "chain" );
			go.Flags |= GameObjectFlags.NotSaved;
			var c = go.AddComponent<Cutout>();
			c.CastShadows = true;
			chains.Add( c );
		}
		var cut = chains[index];
		cut.GameObject.Enabled = true;
		cut.Image = image;
		cut.Width = width;
		cut.StandOnBottom = false;
		// the cutout turns its own picture round if needed; the object just faces the camera
		var cam = Scene.Camera;
		var toCam = cam.IsValid() ? (-cam.WorldRotation.Forward).WithZ( 0 ).Normal : Vector3.Forward;
		cut.GameObject.WorldRotation = Rotation.LookAt( toCam, Vector3.Up );
		return cut;
	}
}