namespace Mr.BrickAdventures.Progression; /// /// Everything shared across all Playthroughs, one per installation. Never reset by starting a new /// Playthrough — see docs/adr/0001-three-progression-scopes.md. /// public sealed class Profile { private readonly Dictionary _bestTimes = new(); private readonly HashSet _achievements = new(); private readonly EventLog _events = new(); private Profile() { } public static Profile Empty() => new(); public bool HasAchievement(AchievementId achievement) => _achievements.Contains(achievement); /// /// Unlocks an Achievement. Announces it only on the transition, so repeated calls are harmless. /// public void UnlockAchievement(AchievementId achievement) { if (!_achievements.Add(achievement)) return; _events.Raise(new AchievementUnlocked(achievement)); } /// /// Removes an unlocked Achievement. Exists for console commands only. /// /// /// Deliberately asymmetric with : unlocking is mirrored to Steam /// by a subscriber, relocking is not and cannot be, because a Steam achievement cannot be /// un-earned. After relocking, this Profile and Steam disagree permanently. That is a property /// of Steam, not a bug here, but it is why this is not offered to gameplay code. /// public void RelockAchievement(AchievementId achievement) => _achievements.Remove(achievement); private readonly Dictionary _statistics = new(); public int ReadStat(StatName stat) => _statistics.GetValueOrDefault(stat); /// /// Adds to a lifetime statistic. The amount must be positive — these only ever go up. /// public void Record(StatName stat, int amount) { if (amount < 1) throw new ArgumentOutOfRangeException(nameof(amount), amount, "A statistic can only be increased."); _statistics[stat] = ReadStat(stat) + amount; } /// /// Takes the events raised since the last drain, clearing them. /// public IReadOnlyList DrainEvents() => _events.Drain(); public LevelTime? BestTimeFor(LevelId level) => _bestTimes.TryGetValue(level, out var time) ? time : null; /// /// Records a completion time, keeping it only if it beats what is already there. Returns true /// when it became the Best Time. A slower run is not an error — it just isn't a record. /// public bool RecordTime(LevelId level, LevelTime time) { if (BestTimeFor(level) is { } best && !time.IsFasterThan(best)) return false; _bestTimes[level] = time; return true; } }