feat(progression): capabilities the adapters need (commit A of the swap)
no behaviour change - additive only. GrantSkill: pickups are free unlocks, and collecting one already owned is a no-op rather than a free upgrade. RevokeSkill: for skills that consume themselves (brick shield). AbandonAttempt: leaving a level without dying, so no life is charged. ApplyCheat rewrites the Playthrough through its snapshot rather than via SetLives/AddCoins mutators, which ADR-0005 rules out - a bare mutator is an invitation, and the old code had two paths to exactly that. Every value is re-validated on the way back in and a rejected cheat leaves the Playthrough untouched, which is asserted. 73 tests.
This commit is contained in:
@@ -130,6 +130,12 @@ public sealed class Playthrough
|
||||
/// Takes the events raised since the last drain, clearing them. The composition root drains
|
||||
/// once after each use case and republishes.
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Forgets an owned Skill. Exists for Skills that consume themselves — the brick shield is spent
|
||||
/// when it breaks — so this is gameplay, not a cheat.
|
||||
/// </summary>
|
||||
public bool RevokeSkill(SkillId skill) => _ownedSkills.Remove(skill);
|
||||
|
||||
public IReadOnlyList<ProgressionEvent> DrainEvents() => _events.Drain();
|
||||
|
||||
public PlaythroughSnapshot Export() => new(
|
||||
|
||||
@@ -176,6 +176,65 @@ public sealed class ProgressionService
|
||||
return PurchaseOutcome.Purchased;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Grants a Skill for free, as a pickup does. No coins change hands, and collecting one already
|
||||
/// owned is a no-op rather than a free upgrade.
|
||||
/// </summary>
|
||||
public bool GrantSkill(SkillId skill)
|
||||
{
|
||||
if (_playthrough is null || _attempt is null) return false;
|
||||
if (IsOwned(skill)) return false;
|
||||
|
||||
if (!_attempt.TryRecordPurchase(skill, SkillLevel.First, Coins.Zero)) return false;
|
||||
|
||||
AnnounceChanges();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Takes a Skill away, from this Attempt's purchases and from the Playthrough alike. For Skills
|
||||
/// that consume themselves rather than as a cheat.
|
||||
/// </summary>
|
||||
public bool RevokeSkill(SkillId skill)
|
||||
{
|
||||
if (_playthrough is null) return false;
|
||||
|
||||
var revoked = _playthrough.RevokeSkill(skill);
|
||||
AnnounceChanges();
|
||||
return revoked;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws the current Attempt away without charging a life — leaving a Level rather than dying in
|
||||
/// it. The ledger is discarded, so coins found and Skills bought during it are lost.
|
||||
/// </summary>
|
||||
public void AbandonAttempt()
|
||||
{
|
||||
_attempt = null;
|
||||
AnnounceChanges();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Console only. Rewrites the Playthrough by transforming its snapshot.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Deliberately shaped this way rather than as <c>SetLives</c> / <c>AddCoins</c> mutators, which
|
||||
/// ADR-0005 rules out — a bare mutator is an invitation, and the code being replaced had two paths
|
||||
/// to exactly that. Going through the snapshot means every value is re-validated on the way back
|
||||
/// in, and a rejected cheat leaves the Playthrough untouched.
|
||||
/// </remarks>
|
||||
public void ApplyCheat(Func<PlaythroughSnapshot, PlaythroughSnapshot> change)
|
||||
{
|
||||
if (_playthrough is null) return;
|
||||
|
||||
var rewritten = Playthrough.Restore(change(_playthrough.Export()));
|
||||
|
||||
_playthrough = rewritten;
|
||||
_attempt = null;
|
||||
_playthroughs.Save(_playthrough);
|
||||
AnnounceChanges();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Charges the active Attempt's death to the Playthrough and persists it. Repeated calls for one
|
||||
/// Attempt cost one life — the Playthrough defers to the Attempt's terminal outcome (ADR-0005),
|
||||
|
||||
Reference in New Issue
Block a user