feat(progression): Profile aggregate (slice 4)

best times only improve; achievement unlock announces on the transition
only, so popup/Steam subscribers cannot double-fire; statistics are
increase-only (IncrementStat took any int).

RelockAchievement is console-only and documented as not reaching Steam -
a Steam achievement cannot be un-earned, so the disagreement is
permanent and deliberate.

refactor: extract EventLog, composed by both aggregates now that the
duplication actually exists. 34 tests.
This commit is contained in:
2026-07-28 18:56:58 +02:00
parent ffa2b986ae
commit 05149bb4a9
7 changed files with 206 additions and 9 deletions

View File

@@ -4,7 +4,7 @@ namespace Mr.BrickAdventures.Progression;
/// A completion time for one Level. Strictly positive — a zero time means the clock was never
/// running, which is a defect rather than a fast run.
/// </summary>
public readonly struct LevelTime
public readonly struct LevelTime : IEquatable<LevelTime>
{
public double Seconds { get; }
@@ -13,4 +13,11 @@ public readonly struct LevelTime
public static LevelTime Of(double seconds) => double.IsFinite(seconds) && seconds > 0
? new LevelTime(seconds)
: throw new ArgumentOutOfRangeException(nameof(seconds), seconds, "A completion time must be a finite positive number.");
public bool IsFasterThan(LevelTime other) => Seconds < other.Seconds;
public bool Equals(LevelTime other) => Seconds.Equals(other.Seconds);
public override bool Equals(object? obj) => obj is LevelTime other && Equals(other);
public override int GetHashCode() => Seconds.GetHashCode();
public override string ToString() => $"{Seconds:0.###}s";
}