test: cover tests.sellItemGrantsHalfPriceAndRemovesIt

This commit is contained in:
dualfroz committed 2026-09-08 21:19:47 +02:00
1 parent e7b64c1e92
commit f5f4e93cd3
1 file changed
+173
+173
View File
@@ -2205,6 +2205,179 @@ function tests.passiveShopkeeperIgnoresTheAdjacentPlayer()
eq(game.player.hp, hpBefore, "an un-robbed shopkeeper does not attack even when adjacent") eq(game.player.hp, hpBefore, "an un-robbed shopkeeper does not attack even when adjacent")
end end
function tests.sellItemGrantsHalfPriceAndRemovesIt()
local game = stagedGame(1, arena(21, 11), 3, 3)
local index = #game.player.inventory
local item = game.player.inventory[index]
local expected = Shop.sellValue(item)
local goldBefore = game.player.gold
local value = game:sellItem(index)
eq(value, expected, "selling returns the catalog half-price")
eq(game.player.gold, goldBefore + expected, "the sale proceeds land in the wallet")
eq(#game.player.inventory, index - 1, "the sold item leaves the inventory")
end
function tests.builderOccasionallySeedsAShopWithMatchingWares()
local level = Builder.build(4, Rng.new(1), {})
local keeper
for _, e in ipairs(level.entities) do
if e.species == "shopkeeper" then keeper = e end
end
ok(keeper ~= nil, "this seed's depth-4 floor spawns a shopkeeper")
local waresFound = 0
for _, ground in ipairs(level.floorItems) do
if ground.price then
waresFound = waresFound + 1
eq(ground.shopId, keeper.id, "every priced ware is tagged with its shopkeeper's id")
ok(ground.price > 0, "every priced ware has a positive price")
end
end
ok(waresFound >= 3, "the shop stocks at least three wares")
end
function tests.shopAndLooseGoldStayOffTheShallowFloors()
for depth = 1, 3 do
local level = Builder.build(depth, Rng.new(1), {})
for _, ground in ipairs(level.floorItems) do
ok(ground.item.kind ~= Items.GOLD, "no loose gold appears above depth 4")
ok(not ground.price, "no shop ware appears above depth 4")
end
for _, e in ipairs(level.entities) do
ok(e.species ~= "shopkeeper", "no shopkeeper appears above depth 4")
end
end
end
local function arenaDark(w, h)
local map = Map.new(w, h)
for y = 2, h - 1 do for x = 2, w - 1 do map:set(x, y, Map.FLOOR) end end
return Level.new({
depth = 1, generator = "arena", map = map,
entities = {}, floorItems = {}, traps = {}, lights = {}, dark = true, explored = {},
upX = 2, upY = 2, downX = w - 1, downY = h - 1,
})
end
function tests.classApplyGrantsFighterStatBonuses()
local base = Game.new(9)
local fighter = Game.new(9, { class = "fighter" })
eq(fighter.player.class, "fighter", "the requested class is stamped onto the player")
eq(fighter.player.maxHp, base.player.maxHp + 8, "fighter grants a flat max hp bonus")
eq(fighter.player.hp, base.player.hp + 8, "the hp bonus also tops up current hp")
eq(fighter.player.attack, base.player.attack + 2, "fighter grants a flat attack bonus")
end
function tests.classApplyGrantsRogueGoldAndAttack()
local base = Game.new(9)
local rogue = Game.new(9, { class = "rogue" })
eq(rogue.player.gold, base.player.gold + 15, "rogue starts with extra gold")
eq(rogue.player.attack, base.player.attack + 1, "rogue grants a small attack bonus")
end
function tests.classApplyGrantsMageManaAndAttack()
local base = Game.new(9)
local mage = Game.new(9, { class = "mage" })
eq(mage.player.maxMana, base.player.maxMana + 8, "mage grants a flat mana bonus")
eq(mage.player.mana, base.player.mana + 8, "the mana bonus also tops up current mana")
eq(mage.player.attack, base.player.attack - 1, "mage trades some attack for its mana pool")
end
function tests.classApplyGrantsRangerBowAndAmmo()
local ranger = Game.new(9, { class = "ranger" })
ok(ranger.player.bow ~= nil, "ranger starts with a bow equipped")
local hasAmmo = false
for _, item in ipairs(ranger.player.inventory) do
if item.kind == Items.AMMO then hasAmmo = true end
end
ok(hasAmmo, "ranger starts with a stack of arrows")
end
function tests.invalidClassIsIgnored()
local base = Game.new(9)
local weird = Game.new(9, { class = "wizard" })
eq(weird.player.class, nil, "an unrecognised class name is not applied")
eq(weird.player.maxHp, base.player.maxHp, "stats are unaffected by an invalid class")
end
function tests.skillLearnRespectsClassLevelAndPoints()
local game = stagedGame(1, arena(21, 11), 3, 6)
game.player.class = "fighter"
game.player.skillPoints = 0
eq(game:learnSkill("cleave"), false, "learning fails without a banked skill point")
game.player.skillPoints = 1
eq(game:learnSkill("backstab"), false, "learning a skill from another class fails")
eq(game.player.skillPoints, 1, "a failed learn spends no point")
eq(game:learnSkill("berserk"), false, "learning fails below the skill's level requirement")
eq(game:learnSkill("cleave"), true, "a same-class, level-eligible skill with a point banked learns")
eq(game.player.skillPoints, 0, "learning spends the point")
eq(Skills.has(game.player, "cleave"), true, "the skill is now known")
game.player.skillPoints = 1
eq(game:learnSkill("cleave"), false, "an already-known skill cannot be learned twice")
eq(game.player.skillPoints, 1, "re-learning a known skill spends no point")
end
function tests.skillCooldownBlocksImmediateReuseAndTicksDown()
local game = stagedGame(1, arena(21, 11), 3, 6)
game.player.class = "fighter"
game.player.skills.secondWind = true
game.player.hp = game.player.maxHp - 10
eq(game:useSkill("secondWind"), true, "second wind activates while off cooldown")
eq(game:useSkill("secondWind"), false, "an immediate reuse fails while on cooldown")
for _ = 1, Skills.active.secondWind.cooldown do
game:endPlayerTurn()
end
eq(game.player.skillCooldowns.secondWind, 0, "the cooldown ticks down to zero over that many player turns")
game.player.hp = game.player.maxHp - 10
eq(game:useSkill("secondWind"), true, "second wind is usable again once its cooldown fully elapses")
end
function tests.fighterCleaveHitsPrimaryAndAdjacentMonsters()
local game = stagedGame(1, arena(21, 11), 3, 6)
game.player.class = "fighter"
game.player.skills.cleave = true
local primary = Monsters.create(Monsters.byKey.orc, 1, 4, 6, 1)
local splash = Monsters.create(Monsters.byKey.orc, 2, 3, 5, 1)
game.level.entities = { primary, splash }
local success = game:useSkill("cleave", primary)
eq(success, true, "cleave lands on an adjacent primary target")
ok(primary.hp < primary.maxHp, "the primary target takes damage")
ok(splash.hp < splash.maxHp, "a second monster adjacent to the player also takes splash damage")
ok((splash.maxHp - splash.hp) < (primary.maxHp - primary.hp), "the splash hit deals less than the primary hit")
eq(game.player.skillCooldowns.cleave, Skills.active.cleave.cooldown, "cleave starts its own cooldown")
end
-- Cleave's splash targets must be mitigated by the wielder's own weapon
-- damage type exactly like the primary target already is through
-- Combat.damage/Combat.attack (see combat.lua's damageTypeOf and affixes.lua's
-- own comment that a flaming sword "can do nothing at all against something
-- fire-immune") - before this test the splash loop subtracted a raw
-- attack-minus-defense number straight off hp with no Resistances.apply call
-- at all, so a flaming weapon's cleave splash burned through fire immunity
-- that the very same swing's primary hit already respected.
function tests.fighterCleaveSplashRespectsAFireImmuneTargetLikeThePrimaryHitAlreadyDoes()
local game = stagedGame(1, arena(21, 11), 3, 6)
game.player.class = "fighter"
game.player.skills.cleave = true
game.player.weapon = Affixes.decorate(Items.weapon("dagger", 2), Affixes.weapon[3])
eq(game.player.weapon.damageType, "fire", "the flaming affix retags the dagger's swing as fire")
local primary = Monsters.create(Monsters.byKey.orc, 1, 4, 6, 1)
local immune = Monsters.create(Monsters.byKey.dragon, 2, 3, 5, 6)
game.level.entities = { primary, immune }
game:useSkill("cleave", primary)
ok(primary.hp < primary.maxHp, "the primary target still burns for full mitigated damage")
eq(immune.hp, immune.maxHp, "a fire-immune splash target takes zero cleave splash from a flaming weapon")
end
function tests.fighterSecondWindRestoresPortionOfMaxHp()
local game = stagedGame(1, arena(21, 11), 3, 6)
game.player.class = "fighter"
game.player.skills.secondWind = true
game.player.hp = 5
game:useSkill("secondWind")
eq(game.player.hp, 5 + math.floor(game.player.maxHp * 0.3), "second wind restores three tenths of max hp")
game.player.skillCooldowns.secondWind = 0
game.player.hp = game.player.maxHp - 1
end end
local order = { local order = {