From 69951891180ccda3651b995d86fa815bec35cbea Mon Sep 17 00:00:00 2001 From: DualFroz Date: Thu, 10 Sep 2026 16:38:26 +0200 Subject: [PATCH] test: cover tests.factionAndProvokedFieldsSurviveEntitySerialization --- tests/run.lua | 173 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) diff --git a/tests/run.lua b/tests/run.lua index 252c759..316da60 100644 --- a/tests/run.lua +++ b/tests/run.lua @@ -5667,6 +5667,179 @@ function tests.factionsAppeaseFailsOnceAFactionIsAlreadyMaxedOut() eq(player.gold, 1000, "nothing is spent when there is nothing to buy") end +function tests.factionAndProvokedFieldsSurviveEntitySerialization() + local kobold = Monsters.create(Monsters.byKey.kobold, 1, 1, 1, 1) + kobold.provoked = true + local restored = Entity.fromData(kobold:toData()) + eq(restored.faction, Factions.GOBLINOID, "a monster's faction tag survives a round trip") + eq(restored.provoked, true, "a monster's provoked flag survives a round trip") +end + +function tests.gameTakeMonsterTurnLeavesAWaryGoblinoidAloneUntilProvoked() + local game = stagedGame(1, arena(21, 11), 3, 6) + local kobold = Monsters.create(Monsters.byKey.kobold, 1, 4, 6, 1) + game.level.entities = { kobold } + local hpBefore = game.player.hp + game:takeMonsterTurn(kobold) + eq(kobold.x, 4, "an unprovoked kobold does not close in") + eq(game.player.hp, hpBefore, "an unprovoked kobold does not attack even when adjacent") +end + +function tests.gamePlayerAttackProvokesAWaryGoblinoidSoItFightsBackNextTurn() + local game = stagedGame(1, arena(21, 11), 3, 6) + local kobold = Monsters.create(Monsters.byKey.kobold, 1, 4, 6, 1) + kobold.hp = 100 + game.level.entities = { kobold } + game:playerAttack(kobold) + eq(kobold.provoked, true, "aiming a melee attack at a wary goblinoid provokes it") + local hpBefore = game.player.hp + game:takeMonsterTurn(kobold) + ok(game.player.hp < hpBefore, "a provoked, adjacent kobold now fights back") +end + +function tests.gameAwardKillSoursFactionReputationWithTheFallensFaction() + local game = stagedGame(1, arena(21, 11), 3, 6) + local orc = Monsters.create(Monsters.byKey.orc, 1, 4, 6, 1) + orc.hp = 1 + game.level.entities = { orc } + game:playerAttack(orc) + eq(orc:isAlive(), false, "the orc dies to the hit") + eq(Factions.reputationFor(game.player, Factions.ORCISH), -5, "the kill sours standing with the orc's faction") +end + +function tests.shopBuyIsMarkedUpWhenMerchantsAreHostile() + local game = stagedGame(1, arena(21, 11), 3, 3) + local keeper = Monsters.create(Monsters.byKey.shopkeeper, 1, 10, 10, 4) + game.level.entities = { keeper } + game.player.gold = 100 + Factions.adjust(game.player, Factions.MERCHANT, -40) + game.level.floorItems = { { x = 4, y = 3, item = Items.potionOf("heal"), price = 15, shopId = keeper.id } } + game:movePlayer(1, 0) + eq(game.player.gold, 78, "a hostile guild marks the stamped price up by half, floored") +end + +function tests.shopBuyIsDiscountedWhenMerchantsAreHonored() + local game = stagedGame(1, arena(21, 11), 3, 3) + local keeper = Monsters.create(Monsters.byKey.shopkeeper, 1, 10, 10, 4) + game.level.entities = { keeper } + game.player.gold = 100 + Factions.adjust(game.player, Factions.MERCHANT, 60) + game.level.floorItems = { { x = 4, y = 3, item = Items.potionOf("heal"), price = 15, shopId = keeper.id } } + game:movePlayer(1, 0) + eq(game.player.gold, 91, "an honored guild discounts the stamped price, floored") +end + +function tests.shopSellValueRisesWithMerchantStanding() + local game = stagedGame(1, arena(21, 11), 3, 3) + game.player.inventory = { Items.potionOf("heal") } + local goldBefore = game.player.gold + Factions.adjust(game.player, Factions.MERCHANT, 60) + local value = game:sellItem(1) + eq(value, 10, "an honored guild pays more than the neutral half-price for the same ware") + eq(game.player.gold, goldBefore + 10, "the sale proceeds land in the wallet") +end + +function tests.stealingFromShopAlsoSoursMerchantReputation() + local game = stagedGame(1, arena(21, 11), 3, 3) + local keeper = Monsters.create(Monsters.byKey.shopkeeper, 1, 10, 10, 4) + game.level.entities = { keeper } + game.level.floorItems = { { x = 4, y = 3, item = Items.scroll("identify"), price = 18, shopId = keeper.id } } + game:stealFromShop(1) + eq(Factions.reputationFor(game.player, Factions.MERCHANT), -20, "a theft sours the merchant guild's opinion of the player") +end + +function tests.resolveQuestNudgesMerchantReputationUpward() + local game = stagedGame(1, arena(21, 11), 5, 5) + game.player.quest = { kind = Quests.CULL, species = "rat", need = 1, have = 1, rewardGold = 30, rewardXp = 15 } + game:resolveQuest() + eq(Factions.reputationFor(game.player, Factions.MERCHANT), 3, "turning in a bounty pads the merchant guild's opinion of the player") +end + +function tests.gameAppeaseFactionSpendsGoldAndLogsTheOutcome() + local game = stagedGame(1, arena(21, 11), 3, 3) + game.player.gold = 100 + local success = game:appeaseFaction(Factions.GOBLINOID) + eq(success, true, "a well-funded tribute succeeds") + eq(game.player.gold, 80, "the tribute's cost is deducted from the wallet") + eq(Factions.reputationFor(game.player, Factions.GOBLINOID), 15, "the tribute raises standing") + ok(contains(game.log:last(), "goblinoid"), "the successful tribute is logged") + + game.player.gold = 0 + local failure = game:appeaseFaction(Factions.BANDIT) + eq(failure, false, "an empty purse cannot pay any tribute") + ok(contains(game.log:last(), "gold"), "the failed tribute explains the shortfall") +end + +function tests.playerReputationSurvivesGameSaveLoadRoundTrip() + local game = stagedGame(1, arena(21, 11), 3, 3) + Factions.adjust(game.player, Factions.GOBLINOID, -40) + Factions.adjust(game.player, Factions.MERCHANT, 10) + local restored = Game.fromData(game:toData()) + eq(Factions.reputationFor(restored.player, Factions.GOBLINOID), -40, "goblinoid standing survives a save/load round trip") + eq(Factions.reputationFor(restored.player, Factions.MERCHANT), 10, "merchant standing survives a save/load round trip") +end + +-- Carry weight (encumbrance.lua): a hoard heavy enough to cross a capacity +-- threshold slows the carrier down, and a wide overage on top of that also +-- costs defense. Weight is derived purely from inventory/equipped items and +-- level, so nothing here needs its own save-format handling. + +function tests.encumbranceItemWeightScalesWithGearPower() + eq(Encumbrance.itemWeight(Items.weapon("dagger", 2)), 6, "a weak weapon carries close to its flat base weight") + eq(Encumbrance.itemWeight(Items.weapon("greatsword", 9)), 9, "a powerful weapon weighs more than a weak one of the same kind") + eq(Encumbrance.itemWeight(Items.armor("padded vest", 0)), 9, "armor has its own, heavier, flat base") +end + +function tests.encumbranceAmmoWeightScalesWithStackCount() + eq(Encumbrance.itemWeight(Items.ammo("arrow", 1)), 1, "a single arrow weighs its unit weight") + eq(Encumbrance.itemWeight(Items.ammo("arrow", 5)), 5, "a stack of arrows weighs the unit weight times the count") +end + +function tests.encumbranceCapacityGrowsWithLevel() + eq(Encumbrance.capacityFor({ level = 1 }), 16, "a level 1 carrier's capacity is the flat base plus one level's worth") + eq(Encumbrance.capacityFor({ level = 5 }), 32, "capacity scales linearly with level") + eq(Encumbrance.capacityFor({}), 16, "a missing level defaults to level 1's capacity") +end + +function tests.encumbranceEquippedGearCountsTowardCarriedWeight() + local carrier = { + inventory = {}, + weapon = Items.weapon("sword", 3), armor = Items.armor("mail", 6), + ring = Items.ring("band", 1, 1), bow = Items.bow("bow", 2, 3), + } + eq(Encumbrance.carriedWeight(carrier), 26, "worn and wielded gear counts exactly like the same items in the pack") +end + +function tests.encumbranceLightLoadHasNoPenalty() + local game = Game.new(1) + eq(game.player:encumbranceTier(), Encumbrance.LIGHT, "a fresh player's starting kit is well under capacity") + eq(game.player:effectiveSpeed(), 100, "a light load never slows the carrier down") + eq(game.player:effectiveDefense(), 1, "a light load never costs defense") +end + +-- Each padding armor has zero power (so bestUpgrade never mistakes it for an +-- upgrade) and so weighs exactly the base 9. +local function padWith(entity, count) + for _ = 1, count do + entity.inventory[#entity.inventory + 1] = Items.armor("lockbox", 0) + end +end + +function tests.encumbranceBurdenedSlowsMovementButNotDefense() + local game = Game.new(1) + game.player.inventory = {} + padWith(game.player, 2) + ok(Encumbrance.carriedWeight(game.player) > Encumbrance.capacityFor(game.player), + "two heavy armors alone clear the fresh player's capacity") + eq(game.player:encumbranceTier(), Encumbrance.BURDENED, "a moderate overage is merely burdened") + eq(game.player:effectiveSpeed(), 80, "a burdened carrier loses a flat chunk of speed") + eq(game.player:effectiveDefense(), 1, "a burdened carrier's defense is untouched") +end + +function tests.encumbranceOverloadedSlowsMovementAndWeakensDefense() + local game = Game.new(1) +end + local order = { "rngDeterminism", "mapDeterminism", "tilesRegistry", "fovKnownScene", "losSightLine", "astarPathing", "combatResolution", "itemsAndEquipment", "affixes", "statusEffects",