CalfPlayer.cs
namespace NiuLai;

public sealed class CalfPlayer : Component
{
	public static CalfPlayer Instance { get; set; }

	[Property] public float WalkSpeed { get; set; } = 220f;
	[Property] public float RunSpeed { get; set; } = 340f;
	[Property] public float JumpSpeed { get; set; } = 340f;

	float _yaw = 180f;
	float _pitch = 18f;
	Vector3 _vel;
	bool _grounded;
	float _bob;
	float _jumpDelay = -1f;
	float _jankTimer;
	float _idle;
	float _nagCool;
	GameObject _mesh;

	protected override void OnAwake()
	{
		Instance = this;
	}

	protected override void OnDestroy()
	{
		if ( Instance == this )
			Instance = null;
	}

	protected override void OnStart()
	{
		_mesh = CowMesh.Build( GameObject, CowMesh.Calf, 1f );
	}

	protected override void OnUpdate()
	{
		if ( Input.EscapePressed )
		{
			Input.EscapePressed = false;
			if ( GameState.Talking )
				GameState.ClearTalk();
			else
				GameFlow.TogglePause();
		}

		if ( GameFlow.BlocksWorld )
			return;

		var look = Input.AnalogLook;
		_yaw += look.yaw;
		_pitch = MathX.Clamp( _pitch + look.pitch, -8f, 42f );

		if ( FilmCut.Playing )
		{
			if ( PressedUse() )
			{
				if ( GameState.Talking )
					GameState.AdvanceTalk();
				FilmCut.Skip();
			}
			return;
		}

		if ( GameState.Talking )
		{
			if ( PressedUse() )
				GameState.AdvanceTalk();
			return;
		}

		if ( PressedUse() )
			TryUse();

		if ( Input.Pressed( "Moo" ) || Input.Pressed( "moo" ) )
			Moo();
	}

	protected override void OnFixedUpdate()
	{
		if ( GameFlow.BlocksWorld || GameState.Talking )
		{
			_vel = _vel.WithX( 0 ).WithY( 0 );
			ApplyGravity( Time.Delta );
			Slide();
			return;
		}

		var dt = Time.Delta;
		var analog = Input.AnalogMove.WithZ( 0 );
		var cam = Rotation.FromYaw( _yaw );
		var wish = cam * analog;
		if ( wish.Length > 1f )
			wish = wish.Normal;

		var speed = Input.Down( "Run" ) ? RunSpeed : WalkSpeed;
		speed *= GameState.WalkMul;
		if ( GameState.InVoid )
			speed *= 0.55f;

		_vel.x = wish.x * speed;
		_vel.y = wish.y * speed;

		if ( wish.Length > 0.1f )
		{
			var face = Rotation.LookAt( wish.WithZ( 0 ), Vector3.Up );
			WorldRotation = Rotation.Slerp( WorldRotation, face, 1f - MathF.Pow( 0.001f, dt ) );
			_bob += dt * 9f;
			_idle = 0;
			if ( Input.Down( "Run" ) && _nagCool <= 0f && GameState.Beat == Beat.Migrate )
			{
				_nagCool = 10f;
				Chatter.Say( "牛妈妈 / Mama", Chatter.Pick( Lines.MomSlow ) );
				GameAudio.PlayClip( GameAudio.SlowDown, 0.7f );
			}
		}
		else
		{
			_bob = 0;
			_idle += dt;
			if ( _idle > 7.5f && _nagCool <= 0f )
			{
				_idle = 0;
				_nagCool = 12f;
				Chatter.Say( "牛妈妈 / Mama", Chatter.Pick( Lines.MomSleep ) );
				GameAudio.PlayClip( GameAudio.MamaWake, 0.75f );
			}
		}
		_nagCool -= dt;

		if ( _jumpDelay >= 0f )
		{
			_jumpDelay -= dt;
			if ( _jumpDelay <= 0f && _grounded )
			{
				_vel.z = JumpSpeed;
				_grounded = false;
				_jumpDelay = -1f;
			}
		}
		else if ( _grounded && Input.Pressed( "Jump" ) )
		{
			// Movie jank: the calf thinks about jumping, then hops too hard.
			_jumpDelay = 0.16f;
			Trophies.Unlock( "think_jump" );
		}

		ApplyGravity( dt );
		Slide();
		UpdateMesh();
		UpdateCamera( dt );
		WatchBeats();
	}

