test: cover tests.commandsApplyStealFailsWithNothingWardedUnderfootWi...

This commit is contained in:
dualfroz committed 2026-09-13 13:53:13 +02:00
1 parent 69dd449825
commit 808016f925
1 file changed
+173
+173
View File
@@ -13623,6 +13623,179 @@ end
function tests.commandsApplyStealTakesTheWardedWareUnderfootAndSpendsTheTurn()
local game = stagedGame(1, arena(21, 11), 5, 5)
local keeper = Monsters.create(Monsters.byKey.shopkeeper, 1, 10, 10, 4)
game.level.entities = { keeper }
game.level.floorItems = { { x = 5, y = 5, item = Items.scroll("identify"), price = 18, shopId = keeper.id } }
local invBefore = #game.player.inventory
local consumesTurn = Commands.apply(game, "steal")
eq(consumesTurn, true, "stealing a warded ware underfoot spends the turn")
eq(#game.player.inventory, invBefore + 1, "the stolen item joins the inventory")
eq(keeper.hostile, true, "the keeper turns hostile after the theft")
end
function tests.commandsApplyStealFailsWithNothingWardedUnderfootWithoutSpendingATurn()
local game = stagedGame(1, arena(21, 11), 5, 5)
local consumesTurn, message = Commands.apply(game, "steal")
eq(consumesTurn, false, "stealing with nothing underfoot spends no turn")
eq(message, "there is nothing here worth stealing", "the failure explains itself")
end
function tests.commandsApplyDismissReleasesTheHirelingAndSpendsTheTurn()
local game = stagedGame(1, arena(21, 11), 5, 5)
Mercenaries.hire(game, "sellsword")
ok(game:hasHireling(), "the hire itself succeeded")
local consumesTurn = Commands.apply(game, "dismiss")
eq(consumesTurn, true, "dismissing an under-contract hireling spends the turn")
eq(game:hasHireling(), false, "the hireling is gone")
end
function tests.commandsApplyDismissFailsWithNoHirelingWithoutSpendingATurn()
local game = stagedGame(1, arena(21, 11), 5, 5)
local consumesTurn = Commands.apply(game, "dismiss")
eq(consumesTurn, false, "dismissing with nobody under contract spends no turn")
end
function tests.commandsApplyLearnSpendsAPointNeverATurn()
local game = stagedGame(1, arena(21, 11), 5, 5)
game.player.class = "fighter"
game.player.skillPoints = 1
local turnBefore = game.turn
local success = Commands.apply(game, "learn cleave")
eq(success, false, "learning a skill never spends a turn")
eq(game.turn, turnBefore, "the turn counter is untouched by learning")
eq(Skills.has(game.player, "cleave"), true, "the skill is actually learned")
eq(game.player.skillPoints, 0, "the skill point is spent")
ok(contains(game.log:last(), "learn"), "learning is logged")
local missing, message = Commands.apply(game, "learn")
eq(missing, false, "learn with no skill name spends no turn")
ok(message ~= nil, "the failure names what is missing")
local alreadyKnown = Commands.apply(game, "learn cleave")
eq(alreadyKnown, false, "re-learning an already-known skill spends no turn")
ok(contains(game.log:last(), "already"), "the failure to re-learn is logged")
end
-- Skills.active/Skills.passive keys are camelCase (secondWind, poisonBlade,
-- manaShield), but Commands.parse lowercases every token including a typed
-- skill name before any handler sees it - a player typing the skill's own
-- printed name would otherwise never match it. Typed exactly as a human
-- would type it (mixed case), routed through the real Commands.apply/parse
-- pipeline, not a pre-lowercased literal.
function tests.commandsApplyLearnAcceptsTheSkillsOwnCamelCaseSpellingDespiteTheParserLowercasingIt()
local game = stagedGame(1, arena(21, 11), 5, 5)
game.player.class = "fighter"
game.player.level = 5
game.player.skillPoints = 2
local success = Commands.apply(game, "learn secondWind")
eq(success, false, "learning a skill never spends a turn")
eq(Skills.has(game.player, "secondWind"), true, "the camelCase skill is actually learned under its real key")
ok(contains(game.log:last(), "second wind"), "learning is logged under the skill's real name")
local rogue = stagedGame(2, arena(21, 11), 5, 5)
rogue.player.class = "rogue"
rogue.player.level = 5
rogue.player.skillPoints = 2
Commands.apply(rogue, "learn poisonBlade")
eq(Skills.has(rogue.player, "poisonBlade"), true, "a differently-classed camelCase skill also resolves correctly")
local mage = stagedGame(3, arena(21, 11), 5, 5)
mage.player.class = "mage"
mage.player.level = 5
mage.player.skillPoints = 2
Commands.apply(mage, "learn manaShield")
eq(Skills.has(mage.player, "manaShield"), true, "a camelCase PASSIVE skill resolves too, not only active ones")
end
function tests.commandsApplyUseskillAcceptsACamelCaseSkillNameDespiteTheParserLowercasingIt()
local game = stagedGame(4, arena(21, 11), 5, 5)
game.player.class = "fighter"
game.player.level = 5
Skills.learn(game.player, "secondWind")
game.player.hp = 1
local consumesTurn = Commands.apply(game, "useskill secondWind")
eq(consumesTurn, true, "using a camelCase-named skill by its real spelling spends the turn")
ok(game.player.hp > 1, "second wind actually healed the player, proving the real skill ran")
eq(game.player.skillCooldowns.secondWind, Skills.active.secondWind.cooldown,
"the cooldown lands on the real camelCase key, not a lowercased one")
local rogue = stagedGame(5, arena(21, 11), 5, 5)
rogue.player.class = "rogue"
rogue.player.level = 5
Skills.learn(rogue.player, "poisonBlade")
local consumesTurn2 = Commands.apply(rogue, "useskill poisonBlade")
eq(consumesTurn2, true, "using poisonBlade by its real camelCase spelling spends the turn")
eq(rogue.player.pendingPoisonBlade, true, "the venom is actually readied, proving the real skill ran")
end
function tests.commandsApplyBrewCombinesTwoHerbsAtACauldronAndSpendsTheTurn()
local game = stagedGame(1, arena(21, 11), 5, 5)
game.level.cauldrons = { { x = 5, y = 5 } }
game.player.inventory = { Alchemy.herb(Alchemy.SUNPETAL), Alchemy.herb(Alchemy.BLOODROOT) }
local consumesTurn = Commands.apply(game, "brew 1 2")
eq(consumesTurn, true, "brewing two herbs at a cauldron underfoot spends the turn")
eq(game.player.inventory[1].kind, Items.POTION, "the brewed potion replaces the spent herbs")
end
function tests.commandsApplyBrewFailsAwayFromACauldronWithoutSpendingATurn()
local game = stagedGame(1, arena(21, 11), 5, 5)
game.player.inventory = { Alchemy.herb(Alchemy.SUNPETAL), Alchemy.herb(Alchemy.BLOODROOT) }
local consumesTurn = Commands.apply(game, "brew 1 2")
eq(consumesTurn, false, "brewing away from any cauldron spends no turn")
eq(#game.player.inventory, 2, "a failed brew for lack of a cauldron spends nothing")
local missing, message = Commands.apply(game, "brew")
eq(missing, false, "brew with no indices spends no turn")
ok(message ~= nil, "the failure names what is missing")
end
-- Real bug found while manually verifying the new moonbell/sunpetal curdle
-- pairing through the full command pipeline: Game:brewAt returns false both
-- when nothing was ever attempted (no cauldron, bad indices) and when a real
-- attempt curdled (both herbs already spent, gas already vented) - the old
-- handler read either shape of false as "give the turn back", so a player
-- who curdled a pairing got a free retry exactly like a typo would. Fixed by
-- duplicating the preconditions in the handler (same shape as disarm's own
-- fix earlier today) so anything that reaches Game:brewAt always spends the
-- turn regardless of which of its two outcomes it lands on.
function tests.commandsApplyBrewSpendsTheTurnEvenWhenThePairingCurdles()
local game = stagedGame(1, arena(21, 11), 5, 5)
game.level.cauldrons = { { x = 5, y = 5 } }
game.player.inventory = { Alchemy.herb(Alchemy.SUNPETAL), Alchemy.herb(Alchemy.NIGHTCAP) }
local consumesTurn, message = Commands.apply(game, "brew 1 2")
eq(consumesTurn, true, "a curdled brew still consumed a real attempt and must spend the turn")
eq(message, nil, "the curdle's own log line speaks for it, like a successful brew's message does")
eq(#game.player.inventory, 0, "the curdle already spent both herbs before the handler ever saw the outcome")
ok(contains(game.log:recent(1)[1] or "", "curdle"), "the curdle is still logged through the command pipeline")
end
function tests.commandsApplyBrewRejectsTheSameIndexTwiceOrANonHerbItemWithoutSpendingATurn()
local game = stagedGame(1, arena(21, 11), 5, 5)
game.level.cauldrons = { { x = 5, y = 5 } }
game.player.inventory = { Alchemy.herb(Alchemy.SUNPETAL), Items.potion(10) }
local sameIndex = Commands.apply(game, "brew 1 1")
eq(sameIndex, false, "naming the same inventory slot twice spends no turn")
local nonHerb = Commands.apply(game, "brew 1 2")
eq(nonHerb, false, "pairing a herb with a non-herb item spends no turn")
eq(#game.player.inventory, 2, "neither rejected attempt spends any item")
end
function tests.commandsApplyHelpMentionsTheCraftingAndEconomyVerbs()
local game = stagedGame(1, arena(21, 11), 5, 5)
local _, message = Commands.apply(game, "help")
for _, word in ipairs({ "steal", "dismiss", "learn", "forge", "merge", "brew", "socket", "temper", "enchant", "repair" }) do
ok(contains(message, word), "help mentions " .. word)
end
end
function tests.commandsApplyHelpMentionsAppeaseDisarmAndHorde()
local game = stagedGame(1, arena(21, 11), 5, 5)
local _, message = Commands.apply(game, "help")
for _, word in ipairs({ "appease", "disarm", "horde" }) do
ok(contains(message, word), "help mentions " .. word)
end
end
-- Game:manualStep - the turn-cycle integration around commands.lua.
end
local order = {