Game/LevelProgress.cs
namespace BlockParty;
/// <summary>
/// Per-player level-map progression: which levels have been beaten and which map node was last
/// highlighted. Backed by the persisted <see cref="GameSettings"/> (so it lives in the same
/// settings file as the other player preferences), written through immediately on every change so
/// progress survives a crash mid-session.
/// </summary>
public static class LevelProgress
{
/// <summary>Bump this to reset campaign progression for every player on their next launch. Also
/// retires the cloud recovery mirror with it — <see cref="CloudProgress"/> bakes this version into
/// its stat names, so an old mirror can't resurrect deliberately wiped progress.</summary>
public const int PROGRESS_VERSION = 9;
/// <summary>True if the given level id (see <see cref="LevelDef.Id"/>) has been beaten.</summary>
public static bool IsBeaten( string levelId )
{
if ( string.IsNullOrEmpty( levelId ) )
return false;
return Settings.Current.BeatenLevelIds.Contains( levelId );
}
/// <summary>Record a level as beaten and persist. No-op if it was already recorded, so calling it
/// after every finished run is cheap and idempotent.</summary>
public static void MarkBeaten( string levelId )
{
if ( string.IsNullOrEmpty( levelId ) )
return;
var beaten = Settings.Current.BeatenLevelIds;
if ( beaten.Contains( levelId ) )
return;
beaten.Add( levelId );
Settings.Save();
}
/// <summary>Set (or clear) a level's beaten flag explicitly and persist. Used by the level-select
/// screen's DEBUG right-click toggle to author/reset map progress while testing gating.</summary>
public static void SetBeaten( string levelId, bool beaten )
{
if ( string.IsNullOrEmpty( levelId ) )
return;
var list = Settings.Current.BeatenLevelIds;
bool has = list.Contains( levelId );
if ( beaten == has )
return; // already in the desired state
if ( beaten )
list.Add( levelId );
else
list.Remove( levelId );
Settings.Save();
}
/// <summary>Flip a level's beaten flag and persist (DEBUG helper — see <see cref="SetBeaten"/>).</summary>
public static void ToggleBeaten( string levelId ) => SetBeaten( levelId, !IsBeaten( levelId ) );
/// <summary>Compare the map's currently selectable levels with the previous level-select arrival,
/// persist the new snapshot, and return ids which became selectable since that visit. A null
/// snapshot belongs to a profile predating this feature, so its current unlocks become the baseline
/// instead of all animating retroactively.</summary>
public static System.Collections.Generic.IReadOnlyList<string> CaptureNewlyUnlocked(
System.Collections.Generic.IEnumerable<string> currentlyUnlocked )
{
var current = currentlyUnlocked
.Where( id => !string.IsNullOrEmpty( id ) )
.Distinct()
.ToList();
var previous = Settings.Current.LastSeenUnlockedLevelIds;
var newlyUnlocked = previous is null
? new System.Collections.Generic.List<string>()
: current.Where( id => !previous.Contains( id ) ).ToList();
if ( previous is null || !current.SequenceEqual( previous ) )
{
Settings.Current.LastSeenUnlockedLevelIds = current;
Settings.Save();
}
return newlyUnlocked;
}
/// <summary>The level id the map should reopen focused on (null = the first major level).</summary>
public static string LastSelected => Settings.Current.LastSelectedLevelId;
/// <summary>Remember the map node the player last highlighted and persist. Cheap enough to call on
/// every selection change; a no-op write is skipped so unchanged re-selects don't hit disk.</summary>
public static void SetLastSelected( string levelId )
{
if ( Settings.Current.LastSelectedLevelId == levelId )
return;
Settings.Current.LastSelectedLevelId = levelId;
Settings.Save();
}
}