From ef064dfe9d988733431316bd9ff51596b68e7cbb Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sat, 12 Sep 2026 22:06:40 +0200 Subject: [PATCH] test: cover tests.commandsApplyItemCommandsRejectAMissingOrNonNumeric... --- tests/run.lua | 173 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) diff --git a/tests/run.lua b/tests/run.lua index 56a432c..05f29ed 100644 --- a/tests/run.lua +++ b/tests/run.lua @@ -12931,6 +12931,179 @@ function tests.commandsApplyUseDrinksAPotionByInventoryIndexAndSpendsTheTurn() eq(#game.player.inventory, before - 1, "the consumed potion leaves the inventory") end +function tests.commandsApplyItemCommandsRejectAMissingOrNonNumericIndexWithoutSpendingATurn() + local game = stagedGame(1, arena(21, 11), 5, 5) + for _, verb in ipairs({ "use", "drop", "sell", "throw", "capture" }) do + local consumesTurn, message = Commands.apply(game, verb) + eq(consumesTurn, false, verb .. " with no inventory number spends no turn") + ok(message ~= nil and #message > 0, verb .. " explains what index it needs") + end + local consumesTurn, message = Commands.apply(game, "use ninety") + eq(consumesTurn, false, "a non-numeric inventory argument spends no turn") + ok(message ~= nil, "the non-numeric argument still gets a clear reason") +end + +function tests.commandsApplyCastAutoTargetsTheNearestVisibleHostile() + local game = stagedGame(1, arena(21, 11), 5, 5) + local near = Monsters.create(Monsters.byKey.rat, 5, 8, 5, 1) + local far = Monsters.create(Monsters.byKey.rat, 9, 15, 5, 1) + near.hp, far.hp = 30, 30 + game.level.entities = { near, far } + local consumesTurn = Commands.apply(game, "cast missile") + eq(consumesTurn, true, "casting a known targeted spell at a visible hostile spends the turn") + ok(near.hp < 30, "the nearer visible hostile takes the hit") + eq(far.hp, 30, "the farther hostile is left untouched") +end + +function tests.commandsApplyCastBreaksATiedDistanceByAscendingMonsterId() + local game = stagedGame(1, arena(21, 11), 5, 5) + local higherId = Monsters.create(Monsters.byKey.rat, 9, 8, 5, 1) + local lowerId = Monsters.create(Monsters.byKey.rat, 3, 5, 8, 1) + higherId.hp, lowerId.hp = 30, 30 + game.level.entities = { higherId, lowerId } + Commands.apply(game, "cast missile") + ok(lowerId.hp < 30, "a tied Chebyshev distance is broken toward the lower monster id") + eq(higherId.hp, 30, "the higher-id tied hostile is left untouched") +end + +function tests.commandsApplyCastFailsWithoutSpendingATurnWhenNoHostileIsVisible() + local game = stagedGame(1, arena(21, 11), 5, 5) + local consumesTurn, message = Commands.apply(game, "cast missile") + eq(consumesTurn, false, "a targeted spell with nothing to target spends no turn") + eq(message, "there is nothing in sight to cast that at", "the failure explains itself") +end + +function tests.commandsApplyCastRejectsAnUnknownOrUnlearnedSpellWithoutSpendingATurn() + local game = stagedGame(1, arena(21, 11), 5, 5) + local consumesTurn, message = Commands.apply(game, "cast nonsense") + eq(consumesTurn, false, "an unknown spell key spends no turn") + eq(message, "you know no spell by that name", "the failure names the problem") +end + +function tests.commandsApplyCastConfuseAppliesTheStatusThroughTheFullCommandPipeline() + local game = stagedGame(1, arena(21, 11), 5, 5) + Spells.learn(game.player, "confuse") + local rat = Monsters.create(Monsters.byKey.rat, 1, 8, 5, 1) + game.level.entities = { rat } + local consumesTurn = Commands.apply(game, "cast confuse") + eq(consumesTurn, true, "casting confuse on a visible hostile spends the turn") + eq(Status.has(rat, Status.CONFUSE), true, "the nearest visible hostile carries the confuse status afterward") +end + +function tests.commandsApplyCastBlindAppliesTheStatusThroughTheFullCommandPipeline() + local game = stagedGame(1, arena(21, 11), 5, 5) + Spells.learn(game.player, "blind") + local rat = Monsters.create(Monsters.byKey.rat, 1, 8, 5, 1) + game.level.entities = { rat } + local consumesTurn = Commands.apply(game, "cast blind") + eq(consumesTurn, true, "casting blind on a visible hostile spends the turn") + eq(Status.has(rat, Status.BLIND), true, "the nearest visible hostile carries the blind status afterward") +end + +function tests.commandsApplyCastWeakenAppliesTheStatusThroughTheFullCommandPipeline() + local game = stagedGame(1, arena(21, 11), 5, 5) + Spells.learn(game.player, "weaken") + local rat = Monsters.create(Monsters.byKey.rat, 1, 8, 5, 1) + game.level.entities = { rat } + local consumesTurn = Commands.apply(game, "cast weaken") + eq(consumesTurn, true, "casting weaken on a visible hostile spends the turn") + eq(Status.has(rat, Status.WEAKEN), true, "the nearest visible hostile carries the weaken status afterward") +end + +-- levitate is self-targeted (Spells.catalog.levitate.targeted == false), so +-- unlike cast confuse/blind above there is no hostile to resolve at all - +-- this is the pipeline's only self-buff cast covered end to end. +function tests.commandsApplyCastLevitateAppliesTheStatusToTheCasterThroughTheFullCommandPipeline() + local game = stagedGame(1, arena(21, 11), 5, 5) + Spells.learn(game.player, "levitate") + local consumesTurn = Commands.apply(game, "cast levitate") + eq(consumesTurn, true, "casting levitate spends the turn") + eq(Status.has(game.player, Status.LEVITATE), true, "the caster carries the levitate status afterward") +end + +-- cleanse is self-targeted like levitate, so no hostile needs to be in sight +-- at all - the pipeline still finds and lifts the caster's own status. +function tests.commandsApplyCastCleanseLiftsTheCastersOwnStatusThroughTheFullCommandPipeline() + local game = stagedGame(1, arena(21, 11), 5, 5) + Spells.learn(game.player, "cleanse") + Status.apply(game.player, Status.POISON, 5, 2) + local consumesTurn = Commands.apply(game, "cast cleanse") + eq(consumesTurn, true, "casting cleanse spends the turn") + eq(Status.has(game.player, Status.POISON), false, "the caster's own poison is gone afterward") +end + +function tests.commandsApplyThrowAndFireFailWithoutSpendingATurnWhenNoHostileIsVisible() + local game = stagedGame(1, arena(21, 11), 5, 5) + local consumesTurn1, message1 = Commands.apply(game, "throw 1") + eq(consumesTurn1, false, "throwing with nothing in sight spends no turn") + eq(message1, "there is nothing in sight to throw at", "the failure explains itself") + local consumesTurn2, message2 = Commands.apply(game, "fire") + eq(consumesTurn2, false, "firing with nothing in sight spends no turn") + eq(message2, "there is nothing in sight to fire at", "the failure explains itself") +end + +function tests.commandsApplyUseskillRejectsAnUnknownOrUnlearnedSkillWithoutSpendingATurn() + local game = stagedGame(1, arena(21, 11), 5, 5) + local unknown = Commands.apply(game, "useskill nonsense") + eq(unknown, false, "an unknown skill key spends no turn") + local unlearned = Commands.apply(game, "useskill cleave") + eq(unlearned, false, "a skill the player never learned spends no turn") +end + +function tests.commandsApplyUseskillCleaveAutoTargetsAnAdjacentHostile() + local game = stagedGame(1, arena(21, 11), 5, 5) + game.player.class = "fighter" + Skills.learn(game.player, "cleave") + local rat = Monsters.create(Monsters.byKey.rat, 1, 6, 5, 1) + rat.hp = 20 + game.level.entities = { rat } + local consumesTurn = Commands.apply(game, "useskill cleave") + eq(consumesTurn, true, "cleave against an adjacent hostile spends the turn") + ok(rat.hp < 20, "cleave's own attack lands on the auto-targeted adjacent hostile") +end + +-- Every targeted handler (throw/capture/fire/cast/useskill) accepts an +-- optional trailing target number, resolved against the exact same 1-based +-- listing Handlers.look/examine already print. These tests all set up a +-- nearer hostile the OLD nearest-only auto-target would have picked, plus a +-- farther one at a later position in game.level.entities (the same order +-- look numbers its list in), and confirm the explicit number reaches past +-- the nearer default to the one actually named. +function tests.commandsApplyThrowWithAnExplicitTargetNumberHitsThatHostileInsteadOfTheNearestOne() + local game = stagedGame(4, arena(21, 11), 5, 5) + local near = Monsters.create(Monsters.byKey.rat, 1, 5, 6, 1) + local far = Monsters.create(Monsters.byKey.rat, 1, 5, 8, 1) + near.hp, far.hp = 20, 20 + game.level.entities = { near, far } + game.player.inventory[#game.player.inventory + 1] = Items.weapon("dagger", 2) + local index = #game.player.inventory + local consumesTurn = Commands.apply(game, "throw " .. index .. " 2") + eq(consumesTurn, true, "throwing at an explicit in-sight target number spends the turn") + ok(far.hp < 20, "the explicitly named second hostile takes the hit") + eq(near.hp, 20, "the nearer, non-targeted hostile is left untouched") +end + +function tests.commandsApplyThrowRejectsAnExplicitTargetNumberThatIsNotInSightWithoutSpendingATurn() + local game = stagedGame(4, arena(21, 11), 5, 5) + local rat = Monsters.create(Monsters.byKey.rat, 1, 5, 6, 1) + game.level.entities = { rat } + game.player.inventory[#game.player.inventory + 1] = Items.weapon("dagger", 2) + local index = #game.player.inventory + local before = #game.player.inventory + local consumesTurn, message = Commands.apply(game, "throw " .. index .. " 5") + eq(consumesTurn, false, "naming a target number nothing is visible at spends no turn") + eq(message, "no monster in sight at that number", "the failure names the specific problem") + eq(#game.player.inventory, before, "the item is never spent on a rejected target number") +end + +function tests.commandsApplyFireWithAnExplicitTargetNumberShootsThatHostileInsteadOfTheNearestOne() + local game = stagedGame(4, arena(21, 11), 5, 5) + local near = Monsters.create(Monsters.byKey.rat, 1, 5, 6, 1) + local far = Monsters.create(Monsters.byKey.rat, 1, 5, 8, 1) + near.hp, far.hp = 20, 20 + game.level.entities = { near, far } + game.player.bow = Items.bow("short bow", 2, 6) + game.player.inventory[#game.player.inventory + 1] = Items.ammo("arrow", 2) end local order = {