Files
przygody-pana-cegly/progression/Attempt.cs
Gabriel Kaszewski 40bd5c1d86 feat(progression): Attempt ledger with terminal-outcome guard (slice 2)
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.
2026-07-28 18:40:20 +02:00

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;
}
}