test: cover tests.craftingTemperRefusesWithoutEnoughEssence

This commit is contained in:
dualfroz committed 2026-09-09 13:05:13 +02:00
1 parent d46d2a49d7
commit bf8645b0ad
1 file changed
+173
+173
View File
@@ -2724,6 +2724,179 @@ end
function tests.craftingTemperRaisesPowerAtRisingCostUpToACap() function tests.craftingTemperRaisesPowerAtRisingCostUpToACap()
local game = stagedGame(1, arena(21, 11), 3, 6) local game = stagedGame(1, arena(21, 11), 3, 6)
local weapon = Items.weapon("dagger", 2) local weapon = Items.weapon("dagger", 2)
Items.equipWeapon(weapon, game.player)
local attackBaseline = game.player.attack
local totalCost = 0
for level = 0, Crafting.TEMPER_MAX - 1 do
totalCost = totalCost + (level + 1) * 4
end
game.player.essence = totalCost
for level = 1, Crafting.TEMPER_MAX do
ok(Crafting.temper(game.player, weapon), "temper step " .. level .. " succeeds with enough essence")
end
eq(weapon.power, 2 + Crafting.TEMPER_MAX, "each temper step adds exactly one flat power")
eq(game.player.attack, attackBaseline + Crafting.TEMPER_MAX,
"tempering an equipped weapon updates attack immediately")
eq(game.player.essence, 0, "the full ladder of rising costs is spent exactly")
local success, reason = Crafting.temper(game.player, weapon)
eq(success, false, "a fully tempered item refuses a further step")
eq(reason, "fully tempered", "the refusal names the reason")
end
function tests.craftingTemperRefusesWithoutEnoughEssence()
local game = stagedGame(1, arena(21, 11), 3, 6)
local weapon = Items.weapon("mace", 3)
Items.equipWeapon(weapon, game.player)
game.player.essence = 3
local success, reason = Crafting.temper(game.player, weapon)
eq(success, false, "tempering fails when essence is short of the cost")
eq(reason, "not enough essence", "the refusal names the reason")
eq(weapon.power, 3, "a failed temper leaves power untouched")
eq(game.player.essence, 3, "a failed temper spends nothing")
end
function tests.craftingEnchantGrantsAChosenAffixOnce()
local game = stagedGame(1, arena(21, 11), 3, 6)
local weapon = Items.weapon("dagger", 2)
Items.equipWeapon(weapon, game.player)
local attackBaseline = game.player.attack
game.player.essence = Crafting.ENCHANT_COST
ok(Crafting.enchant(game.player, weapon, "flaming"), "enchanting an unaffixed weapon succeeds")
eq(weapon.affix.key, "flaming", "the chosen affix is attached, not a random roll")
eq(weapon.power, 3, "the affix's power lands on the item")
eq(weapon.name, "flaming dagger", "the affix prefix is applied to the name")
eq(game.player.attack, attackBaseline + 1, "enchanting an equipped weapon updates attack immediately")
eq(game.player.essence, 0, "the enchant cost is spent")
local repeatSuccess, repeatReason = Crafting.enchant(game.player, weapon, "vicious")
eq(repeatSuccess, false, "an already-affixed weapon refuses a second enchant")
eq(repeatReason, "already bears an affix", "the refusal names the reason")
local badSuccess, badReason = Crafting.enchant(game.player, Items.armor("leather armor", 1), "not-a-real-affix")
eq(badSuccess, false, "an unknown affix key is refused")
eq(badReason, "unknown affix", "the refusal names the reason")
local ringSuccess, ringReason = Crafting.enchant(game.player, Items.ring("ring of power", 1, 1), "sharp")
eq(ringSuccess, false, "gear without an affix pool cannot be enchanted")
eq(ringReason, "cannot be enchanted", "the refusal names the reason")
end
function tests.craftingDisenchantStripsAffixAndRefundsEssence()
local game = stagedGame(1, arena(21, 11), 3, 6)
local armor = Affixes.decorate(Items.armor("chain mail", 2), Affixes.armor[2])
Items.equipArmor(armor, game.player)
local defenseBaseline = game.player.defense
eq(armor.name, "reinforced chain mail", "sanity check: the drop rolled the expected affix")
local success, refund = Crafting.disenchant(game.player, armor)
ok(success, "disenchanting an affixed armor succeeds")
eq(refund, Crafting.FORGE_COST + 2 * 3, "the refund scales with the stripped affix's power")
eq(armor.power, 2, "power drops back to its base value")
eq(armor.name, "chain mail", "the affix prefix is removed from the name")
eq(armor.affix, nil, "the affix is gone")
eq(game.player.defense, defenseBaseline - 2, "disenchanting an equipped armor updates defense immediately")
eq(game.player.essence, refund, "the refund lands in the player's essence")
local again, reason = Crafting.disenchant(game.player, armor)
eq(again, false, "an already-plain item has nothing left to disenchant")
eq(reason, "nothing to disenchant", "the refusal names the reason")
end
function tests.craftingSocketedGemRidersStackWithARolledAffixOnHit()
local game = stagedGame(1, arena(21, 11), 3, 6)
local weapon = Affixes.decorate(Items.weapon("dagger", 2), Affixes.weapon[4])
Items.equipWeapon(weapon, game.player)
ok(Crafting.socket(game.player, weapon, Crafting.gem(Crafting.SHOCK, 1)),
"a shock gem sockets alongside the rolled affix")
local orc = Monsters.create(Monsters.byKey.orc, 1, 4, 6, 1)
orc.hp, orc.maxHp = 30, 30
game.level.entities = { orc }
game:playerAttack(orc)
eq(Status.has(orc, Status.POISON), true, "the rolled venomed affix still lands its rider")
eq(Status.has(orc, Status.STUN), true, "the socketed shock gem lands its own rider on the same hit")
end
-- Regression: a shock rider applied during the PLAYER's turn is ticked for
-- the first time on the very same round, inside the victim's own upcoming
-- Game:takeMonsterTurn (Status.tick runs there before Status.skipsTurn is
-- ever checked) - so a duration of 1 would already read 0 and be gone
-- before skipsTurn saw it, and the "stun" would silently never do anything.
-- This confirms the fix (duration 2, see crafting.lua) actually skips the
-- victim's very next turn, not just that the status gets attached.
function tests.craftingShockRiderActuallyStunsTheVictimOnItsNextTurnNotJustOnPaper()
local game = stagedGame(1, arena(21, 11), 3, 6)
local weapon = Items.weapon("dagger", 2)
Items.equipWeapon(weapon, game.player)
ok(Crafting.socket(game.player, weapon, Crafting.gem(Crafting.SHOCK, 1)),
"a bare shock gem sockets onto an unaffixed weapon")
local orc = Monsters.create(Monsters.byKey.orc, 1, 4, 6, 1)
orc.hp, orc.maxHp = 30, 30
game.level.entities = { orc }
game:playerAttack(orc)
eq(Status.has(orc, Status.STUN), true, "the shock rider lands immediately on the hit")
local hpBefore, xBefore, yBefore = game.player.hp, orc.x, orc.y
game:takeMonsterTurn(orc)
eq(game.player.hp, hpBefore, "a stunned orc's very next turn deals no melee damage")
eq(orc.x, xBefore, "a stunned orc's very next turn does not move")
eq(orc.y, yBefore, "a stunned orc's very next turn does not move")
game:takeMonsterTurn(orc)
eq(Status.has(orc, Status.STUN), false, "the stun has fully expired by the turn after that")
ok(game.player.hp < hpBefore, "once unstunned the orc resumes attacking normally")
end
function tests.gameCraftingWrappersDriveTheFullPipeline()
local game = stagedGame(1, arena(21, 11), 3, 6)
local weapon = Items.weapon("dagger", 2)
Items.equipWeapon(weapon, game.player)
local attackBaseline = game.player.attack
game.player.essence = Crafting.FORGE_COST * 2
ok(game:forgeGem(Crafting.FIRE), "forging a gem through the game wrapper succeeds")
ok(game:forgeGem(Crafting.FIRE), "forging a second matching gem succeeds")
local invBefore = #game.player.inventory
local firstGemIndex, secondGemIndex
for i, item in ipairs(game.player.inventory) do
if item.kind == Items.GEM then
if not firstGemIndex then firstGemIndex = i else secondGemIndex = i end
end
end
ok(game:mergeGemsAt(firstGemIndex, secondGemIndex), "merging through the game wrapper succeeds")
eq(#game.player.inventory, invBefore - 1, "merging removes both source gems and adds one merged gem")
local mergedIndex
for i, item in ipairs(game.player.inventory) do
if item.kind == Items.GEM then mergedIndex = i end
end
ok(game:socketItem("weapon", mergedIndex), "socketing the equipped weapon through the game wrapper succeeds")
eq(weapon.socket.element, Crafting.FIRE, "the socketed gem's element lands on the weapon")
eq(game.player.attack, attackBaseline + 2, "socketing a tier-2 gem adds its power to attack")
ok(not game:socketItem("weapon", mergedIndex), "the gem slot is consumed and cannot be socketed twice")
game.player.essence = Crafting.temperCost(weapon)
ok(game:temperItem("weapon"), "tempering the equipped weapon through the game wrapper succeeds")
eq(weapon.power, 2 + 2 + 1, "temper stacks on top of the socket's power")
eq(game.player.attack, attackBaseline + 3, "tempering the equipped weapon updates attack immediately")
game.player.essence = Crafting.ENCHANT_COST
ok(game:enchantItem("weapon", "vicious"), "enchanting the equipped weapon through the game wrapper succeeds")
eq(weapon.affix.key, "vicious", "the chosen affix lands on the weapon")
local beforeDisenchant = game.player.attack
local expectedRefund = Crafting.FORGE_COST + 2 * 3
ok(game:disenchantItem("weapon"), "disenchanting the equipped weapon through the game wrapper succeeds")
eq(game.player.attack, beforeDisenchant - 2, "disenchanting removes the affix's power from attack")
eq(game.player.essence, expectedRefund, "the disenchant refund lands in essence")
ok(contains(game.log:recent(1)[1] or "", "disenchant"), "the disenchant is logged")
end
function tests.monsterKillsGrantEssenceScaledToTheirXpReward()
end end
local order = { local order = {