FilmCut.cs
namespace NiuLai;

/// <summary>
/// Janky in-engine cutscenes. Theater mud + crowd laugh. No film stills.
/// E skips a shot. Camera slams. Models slide.
/// </summary>
public enum CutId
{
	None,
	OpenEyes,
	LarkBack,
	EatFirst,
	LeopardFace,
	MilkDad,
	TooClose,
	Headbutt,
	NoRoad,
	Ng
}

public static class FilmCut
{
	public static bool Playing { get; private set; }
	public static CutId Id { get; private set; }
	public static int Shot { get; private set; }
	public static int Version { get; private set; }
	public static string CardCn { get; private set; } = "";
	public static string CardEn { get; private set; } = "";

	static float _t;
	static float _hold;
	static int _shots;
	static Action _done;
	static Vector3 _camPos;
	static Rotation _camRot;
	static GameObject _focus;
	static bool _tipped;

	public static void Play( CutId id, Action done = null )
	{
		if ( id == CutId.None )
			return;

		Id = id;
		Playing = true;
		Shot = 0;
		_t = 0;
		_done = done;
		_tipped = false;
		CardCn = "";
		CardEn = "";
		_shots = id switch
		{
			CutId.OpenEyes => 3,
			CutId.LarkBack => 2,
			CutId.EatFirst => 1,
			CutId.LeopardFace => 2,
			CutId.MilkDad => 2,
			CutId.TooClose => 2,
			CutId.Headbutt => 2,
			CutId.NoRoad => 2,
			CutId.Ng => 2,
			_ => 1
		};
		BeginShot();
		Version++;
	}

	public static void Skip()
	{
		if ( !Playing )
			return;
		AdvanceShot();
	}

	public static void Tick( float dt )
	{
		if ( !Playing )
			return;

		_t += dt;
		Animate();
		AimCamera();

		if ( _t >= _hold )
			AdvanceShot();
	}

	static void AdvanceShot()
	{
		Shot++;
		if ( Shot >= _shots )
		{
			End();
			return;
		}

		_t = 0;
		BeginShot();
		Version++;
	}

	static void BeginShot()
	{
		_hold = 3.4f;
		_focus = null;
		CardCn = "";
		CardEn = "";

		switch ( Id )
		{
			case CutId.OpenEyes:
				if ( Shot == 0 )
				{
					CardCn = "第一场 · 睁开眼睛";
					CardEn = "Scene 1 · Open your eyes";
					_hold = 2.2f;
					GameAudio.PlayClip( GameAudio.NameNiu, 0.85f );
				}
				else if ( Shot == 1 )
				{
					TipPlayer( true );
					_focus = CalfPlayer.Instance?.GameObject;
					_hold = 4.2f;
					GameState.Talk( "牛妈妈 / Mama", Lines.MomSleep );
					GameAudio.PlayClip( GameAudio.MamaWake );
					GameAudio.PlayCrowd( GameAudio.CrowdWake );
				}
				else
				{
					TipPlayer( false );
					_focus = CalfPlayer.Instance?.GameObject;
					_hold = 1.6f;
					GameAudio.PlayClip( GameAudio.Stand, 0.9f );
				}
				break;

			case CutId.LarkBack:
				if ( Shot == 0 )
				{
					CardCn = "愿意到我背上来吗";
					CardEn = "Willing come up on my back?";
					_hold = 2.0f;
				}
				LieNamed( "lark", true );
				_focus = Named( "lark" );
				if ( Shot == 1 )
				{
					_hold = 4.6f;
					GameState.Talk( "云玎 / Yun Ding", Lines.Dream );
					GameAudio.PlayClip( GameAudio.LarkRide );
					GameAudio.PlayCrowd( GameAudio.CrowdRide );
				}
				break;

			case CutId.EatFirst:
				CardCn = "吃饱了再去不好吗";
				CardEn = "Eat full then go, not better?";
				_focus = Named( "mom" ) ?? CalfPlayer.Instance?.GameObject;
				_hold = 3.8f;
				GameState.Talk( "牛妈妈 / Mama", Lines.EatFirst );
				GameAudio.PlayClip( GameAudio.SlowDown );
				break;

			case CutId.LeopardFace:
				if ( Shot == 0 )
				{
					CardCn = "豹拉特写 · 院线在笑";
					CardEn = "Bao La close-up · theater laughing";
					_hold = 1.8f;
				}
				_focus = Named( "leopard" );
				if ( Shot == 1 )
				{
					_hold = 5.0f;
					GameState.Talk( "豹拉 / Bao La", Lines.Protect );
					GameAudio.PlayClip( GameAudio.Leopard );
					GameAudio.PlayCrowd( GameAudio.CrowdBig );
				}
				break;

			case CutId.MilkDad:
				if ( Shot == 0 )
				{
					CardCn = "少了点妈妈的味道";
					CardEn = "Missing a little Mama flavor";
					_hold = 1.8f;
					GameAudio.PlayClip( GameAudio.Milk );
				}
				_focus = Named( "leopard" );
				if ( Shot == 1 )
				{
					_hold = 5.2f;
					GameState.Talk( "豹拉 / Bao La", Lines.MilkScene );
					GameAudio.PlayCrowd( GameAudio.CrowdMilk );
				}
				break;

			case CutId.TooClose:
				if ( Shot == 0 )
				{
					CardCn = "第一,他不能离牛群太近";
					CardEn = "First, he cannot leave herd too near";
					_hold = 2.0f;
				}
				_focus = Named( "niuer" ) ?? Named( "dad" );
				if ( Shot == 1 )
				{
					_hold = 4.4f;
					GameState.Talk( "牛二 / Niu Er", Lines.TooClose );
					GameAudio.PlayClip( GameAudio.SlowDown );
					GameAudio.PlayCrowd( GameAudio.CrowdMilk );
				}
				break;

			case CutId.Headbutt:
				CardCn = "我喜欢顶脑门儿";
				CardEn = "I like bump forehead";
				_focus = Named( "leopard" );
				_hold = Shot == 0 ? 1.6f : 4.0f;
				if ( Shot == 1 )
				{
					GameState.Talk( "豹拉 / Bao La", Lines.Headbutt );
					GameAudio.PlayClip( GameAudio.LikeHere );
					GameAudio.PlayCrowd( GameAudio.CrowdBig );
				}
				break;

			case CutId.NoRoad:
				if ( Shot == 0 )
				{
					CardCn = "没路了";
					CardEn = "No road anymore";
					_hold = 2.2f;
					GameAudio.PlayClip( GameAudio.CartGrow );
				}
				_focus = Named( "cart" );
				if ( Shot == 1 )
				{
					_hold = 4.6f;
					GameState.Talk( "旁白 / Narrator", Lines.NoRoad );
					GameAudio.PlayCrowd( GameAudio.CrowdCart );
				}
				break;

			case CutId.Ng:
				if ( Shot == 0 )
				{
					CardCn = "嗯";
					CardEn = "Ng.";
					_hold = 1.6f;
				}
				LieNamed( "lark", false );
				_focus = Named( "lark" );
				if ( Shot == 1 )
				{
					_hold = 4.2f;
					GameState.Talk( "云玎 / Yun Ding", Lines.Farewell );
					GameAudio.PlayClip( GameAudio.LarkNg );
					GameAudio.PlayCrowd( GameAudio.CrowdNg );
				}
				break;
		}
	}

