Critters.cs
namespace NiuLai;

public sealed class LarkDrift : Component
{
	float _t;

	protected override void OnUpdate()
	{
		if ( GameFlow.BlocksWorld )
			return;

		_t += Time.Delta;
		var player = CalfPlayer.Instance;
		if ( !player.IsValid() )
			return;

		var radius = GameState.InVoid ? 40f : GameState.Beat == Beat.Wake ? 70f : 95f;
		var height = GameState.InVoid ? 48f : 90f;
		WorldPosition = player.WorldPosition + new Vector3( MathF.Cos( _t * 0.7f ) * radius, MathF.Sin( _t * 0.7f ) * radius, height + MathF.Sin( _t * 2.2f ) * 10f );
		WorldRotation = Rotation.FromYaw( _t * 50f ) * Rotation.FromRoll( MathF.Sin( _t * 6f ) * 18f );
	}
}

public sealed class SnakeSlink : Component
{
	Vector3 _origin;
	float _t;

	protected override void OnStart() => _origin = WorldPosition;

	protected override void OnUpdate()
	{
		if ( GameFlow.BlocksWorld )
			return;

		_t += Time.Delta;
		WorldPosition = _origin + new Vector3( MathF.Sin( _t ) * 48f, MathF.Cos( _t * 0.4f ) * 18f, 6f + MathF.Abs( MathF.Sin( _t * 8f ) ) * 3f );
		WorldRotation = Rotation.FromYaw( _t * 40f );
		var n = 0;
		foreach ( var child in GameObject.Children )
		{
			if ( !child.IsValid() )
				continue;
			child.LocalRotation = Rotation.FromYaw( MathF.Sin( _t * 6f + n * 0.7f ) * 8f );
			n++;
		}

		var player = CalfPlayer.Instance;
		if ( player.IsValid() && GameState.Beat == Beat.Snake && !GameState.Bitten )
		{
			if ( (player.WorldPosition - WorldPosition).Length < 55f )
				GameState.GetBitten();
		}
	}
}

public sealed class BaoLaFollow : Component
{
	public static GameObject Body { get; private set; }
	float _gym;

	protected override void OnStart() => Body = GameObject;

	protected override void OnDestroy()
	{
		if ( Body == GameObject )
			Body = null;
	}

	protected override void OnUpdate()
	{
		if ( GameFlow.BlocksWorld && !FilmCut.Playing )
			return;

		if ( GameState.WolvesHunt )
		{
			_gym += Time.Delta;
			WorldRotation = Rotation.FromYaw( _gym * 420f ) * Rotation.FromPitch( 35f );
			WorldPosition += Vector3.Up * MathF.Sin( _gym * 8f ) * 2f;
		}

		if ( GameFlow.BlocksWorld || !GameState.BaoLaFollows )
			return;

		var player = CalfPlayer.Instance;
		if ( !player.IsValid() )
			return;

		var dest = player.WorldPosition - player.WorldRotation.Forward * 70f;
		dest.z = WorldPosition.z;
		var delta = dest - WorldPosition;
		if ( delta.Length > 20f )
		{
			WorldPosition += delta.Normal * 160f * Time.Delta;
			WorldRotation = Rotation.LookAt( delta.WithZ( 0 ), Vector3.Up );
		}
	}
}

public sealed class WolfHunt : Component
{
	[Property] public Vector3 Home { get; set; }
	float _cooldown;
	bool _howled;

	protected override void OnUpdate()
	{
		if ( GameFlow.BlocksWorld )
			return;

		_cooldown -= Time.Delta;
		var player = CalfPlayer.Instance;
		if ( !player.IsValid() )
			return;

		var hunt = GameState.WolvesHunt;
		if ( hunt && !_howled )
		{
			_howled = true;
			GameAudio.PlayWolvesAt( WorldPosition );
		}

		Vector3 target;
		if ( hunt && GameState.BaoLaFollows && BaoLaFollow.Body.IsValid() )
		{
			target = BaoLaFollow.Body.WorldPosition;
		}
		else if ( hunt )
		{
			target = player.WorldPosition;
		}
		else
		{
			target = Home;
			var rock = new Vector3( 1140, 40, WorldPosition.z );
			WorldRotation = Rotation.LookAt( (rock - WorldPosition).WithZ( 0 ), Vector3.Up );
		}

		var delta = (target.WithZ( WorldPosition.z ) - WorldPosition);
		if ( delta.Length > 10f )
		{
			WorldPosition += delta.Normal * (hunt ? 200f : 70f) * Time.Delta;
			WorldRotation = Rotation.LookAt( delta.WithZ( 0 ), Vector3.Up );
		}

		if ( hunt && delta.Length < 55f && _cooldown <= 0f )
		{
			_cooldown = 3.2f;
			if ( GameState.Beat == Beat.Wolves )
				GameState.FirstWolfWave();
		}
	}
}

