Attempt carries its LevelId so settlement decides First Clear vs Replay without trusting the caller. Outcome is terminal and refuses to change: one death costs one life, one completion banks once. Coins.Zero/Add driven out by the ledger rather than added up front. 21 tests. ADR-0005: Playthrough settles Attempts, has no LoseLife.
42 lines
1.4 KiB
C#
42 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);
|
|
|
|
/// <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();
|
|
}
|