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++;
}
}