diff --git a/tests/run.lua b/tests/run.lua index 007b45d..df1d081 100644 --- a/tests/run.lua +++ b/tests/run.lua @@ -12066,6 +12066,179 @@ function tests.gameEndPlayerTurnTicksThePlayersDiseaseAndLogsItsEruption() Disease.infect(game.player, Disease.PARASITES) local incubation = Disease.defs[Disease.PARASITES].incubation for _ = 1, incubation - 1 do + game:endPlayerTurn() + eq(game.player.disease.stage, 0, "the infection keeps quietly incubating turn by turn") + end + game:endPlayerTurn() + eq(game.player.disease.stage, 1, "the infection erupts on schedule as the player's own turns tick past") + eq(game.log:last(), "you break out in gut parasites", + "the eruption is reported in the message log with correct you-are-the-subject grammar, not the third-person 'breaks out' a named monster would get") +end + +function tests.gameUseItemDrinkingACurePotionLiftsTheInfectionAndIdentifiesThePotion() + local game = stagedGame(1, arena(21, 11), 3, 6) + Disease.infect(game.player, Disease.ROT) + game.player.inventory = { Items.potionOf("cure") } + local used = game:useItem(1) + eq(used, true, "drinking the potion succeeds") + ok(not Disease.isInfected(game.player), "the cure potion lifts the infection entirely") + ok(contains(game.log:last(), "cleansed"), "the log confirms the cure took effect") + eq(#game.player.inventory, 0, "the spent potion leaves the inventory like any other") +end + +function tests.gameUseItemDrinkingACurePotionAlsoLiftsHostileStatusesNotJustDisease() + local game = stagedGame(1, arena(21, 11), 3, 6) + Status.apply(game.player, Status.POISON, 5, 2) + Status.apply(game.player, Status.CONFUSE, 6, 1) + Status.apply(game.player, Status.STRENGTH, 30, 3) + game.player.inventory = { Items.potionOf("cure") } + local used = game:useItem(1) + eq(used, true, "drinking the potion succeeds") + ok(not Status.has(game.player, Status.POISON), "the cure potion lifts poison even with no disease present") + ok(not Status.has(game.player, Status.CONFUSE), "the cure potion lifts confusion too") + ok(Status.has(game.player, Status.STRENGTH), "the cure potion never touches a buff like strength") + ok(contains(game.log:last(), "fades"), "the log reports the lifted statuses when there was no disease to mention") +end + +function tests.gameUseItemDrinkingACurePotionReportsBothDiseaseAndStatusesWhenBothArePresent() + local game = stagedGame(1, arena(21, 11), 3, 6) + Disease.infect(game.player, Disease.ROT) + Status.apply(game.player, Status.POISON, 5, 2) + game.player.inventory = { Items.potionOf("cure") } + game:useItem(1) + ok(not Disease.isInfected(game.player), "the infection is lifted alongside the status effect") + ok(not Status.has(game.player, Status.POISON), "the status effect is lifted alongside the infection") + ok(contains(game.log:last(), "cleansed away") and contains(game.log:last(), "fades"), + "the single log line credits both the disease cure and the status cure") +end + +function tests.gameUseItemDrinkingACurePotionWithNothingToCureIsHonestInTheLog() + local game = stagedGame(1, arena(21, 11), 3, 6) + game.player.inventory = { Items.potionOf("cure") } + local used = game:useItem(1) + eq(used, true, "the potion is still consumed even when there is nothing to cleanse") + ok(contains(game.log:last(), "nothing to cleanse"), "the log admits nothing was actually cured") +end + +function tests.diseaseAndBiteDiseaseFieldsSurviveALevelSerializationRoundTrip() + local game = stagedGame(1, arena(21, 11), 5, 5) + local rat = Monsters.create(Monsters.byKey.rat, 1, 6, 5, 1) + Disease.infect(rat, Disease.PLAGUE) + rat.disease.stage = 2 + game.level.entities = { rat } + + local restored = Level.fromData(game.level:toData()) + eq(restored.entities[1].biteDisease, Disease.PARASITES, "the species' own carried affliction survives a level round trip") + eq(restored.entities[1].disease.key, Disease.PLAGUE, "the active infection's key survives a level round trip") + eq(restored.entities[1].disease.stage, 2, "the active infection's progressed stage survives a level round trip") +end + +-- captives.lua: netting a battered monster alive instead of finishing it +-- off, and cashing the resulting captive in for goodwill, gold or intel. + +function tests.captivesEligibleAcceptsABatteredWildHostileAndRejectsATooHealthyOne() + local rat = Monsters.create(Monsters.byKey.rat, 1, 6, 6, 1) + rat.hp = 1 + ok(Captives.eligible(rat), "a wild hostile battered down to a sliver of hp is eligible for a net") + rat.hp = rat.maxHp + ok(not Captives.eligible(rat), "a target still at full health is too fierce to net") +end + +function tests.captivesEligibleExcludesBossesMimicsGravesNemesesAndTheVendingShopkeeper() + local orc = Monsters.create(Monsters.byKey.orc, 1, 6, 6, 3) + orc.hp = 1 + orc.isBoss = true + ok(not Captives.eligible(orc), "a boss is excluded from netting even when battered") + orc.isBoss = false + orc.mimic = true + ok(not Captives.eligible(orc), "a mimic is excluded from netting even when battered") + orc.mimic = false + orc.grave = true + ok(not Captives.eligible(orc), "a grave shade is excluded from netting even when battered") + orc.grave = false + orc.nemesis = true + ok(not Captives.eligible(orc), "a nemesis is excluded from netting even when battered") + orc.nemesis = false + ok(Captives.eligible(orc), "the same battered orc becomes eligible again once none of the exclusions apply") + local shopkeeper = Monsters.create(Monsters.byKey.shopkeeper, 2, 6, 6, 4) + shopkeeper.hp = 1 + ok(not Captives.eligible(shopkeeper), "the vending shopkeeper is never eligible for a net even when battered") +end + +function tests.captivesCanReachRespectsRangeAndLineOfSight() + local level = arena(21, 11) + ok(Captives.canReach(level.map, 3, 6, 3 + Captives.RANGE, 6), "a target at exactly max range with a clear line is reachable") + ok(not Captives.canReach(level.map, 3, 6, 3 + Captives.RANGE + 1, 6), "a target one cell beyond max range is unreachable") + level.map:set(5, 6, Tiles.WALL) + ok(not Captives.canReach(level.map, 3, 6, 7, 6), "a wall blocking the line of sight makes an in-range target unreachable") +end + +function tests.captivesValueOfMatchesKillXpAndAddsAnEliteBonus() + local orc = Monsters.create(Monsters.byKey.orc, 1, 6, 6, 3) + eq(Captives.valueOf(orc), Monsters.xpFor(orc), "a plain monster's capture value equals its kill xp") + orc.elite = true + orc.eliteTier = "champion" + local baseXp = Monsters.xpFor(orc) + eq(Captives.valueOf(orc), baseXp + Elites.xpBonus(orc, baseXp), + "an elite's capture value adds the same bonus a kill's xp would") + ok(Captives.valueOf(orc) > baseXp, "the elite bonus actually raises the capture value above the plain kill xp") +end + +function tests.captivesResolveRefusesWithNoClearThrowWithoutSpendingTheNet() + local game = stagedGame(4, arena(21, 11), 2, 2) + local rat = Monsters.create(Monsters.byKey.rat, 1, 19, 9, 1) + rat.hp = 1 + game.level.entities = { rat } + local net = Items.net() + local success, lines = Captives.resolve(game, game.player, net, rat) + eq(success, false, "a net thrown beyond range never leaves the hand") + ok(contains(lines[1], "clear throw"), "the reason given is the lack of a clear throw") +end + +function tests.captivesResolveWastesTheNetOnATargetStillTooHealthyToSubdue() + local game = stagedGame(4, arena(21, 11), 3, 6) + local rat = Monsters.create(Monsters.byKey.rat, 1, 6, 6, 1) + game.level.entities = { rat } + local net = Items.net() + local success, lines = Captives.resolve(game, game.player, net, rat) + eq(success, true, "a net thrown at a still-healthy target still leaves the hand") + eq(#game.level.captives, 0, "no captive is created from a target too healthy to subdue") + eq(rat.hp, rat.maxHp, "the target takes no damage from a wasted net") + ok(contains(lines[1], "fierce"), "the log explains the target was too fierce to net") +end + +function tests.captivesResolveCanThrashFreeAndStillSpendTheNet() + local game = stagedGame(4, arena(21, 11), 3, 6) + local rat = Monsters.create(Monsters.byKey.rat, 1, 6, 6, 1) + rat.hp = 1 + game.level.entities = { rat } + game.rng = Rng.new(1) + local net = Items.net() + local success, lines = Captives.resolve(game, game.player, net, rat) + eq(success, true, "a lost catch roll still leaves the net thrown") + eq(#game.level.captives, 0, "no captive is created when the target thrashes free") + eq(rat.hp, 1, "a thrashing target takes no further damage") + ok(rat:isAlive(), "a target that thrashes free is still alive") + ok(contains(lines[1], "thrashes free"), "the log reports the target thrashing free") +end + +function tests.captivesResolveCatchesABatteredTargetIntoACaptiveAndZeroesItsHp() + local game = stagedGame(4, arena(21, 11), 3, 6) + local rat = Monsters.create(Monsters.byKey.rat, 1, 6, 6, 1) + rat.hp = 1 + game.level.entities = { rat } + game.rng = Rng.new(2) + local net = Items.net() + local success, lines = Captives.resolve(game, game.player, net, rat) + eq(success, true, "a successful catch leaves the net thrown") + eq(#game.level.captives, 1, "a successful catch adds exactly one captive") + local captive = game.level.captives[1] + eq(captive.name, rat.name, "the captive keeps the caught monster's name") + eq(captive.species, rat.species, "the captive keeps the caught monster's species") + eq(captive.faction, rat.faction, "the captive keeps the caught monster's faction") + eq(captive.value, Monsters.xpFor(rat), "the captive's value matches Captives.valueOf") + eq(rat.hp, 0, "the caught monster's hp is zeroed") + ok(not rat:isAlive(), "the caught monster is no longer alive") end local order = {