Trophies.cs
using Sandbox.Services;

namespace NiuLai;

/// <summary>One package trophy. Ident must match the dashboard Manual achievement.</summary>
public sealed class TrophyDef
{
	public string Id { get; init; }
	public string Title { get; init; }
	public string Description { get; init; }
	public int Score { get; init; }
	public bool Hidden { get; init; }
}

/// <summary>
/// 30 film trophies. Local toast + Achievements.Unlock(ident).
/// Paste Catalog Id / Title / Description onto the package dashboard as Manual.
/// </summary>
public static class Trophies
{
	public static readonly TrophyDef[] Catalog =
	{
		new() { Id = "walk_slow", Title = "You Slow A Bit", Description = "Start the walk. The herd is already ahead of you.", Score = 5 },
		new() { Id = "five_year", Title = "Five-Year Handmade", Description = "Open the title. Loading takes five years. Joke is on us.", Score = 5 },
		new() { Id = "open_eye", Title = "Don't Sleep", Description = "Talk to 牛妈妈. She says open your eyes.", Score = 10 },
		new() { Id = "grass_free", Title = "Grass Is Free", Description = "Eat one tuft. Mama said it does not cost money.", Score = 5 },
		new() { Id = "bigger_little", Title = "I Bigger A Little", Description = "Eat 6 tufts and feel the model scale up.", Score = 10 },
		new() { Id = "share_ready", Title = "Hold Some Grass", Description = "Carry 3 tufts. Somebody hungry is coming.", Score = 5 },
		new() { Id = "yun_ding", Title = "Come Up On My Back", Description = "Meet 云玎. Then the world goes black.", Score = 10 },
		new() { Id = "pure_black", Title = "Pure Black Background", Description = "Finish the first dream with the lark.", Score = 10 },
		new() { Id = "not_rope", Title = "I Am Not Rope", Description = "Let 小绳头 bite you. He was never a rope.", Score = 10 },
		new() { Id = "only_jump", Title = "Only Can Jump", Description = "Jump the creek because the snake is behind you.", Score = 10 },
		new() { Id = "empty_bag", Title = "First Go Eat Grass", Description = "Offer 豹拉 an empty bag. He is not impressed.", Score = 5, Hidden = true },
		new() { Id = "bao_la", Title = "Stomach Empty", Description = "Find 豹拉 at night. His belly is a cave.", Score = 10 },
		new() { Id = "share_grass", Title = "You Give Me Food", Description = "Share one tuft with the leopard.", Score = 10 },
		new() { Id = "niu_er", Title = "Identity Not Right", Description = "Hear 牛二 decide 豹拉 is the problem.", Score = 10 },
		new() { Id = "wolf_come", Title = "Wolf Come. Really Come.", Description = "Survive the first wolf rush.", Score = 10 },
		new() { Id = "not_allow", Title = "Not Allow Follow", Description = "Watch the herd ban 豹拉.", Score = 10 },
		new() { Id = "still_here", Title = "I Still Here", Description = "Talk to 豹拉 after nobody believes him.", Score = 10 },
		new() { Id = "dont_look_back", Title = "Don't Look Back", Description = "Hear 妈妈 stay behind in the canyon.", Score = 15 },
		new() { Id = "cart_big", Title = "The Cart Become More Big", Description = "Reach the 台车 while it is still growing.", Score = 10 },
		new() { Id = "think_jump", Title = "Think Then Hop", Description = "Press jump. Wait. Then hop too hard.", Score = 5, Hidden = true },
		new() { Id = "moo", Title = "哞————", Description = "Press F. Moo at nothing in particular.", Score = 5 },
		new() { Id = "moo_cart", Title = "Niu Lai No Listen", Description = "云玎 said don't fight. You mooed at the cart.", Score = 15 },
		new() { Id = "thank_you", Title = "Yun Ding, Thank You", Description = "Say goodbye in the second black dream.", Score = 10 },
		new() { Id = "stand_stable", Title = "Niu Lai Stand Stable", Description = "Wake up. Stand still for once.", Score = 10 },
		new() { Id = "fly_line", Title = "Follow Her Fly Line", Description = "Follow 云玎 north after the dream.", Score = 10 },
		new() { Id = "too_early", Title = "First Finish Your Road", Description = "Talk to 牛爸爸 before you finished the road.", Score = 5, Hidden = true },
		new() { Id = "herd_boss", Title = "I Am Cow Father", Description = "Find 牛爸爸 at the end of the line.", Score = 15 },
		new() { Id = "we_walk", Title = "We Walk Together", Description = "Start the reunion walk with the herd.", Score = 15 },
		new() { Id = "after_rain", Title = "雨后轻风有香", Description = "Finish the movie. The song plays.", Score = 25 },
		new() { Id = "watch_again", Title = "再看一遍", Description = "Start the film a second time.", Score = 10 },
	};

