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.
76 lines
2.7 KiB
C#
76 lines
2.7 KiB
C#
namespace Mr.BrickAdventures.Progression;
|
|
|
|
/// <summary>
|
|
/// One try at a Level: a ledger of everything that happened during the try, plus how it ended.
|
|
/// Never persisted — a discarded Attempt is one that is simply thrown away.
|
|
/// </summary>
|
|
public sealed class Attempt
|
|
{
|
|
public Attempt(LevelId level) => Level = level;
|
|
|
|
/// <summary>
|
|
/// Which Level this is an attempt at. Settlement reads this rather than trusting a level
|
|
/// supplied by the caller, so finishing a Replay cannot advance campaign position.
|
|
/// </summary>
|
|
public LevelId Level { get; }
|
|
|
|
public AttemptOutcome Outcome { get; private set; } = AttemptOutcome.InProgress;
|
|
|
|
public Coins CoinsFound { get; private set; } = Coins.Zero;
|
|
|
|
/// <summary>
|
|
/// Records coins picked up during this Attempt. Silently ignored once the Attempt has ended:
|
|
/// a pickup firing after death is a real event, not an error, and it does not count.
|
|
/// </summary>
|
|
public void RecordCoinsFound(Coins amount)
|
|
{
|
|
if (Outcome != AttemptOutcome.InProgress) return;
|
|
|
|
CoinsFound = CoinsFound.Add(amount);
|
|
}
|
|
|
|
public Coins CoinsSpent { get; private set; } = Coins.Zero;
|
|
|
|
private readonly Dictionary<SkillId, SkillLevel> _skillsBought = new();
|
|
|
|
/// <summary>
|
|
/// Records a Skill bought or upgraded during this Attempt, at the level it ended up at.
|
|
/// Returns false once the Attempt has ended.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Affordability is not checked here — it spans the Attempt and the Playthrough's Bank, so it
|
|
/// belongs to the caller that can see both. This records what was agreed, not whether it was
|
|
/// allowed.
|
|
/// </remarks>
|
|
public bool TryRecordPurchase(SkillId skill, SkillLevel resulting, Coins cost)
|
|
{
|
|
if (Outcome != AttemptOutcome.InProgress) return false;
|
|
|
|
_skillsBought[skill] = resulting;
|
|
CoinsSpent = CoinsSpent.Add(cost);
|
|
return true;
|
|
}
|
|
|
|
public IReadOnlyDictionary<SkillId, SkillLevel> SkillsBought => _skillsBought;
|
|
|
|
/// <summary>
|
|
/// Ends this Attempt in death. Returns false if it has already ended, in which case nothing
|
|
/// changes — a second death event costs no further lives.
|
|
/// </summary>
|
|
public bool TryEndInDeath() => TryEnd(AttemptOutcome.Died);
|
|
|
|
/// <summary>
|
|
/// Ends this Attempt in completion. Returns false if it has already ended, in which case
|
|
/// nothing changes — a second completion event banks nothing further.
|
|
/// </summary>
|
|
public bool TryEndInCompletion() => TryEnd(AttemptOutcome.Cleared);
|
|
|
|
private bool TryEnd(AttemptOutcome outcome)
|
|
{
|
|
if (Outcome != AttemptOutcome.InProgress) return false;
|
|
|
|
Outcome = outcome;
|
|
return true;
|
|
}
|
|
}
|