public sealed class CartGrow : Component
{
	Vector3 _start;
	float _z;

	protected override void OnStart()
	{
		_start = LocalScale;
		_z = WorldPosition.z;
	}

	protected override void OnUpdate()
	{
		if ( GameState.Beat != Beat.Cart )
			return;

		var t = Math.Clamp( GameState.CartMoos / 3f + (GameState.PlayTime % 20f) / 40f, 0f, 1.4f );
		LocalScale = _start * (1f + t * 4.2f );
		var p = WorldPosition;
		p.z = _z + t * 36f;
		WorldPosition = p;
	}
}

public sealed class MomActor : Component
{
	float _sink;

	protected override void OnUpdate()
	{
		if ( !GameState.MomGone )
			return;

		_sink += Time.Delta;
		WorldPosition += Vector3.Down * 28f * Time.Delta;
		if ( _sink > 3.5f )
			GameObject.Enabled = false;
	}
}

public sealed class HerdMarch : Component
{
	[Property] public float Speed { get; set; } = 55f;

	protected override void OnUpdate()
	{
		if ( GameFlow.BlocksWorld )
			return;

		if ( GameState.Beat == Beat.Migrate && !GameState.Talking )
		{
			WorldPosition += Vector3.Forward * (Speed * 0.15f) * Time.Delta;
			return;
		}

		if ( !GameState.SongWalk )
			return;

		WorldPosition += Vector3.Forward * Speed * Time.Delta;
		WorldRotation = Rotation.LookAt( Vector3.Forward, Vector3.Up );
	}
}

public sealed class GrassTuft : Component
{
	[Property] public float HideFor { get; set; } = 10f;
	Vector3 _home;
	float _cd = -1f;
	bool _out;

	protected override void OnStart() => _home = WorldPosition;

	protected override void OnUpdate()
	{
		if ( !_out )
			return;
		_cd -= Time.Delta;
		if ( _cd > 0f )
			return;
		_out = false;
		WorldPosition = _home;
		var hs = Components.Get<Hotspot>();
		if ( hs.IsValid() )
			hs.Enabled = true;
		if ( !GameState.Talking && !FilmCut.Playing && Random.Shared.Float() < 0.55f )
			Chatter.Say( "草 / Grass", Chatter.Pick( Lines.GrassBack ), 2.2f );
	}

	public void Eat()
	{
		if ( _out )
			return;
		GameState.EatGrass();
		GameAudio.PlayClip( GameAudio.SlowDown, 0.75f );
		_out = true;
		_cd = HideFor + Random.Shared.Float() * 5f;
		WorldPosition = _home + Vector3.Down * 80f;
		var hs = Components.Get<Hotspot>();
		if ( hs.IsValid() )
			hs.Enabled = false;
	}
}

public sealed class HerdGossip : Component
{
	float _cool;

	protected override void OnUpdate()
	{
		if ( GameFlow.BlocksWorld || GameState.Talking )
			return;
		_cool -= Time.Delta;
		var p = CalfPlayer.Instance;
		if ( !p.IsValid() || _cool > 0f )
			return;
		if ( (p.WorldPosition - WorldPosition).Length > 70f )
			return;
		_cool = 7f + Random.Shared.Float() * 6f;
		Chatter.Say( "牛群 / Herd", Chatter.Pick( Lines.Gossip ), 2.6f );
	}
}

public sealed class FlyBeacon : Component
{
	float _t;

	protected override void OnUpdate()
	{
		_t += Time.Delta;
		var on = GameState.Beat >= Beat.Wake;
		var s = on ? 0.22f + MathF.Abs( MathF.Sin( _t * 3f ) ) * 0.12f : 0.06f;
		LocalScale = new Vector3( s, s, s );
	}
}

public sealed class VoidDrift : Component
{
	[Property] public int Index { get; set; }
	Vector3 _home;
	float _t;

	protected override void OnStart() => _home = WorldPosition;

	protected override void OnUpdate()
	{
		if ( !GameState.InVoid )
			return;
		_t += Time.Delta;
		WorldPosition = _home + new Vector3( MathF.Sin( _t + Index ) * 12f, MathF.Cos( _t * 0.6f ) * 10f, MathF.Sin( _t * 1.4f ) * 8f );
		if ( Random.Shared.Float() < 0.004f )
			Chatter.Say( "黑 / Black", Chatter.Pick( Lines.VoidBits ), 2.1f );
	}
}

public sealed class BobIdle : Component
{
	[Property] public float Height { get; set; } = 4f;
	[Property] public float Rate { get; set; } = 1.4f;
	Vector3 _origin;
	float _t;

	protected override void OnStart() => _origin = WorldPosition;

	protected override void OnUpdate()
	{
		_t += Time.Delta * Rate;
		WorldPosition = _origin.WithZ( _origin.z + MathF.Sin( _t ) * Height );
	}
}