	const float ToastSeconds = 4.4f;

	static readonly HashSet<string> Got = new( StringComparer.OrdinalIgnoreCase );
	static readonly Queue<string> ToastQ = new();
	static string _toastId = "";
	static float _toastLeft;

	public static bool EverWon { get; private set; }
	public static int Version { get; private set; }
	public static bool ToastVisible => _toastLeft > 0f && !string.IsNullOrEmpty( _toastId );
	public static TrophyDef Toast => Find( _toastId );

	public static TrophyDef Find( string id )
	{
		if ( string.IsNullOrEmpty( id ) )
			return null;

		foreach ( var d in Catalog )
		{
			if ( string.Equals( d.Id, id, StringComparison.OrdinalIgnoreCase ) )
				return d;
		}

		return null;
	}

	public static void Unlock( string id )
	{
		if ( string.IsNullOrEmpty( id ) )
			return;
		if ( Find( id ) is null )
			return;
		if ( !Got.Add( id ) )
			return;

		try
		{
			Achievements.Unlock( id );
		}
		catch
		{
			// Dashboard ident missing or services offline — local toast still pops.
		}

		ToastQ.Enqueue( id );
		if ( !ToastVisible )
			ShowNext();
		Version++;
	}

	public static void MarkWon() => EverWon = true;

	public static void Tick( float dt )
	{
		if ( _toastLeft <= 0f )
			return;

		_toastLeft -= dt;
		if ( _toastLeft > 0f )
			return;

		_toastLeft = 0f;
		_toastId = "";
		Version++;
		ShowNext();
	}

	public static void Evaluate()
	{
		if ( GameState.GrassEaten >= 1 )
			Unlock( "grass_free" );
		if ( GameState.GrassEaten >= 6 )
			Unlock( "bigger_little" );
		if ( GameState.GrassBag >= 3 )
			Unlock( "share_ready" );
		if ( GameState.Beat >= Beat.Snake )
			Unlock( "pure_black" );
		if ( GameState.Bitten )
			Unlock( "not_rope" );
		if ( GameState.CreekJumped )
			Unlock( "only_jump" );
		if ( GameState.FedLeopard )
			Unlock( "share_grass" );
		if ( GameState.HerdSuspects )
			Unlock( "niu_er" );
		if ( GameState.Beat >= Beat.Ban )
			Unlock( "wolf_come" );
		if ( GameState.BaoLaBanned )
			Unlock( "not_allow" );
		if ( GameState.MomGone )
			Unlock( "dont_look_back" );
		if ( GameState.Beat >= Beat.Cart )
			Unlock( "cart_big" );
		if ( GameState.MooedCart )
			Unlock( "moo_cart" );
		if ( GameState.Beat >= Beat.Farewell )
			Unlock( "thank_you" );
		if ( GameState.Beat >= Beat.Wake )
			Unlock( "stand_stable" );
		if ( GameState.FoundDad )
			Unlock( "herd_boss" );
		if ( GameState.SongWalk )
			Unlock( "we_walk" );
		if ( GameState.Won )
			Unlock( "after_rain" );
	}

	public static void OnUsed( string id )
	{
		switch ( id )
		{
			case "mom":
				Unlock( "open_eye" );
				break;
			case "lark":
				if ( GameState.Beat == Beat.Migrate )
					Unlock( "yun_ding" );
				break;
			case "leopard":
				Unlock( "bao_la" );
				if ( GameState.FedLeopard )
					Unlock( "still_here" );
				break;
			case "dad":
				if ( GameState.Beat is not Beat.Wake and not Beat.Reunion )
					Unlock( "too_early" );
				break;
		}
	}

	static void ShowNext()
	{
		if ( ToastVisible || ToastQ.Count == 0 )
			return;

		_toastId = ToastQ.Dequeue();
		_toastLeft = ToastSeconds;
		Version++;
	}
}