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.
44 lines
1.4 KiB
C#
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();
|
|
}
|