	static void Animate()
	{
		if ( Id == CutId.Headbutt && _focus.IsValid() && CalfPlayer.Instance.IsValid() )
		{
			var a = CalfPlayer.Instance.WorldPosition;
			var b = _focus.WorldPosition;
			var mid = (a + b) * 0.5f;
			var bump = MathF.Sin( _t * 10f ) * 8f;
			CalfPlayer.Instance.WorldPosition = mid + Vector3.Left * (18f + bump);
			_focus.WorldPosition = mid + Vector3.Right * (18f - bump);
		}

		if ( Id == CutId.Ng && _focus.IsValid() )
		{
			_focus.WorldPosition = GameState.DreamPos + new Vector3( 0, 0, 40f + MathF.Sin( _t * 2f ) * 8f );
			_focus.WorldRotation = Rotation.FromYaw( _t * 40f ) * Rotation.FromRoll( 18f );
		}
	}

	static void AimCamera()
	{
		var cam = CalfPlayer.Instance?.Scene?.Camera;
		if ( cam is null || !cam.IsValid() )
			return;

		var target = _focus.IsValid() ? _focus.WorldPosition : (CalfPlayer.Instance?.WorldPosition ?? Vector3.Zero);
		target += Vector3.Up * (Id == CutId.LeopardFace ? 22f : 28f);

		var dist = Id switch
		{
			CutId.LeopardFace => 38f,
			CutId.OpenEyes => 42f,
			CutId.LarkBack => 55f,
			CutId.Ng => 90f,
			CutId.NoRoad => 220f,
			_ => 70f
		};

		var side = Id == CutId.LarkBack ? 10f : 16f;
		var height = Id == CutId.LarkBack ? 12f : 22f;
		if ( Id == CutId.Ng )
			height = 8f;

		_camPos = target - Vector3.Forward * dist + Vector3.Right * side + Vector3.Up * height;
		if ( Id == CutId.NoRoad )
			_camPos = target + new Vector3( -180, -140, 90 );
		if ( Id == CutId.Ng )
			_camPos = target + new Vector3( 0, -70, 10 );

		var look = (target - _camPos);
		if ( look.Length > 1f )
			_camRot = Rotation.LookAt( look, Vector3.Up );

		cam.WorldPosition = _camPos;
		cam.WorldRotation = _camRot;
		if ( GameState.InVoid || Id == CutId.Ng )
			cam.BackgroundColor = Color.Black;
	}

	static void End()
	{
		Playing = false;
		var done = _done;
		var id = Id;
		Id = CutId.None;
		_done = null;
		CardCn = "";
		CardEn = "";
		LieNamed( "lark", false );
		TipPlayer( false );
		Version++;
		done?.Invoke();

		if ( id == CutId.LarkBack )
			GameState.EnterDreamFromCut();
		else if ( id == CutId.Ng )
			GameState.FinishFarewellCut();
	}

	static void TipPlayer( bool down )
	{
		var p = CalfPlayer.Instance;
		if ( !p.IsValid() )
			return;
		_tipped = down;
		p.WorldRotation = down
			? Rotation.FromPitch( 80f )
			: Rotation.FromYaw( p.WorldRotation.Yaw() );
	}

	static void LieNamed( string name, bool lie )
	{
		var go = Named( name );
		if ( !go.IsValid() )
			return;
		go.WorldRotation = lie
			? Rotation.FromRoll( 90f ) * Rotation.FromPitch( 20f )
			: Rotation.Identity;
	}

	static GameObject Named( string name )
	{
		if ( WorldBuilder.Stage.TryGetValue( name, out var go ) && go.IsValid() )
			return go;
		return null;
	}
}