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:
@@ -230,6 +230,71 @@ public class ProgressionServiceTests
|
||||
Assert.False(service.CanEnter(Second));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_collected_skill_is_granted_free_and_only_once()
|
||||
{
|
||||
// Skill pickups are not purchases — no coins change hands. Collecting one you already own is
|
||||
// a no-op rather than a free upgrade.
|
||||
var service = ServiceWithSkills();
|
||||
service.RecordCoinsFound(Coins.Of(10));
|
||||
|
||||
Assert.True(service.GrantSkill(BrickThrow));
|
||||
Assert.Equal(SkillLevel.First, service.LevelOf(BrickThrow));
|
||||
Assert.Equal(Coins.Of(10), service.TotalCoins);
|
||||
|
||||
Assert.False(service.GrantSkill(BrickThrow));
|
||||
Assert.Equal(SkillLevel.First, service.LevelOf(BrickThrow));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_revoked_skill_is_gone_from_both_the_attempt_and_the_playthrough()
|
||||
{
|
||||
// BrickShieldSkillComponent consumes itself when broken, so this is gameplay rather than a cheat.
|
||||
var campaign = TwoLevels();
|
||||
var store = new InMemoryPlaythroughStore();
|
||||
var service = new ProgressionService(
|
||||
Profile.Empty(), store, new InMemoryProfileStore(), campaign, Skills(), Lives.Of(3));
|
||||
service.StartPlaythrough(Id);
|
||||
service.EnterLevel(First);
|
||||
service.GrantSkill(BrickThrow);
|
||||
service.FinishLevel(LevelTime.Of(10.0)); // commits it to the Playthrough
|
||||
service.EnterLevel(Second);
|
||||
|
||||
service.RevokeSkill(BrickThrow);
|
||||
|
||||
Assert.False(service.IsOwned(BrickThrow));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Abandoning_an_attempt_discards_it_without_costing_a_life()
|
||||
{
|
||||
var service = ServiceWithSkills();
|
||||
service.RecordCoinsFound(Coins.Of(30));
|
||||
|
||||
service.AbandonAttempt();
|
||||
|
||||
Assert.Equal(3, service.Lives.Value);
|
||||
Assert.Equal(Coins.Zero, service.TotalCoins);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_cheat_rewrites_the_save_shaped_state_and_is_still_validated()
|
||||
{
|
||||
// Cheats go through the snapshot rather than bare mutators, so ADR-0005 holds: there is still
|
||||
// no SetLives or AddCoins for gameplay code to reach for.
|
||||
var service = ServiceWithSkills();
|
||||
|
||||
service.ApplyCheat(snapshot => snapshot with { Bank = 500, Lives = 9 });
|
||||
|
||||
Assert.Equal(Coins.Of(500), service.TotalCoins);
|
||||
Assert.Equal(9, service.Lives.Value);
|
||||
|
||||
// Still validated on the way back in: a negative bank is refused, state left alone.
|
||||
Assert.Throws<ArgumentOutOfRangeException>(
|
||||
() => service.ApplyCheat(snapshot => snapshot with { Bank = -1 }));
|
||||
Assert.Equal(Coins.Of(500), service.TotalCoins);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Picking_up_a_coin_announces_the_new_total_so_the_hud_never_polls()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user