	void WatchBeats()
	{
		if ( GameState.Beat == Beat.Migrate && GameState.GrassEaten < 1 && WorldPosition.x > 240f )
		{
			WorldPosition = WorldPosition.WithX( 230f );
			if ( !GameState.DidEatNag )
			{
				GameState.MarkEatNag();
				FilmCut.Play( CutId.EatFirst );
			}
			else
			{
				Chatter.Say( "牛妈妈 / Mama", Chatter.Pick( Lines.EatFirst ) );
			}
		}

		if ( GameState.Beat == Beat.Creek && !GameState.CreekJumped && WorldPosition.x > 500f )
			GameState.JumpCreek();

		if ( GameState.Beat == Beat.Wake && WorldPosition.y > 280f )
			Trophies.Unlock( "fly_line" );

		if ( !GameState.SawTail && GameState.Beat >= Beat.Night && GameState.Beat < Beat.Farewell )
		{
			if ( WorldBuilder.Stage.TryGetValue( "leopard", out var leo ) && leo.IsValid() )
			{
				if ( (WorldPosition - leo.WorldPosition).Length < 85f )
				{
					GameState.MarkTail();
					Chatter.Say( "牛来 / Niu Lai", Chatter.Pick( Lines.Tail ), 3.3f );
					GameAudio.PlayClip( GameAudio.Snake, 0.85f );
				}
			}
		}

		if ( _nagCool <= 0f && !GameState.Talking )
		{
			if ( GameState.Beat == Beat.Snake && WorldPosition.x > 220f && WorldPosition.x < 300f )
			{
				_nagCool = 16f;
				Chatter.Say( "旁白 / Narrator", Lines.Tease[0] );
			}
			else if ( GameState.Beat == Beat.Night && WorldPosition.x > 640f )
			{
				_nagCool = 16f;
				Chatter.Say( "旁白 / Narrator", Lines.Tease[1] );
			}
			else if ( GameState.Beat == Beat.Wake && WorldPosition.y > 120f )
			{
				_nagCool = 16f;
				Chatter.Say( "云玎 / Yun Ding", Lines.Tease[2] );
			}
			else if ( GameState.Beat == Beat.Cart && WorldPosition.x > 700f )
			{
				_nagCool = 16f;
				Chatter.Say( "旁白 / Narrator", Lines.Tease[3] );
			}
		}
	}

	void ApplyGravity( float dt )
	{
		if ( !_grounded )
			_vel.z -= 900f * dt;
	}

	void Slide()
	{
		var dt = Time.Delta;
		if ( GameState.WantHop )
		{
			GameState.WantHop = false;
			_vel.z = 420f;
			_grounded = false;
		}

		var next = WorldPosition + _vel * dt;
		if ( !GameState.CreekJumped && next.x > 400f && next.x < 480f && GameState.Beat is Beat.Creek or Beat.Snake )
		{
			next.x = 400f;
			_vel.x = 0;
			if ( _nagCool <= 0f )
			{
				_nagCool = 5f;
				Chatter.Say( "牛来 / Niu Lai", Chatter.Pick( Lines.CreekFear ) );
			}
		}

		var wall = Scene.Trace.Ray( WorldPosition + Vector3.Up * 20f, next + Vector3.Up * 20f )
			.IgnoreGameObjectHierarchy( GameObject )
			.Radius( 14f )
			.Run();
		if ( wall.Hit )
			next = WorldPosition + (next - WorldPosition) * MathF.Max( wall.Fraction - 0.05f, 0f );

		var downFrom = next + Vector3.Up * 48f;
		var ground = Scene.Trace.Ray( downFrom, next + Vector3.Down * 80f )
			.IgnoreGameObjectHierarchy( GameObject )
			.Run();

		if ( ground.Hit )
		{
			next = ground.EndPosition;
			_grounded = _vel.z <= 20f;
			if ( _vel.z < 0f )
				_vel.z = 0f;
		}
		else
		{
			_grounded = false;
		}

		WorldPosition = next;
	}

