Jump/JumpAchievements.cs
using System;
using System.Collections.Generic;
using System.Linq;

/// <summary>
/// 成就与累计进度。
///
/// 成就本身要在 sbox 网站后台按 id 建好(解锁模式选 Manual),代码里只负责在条件满足时调用
/// Achievements.Unlock(id)。后台没建过的 id 调用会被安静忽略,不会报错。
///
/// 累计计数(总跳跃次数等)存在本地文件里;磁盘写入只在"一局结束"和"解锁成就"时发生——
/// 每次起跳都写盘太浪费,所以计数先留在内存,局终再落盘。
/// </summary>
public static class JumpAchievements
{
	/// <summary> 一条成就定义(id / 标题 / 描述),后台照着这个表填 </summary>
	public readonly record struct Info( string Id, string Title, string Description );

	/// <summary> 全部成就。id 是后端标识,一旦上传就别再改 </summary>
	public static readonly Info[] All =
	{
		new( "first_jump",       "First Step",        "Make your first jump." ),
		new( "first_landing",    "Safe Landing",      "Land on a platform." ),
		new( "score_10",         "Getting Warm",      "Score 10 in a single run." ),
		new( "score_30",         "Steady Hand",       "Score 30 in a single run." ),
		new( "score_50",         "Half Century",      "Score 50 in a single run." ),
		new( "score_100",        "Centurion",         "Score 100 in a single run." ),
		new( "perfect_3",        "Triple Perfect",    "Land 3 perfect jumps in a row." ),
		new( "perfect_10",       "Perfect Ten",       "Land 10 perfect jumps in a row." ),
		new( "hard_20",          "Steady On Hard",    "Score 20 or more on Hard." ),
		new( "backjump",         "Homeward",          "Jump back onto the platform you came from." ),
		new( "total_100_jumps",  "Hundred Leaps",     "Jump 100 times in total." ),
		new( "total_50_games",   "Persistence",       "Play 50 games in total." ),
		new( "total_500_perfect","Sharpshooter",      "Land 500 perfect jumps in total." ),
	};

	// ---- 累计进度(持久化) ----

	public class Progress
	{
		public int TotalJumps { get; set; }
		public int TotalPerfect { get; set; }
		public int TotalGames { get; set; }
		public int TotalBackJumps { get; set; }
		public int BestStreak { get; set; }
	}

	const string SaveFile = "jump_progress.json";

	static Progress _progress = new();
	static bool _loaded;
	static bool _dirty;

	public static Progress Data
	{
		get { Load(); return _progress; }
	}

	static void Load()
	{
		if ( _loaded ) return;

		try
		{
			// 编辑器早期 FileSystem.Data 还没就绪会抛异常,此时不置位 _loaded,留给下次重试
			if ( FileSystem.Data is null ) return;

			if ( FileSystem.Data.FileExists( SaveFile ) )
			{
				_progress = Json.Deserialize<Progress>( FileSystem.Data.ReadAllText( SaveFile ) ) ?? new Progress();
			}

			_loaded = true;
		}
		catch ( Exception e )
		{
			Log.Warning( $"JumpAchievements load: {e.Message}" );
		}
	}

	public static void Save()
	{
		if ( !_dirty ) return;
		Load();

		try
		{
			FileSystem.Data.WriteAllText( SaveFile, Json.Serialize( _progress ) );
			_dirty = false;
		}
		catch ( Exception e )
		{
			Log.Warning( $"JumpAchievements save: {e.Message}" );
		}
	}

	// ---- 事件入口(都由 JumpGame 调用;菜单演示与新手演示不计入) ----

	/// <summary> 起跳 </summary>
	public static void OnJump()
	{
		Load();

		_progress.TotalJumps++;
		_dirty = true;

		Unlock( "first_jump" );

		if ( _progress.TotalJumps >= 100 ) Unlock( "total_100_jumps" );
	}

	/// <summary> 落在普通平台上 </summary>
	public static void OnLanding() => Unlock( "first_landing" );

	/// <summary> 完美落地,streak 为当前连击数 </summary>
	public static void OnPerfect( int streak )
	{
		Load();

		_progress.TotalPerfect++;
		_progress.BestStreak = Math.Max( _progress.BestStreak, streak );
		_dirty = true;

		if ( streak >= 3 ) Unlock( "perfect_3" );
		if ( streak >= 10 ) Unlock( "perfect_10" );
		if ( _progress.TotalPerfect >= 500 ) Unlock( "total_500_perfect" );
	}

	/// <summary> 倒跳落回身后的平台 </summary>
	public static void OnBackJump()
	{
		Load();

		_progress.TotalBackJumps++;
		_dirty = true;

		Unlock( "backjump" );
	}

	/// <summary> 一局结束。计数在这里落盘 </summary>
	public static void OnGameEnd( int score, JumpDifficulty difficulty )
	{
		Load();

		_progress.TotalGames++;
		_dirty = true;

		if ( score >= 10 ) Unlock( "score_10" );
		if ( score >= 30 ) Unlock( "score_30" );
		if ( score >= 50 ) Unlock( "score_50" );
		if ( score >= 100 ) Unlock( "score_100" );
		if ( difficulty == JumpDifficulty.Hard && score >= 20 ) Unlock( "hard_20" );

		if ( _progress.TotalGames >= 50 ) Unlock( "total_50_games" );

		Save();
	}

	/// <summary> 该成就是否已解锁(本地缓存,未拉到后台数据时一律按未解锁处理) </summary>
	public static bool IsUnlocked( string id )
	{
		try
		{
			return Sandbox.Services.Achievements.All.FirstOrDefault( x => x.Name == id )?.IsUnlocked ?? false;
		}
		catch ( Exception )
		{
			return false;
		}
	}

	/// <summary> 解锁(重复调用会被引擎忽略)。成功后立即落盘累计计数 </summary>
	static void Unlock( string id )
	{
		try
		{
			Sandbox.Services.Achievements.Unlock( id );
			Save();
		}
		catch ( Exception e )
		{
			Log.Warning( $"JumpAchievements unlock {id}: {e.Message}" );
		}
	}
}