Files
przygody-pana-cegly/progression/Coins.cs
Gabriel Kaszewski ffa2b986ae feat(progression): Playthrough aggregate (slice 3)
no LoseLife (ADR-0005). SettleDeath / SettleCompletion defer to the
Attempt's terminal outcome, so both are atomic and idempotent.
SettleCompletion branches on HasCleared itself - a Replay cannot bank
or move campaign position backwards (ADR-0003).

Campaign value object holds authored level order; ReachableLevels is
derived, replacing the stored UnlockedLevels. Coins.IsAtLeast driven
out by cross-scope affordability.

29 tests. CONTEXT.md: Campaign, Settle.
2026-07-28 18:52:20 +02:00

44 lines
1.4 KiB
C#

namespace Mr.BrickAdventures.Progression;
/// <summary>
/// A non-negative quantity of coins. Amounts only — there is no signed coin value; a shortfall is
/// a failed subtraction the caller must handle, never a negative total.
/// </summary>
public readonly struct Coins : IEquatable<Coins>
{
public int Value { get; }
private Coins(int value) => Value = value;
public static readonly Coins Zero = new(0);
public static Coins Of(int value) => value < 0
? throw new ArgumentOutOfRangeException(nameof(value), value, "Coins cannot be negative.")
: new Coins(value);
public Coins Add(Coins other) => new(Value + other.Value);
public bool IsAtLeast(Coins other) => Value >= other.Value;
/// <summary>
/// Subtracts <paramref name="amount"/>. Returns false when the amount exceeds what is held,
/// leaving <paramref name="remaining"/> untouched rather than clamping to zero.
/// </summary>
public bool TrySubtract(Coins amount, out Coins remaining)
{
if (amount.Value > Value)
{
remaining = this;
return false;
}
remaining = new Coins(Value - amount.Value);
return true;
}
public bool Equals(Coins other) => Value == other.Value;
public override bool Equals(object? obj) => obj is Coins other && Equals(other);
public override int GetHashCode() => Value;
public override string ToString() => Value.ToString();
}