	void UpdateMesh()
	{
		if ( !_mesh.IsValid() )
			return;

		var g = GameState.Growth;
		_mesh.LocalScale = g;
		var stiff = MathF.Sin( _bob ) * 8f;
		_mesh.LocalRotation = new Angles( stiff, 0, MathF.Sin( _bob * 0.5f ) * 3f );
		_mesh.LocalPosition = Vector3.Up * MathF.Abs( MathF.Sin( _bob ) ) * 3f;
	}

	void UpdateCamera( float dt )
	{
		if ( !Scene.Camera.IsValid() )
			return;

		_jankTimer -= dt;
		var hitch = _jankTimer > 0f ? 8f : 0f;
		if ( Random.Shared.Float() < 0.004f )
			_jankTimer = 0.08f;

		var rot = Rotation.From( _pitch, _yaw, hitch );
		var head = WorldPosition + Vector3.Up * (48f * GameState.Growth + 16f);
		var desired = head - rot.Forward * (170f + GameState.Growth * 20f) + rot.Right * 12f;

		var camHit = Scene.Trace.Ray( head, desired )
			.IgnoreGameObjectHierarchy( GameObject )
			.Run();
		if ( camHit.Hit )
			desired = camHit.EndPosition + camHit.Normal * 6f;

		Scene.Camera.WorldPosition = desired;
		Scene.Camera.WorldRotation = rot;
	}

	void TryUse()
	{
		Hotspot best = null;
		var bestD = float.MaxValue;
		foreach ( var hs in Scene.GetAllComponents<Hotspot>() )
		{
			if ( !hs.IsValid() || !hs.InRange( WorldPosition ) )
				continue;
			var d = (hs.WorldPosition - WorldPosition).Length;
			if ( d < bestD )
			{
				bestD = d;
				best = hs;
			}
		}

		best?.Use();
	}

	void Moo()
	{
		Trophies.Unlock( "moo" );
		Hotspot near = null;
		var best = float.MaxValue;
		foreach ( var hs in Scene.GetAllComponents<Hotspot>() )
		{
			if ( !hs.IsValid() )
				continue;
			var d = (hs.WorldPosition - WorldPosition).Length;
			if ( d < best && d < 110f )
			{
				best = d;
				near = hs;
			}
		}

		GameState.MooWorld( near?.Id );
	}

	public void RefreshPrompt()
	{
		if ( GameFlow.BlocksWorld || GameState.Talking )
			return;

		Hotspot best = null;
		var bestD = float.MaxValue;
		foreach ( var hs in Scene.GetAllComponents<Hotspot>() )
		{
			if ( !hs.IsValid() || !hs.InRange( WorldPosition ) )
				continue;
			var d = (hs.WorldPosition - WorldPosition).Length;
			if ( d < bestD )
			{
				bestD = d;
				best = hs;
			}
		}

		if ( best is null )
		{
			GameState.SetPrompt( "", "" );
			return;
		}

		GameState.SetPrompt( $"按 E  {best.LabelCn}", $"E  {best.LabelEn}" );
	}

	static bool PressedUse() =>
		Input.Pressed( "Use" ) || Input.Pressed( "use" ) || Input.Pressed( "Submit" ) || Input.Pressed( "submit" );
}