diff --git a/tests/run.lua b/tests/run.lua index f1d10f5..252c759 100644 --- a/tests/run.lua +++ b/tests/run.lua @@ -5492,6 +5492,179 @@ function tests.hazardsAfflictAppliesTargetResistance() end function tests.spellMissileAndFireballRespectTargetResistance() + local game = stagedGame(1, arena(21, 11), 3, 6) + local wisp = Monsters.create(Monsters.byKey.orc, 1, 4, 6, 1) + wisp.hp, wisp.maxHp = 40, 40 + wisp.resist = { arcane = Resistances.IMMUNE } + game.level.entities = { wisp } + game:castSpell("missile", wisp) + eq(wisp.hp, 40, "an arcane-immune target takes no damage from magic missile") + + local blastGame = stagedGame(2, arena(21, 11), 5, 6) + local orc = Monsters.create(Monsters.byKey.orc, 1, 6, 6, 1) + orc.hp, orc.maxHp = 20, 20 + orc.resist = { fire = Resistances.RESIST } + blastGame.level.entities = { orc } + Effects.apply("fireball", blastGame, blastGame.player, {}) + eq(orc.hp, 16, "a fire-resistant target only takes half of fireball's flat burn") +end + +function tests.throwingAndRangedDamageRespectResistance() + local game = stagedGame(1, arena(21, 11), 3, 6) + local flamingDagger = Affixes.decorate(Items.weapon("dagger", 3), Affixes.weapon[3]) + game.player.inventory[#game.player.inventory + 1] = flamingDagger + local salamander = Monsters.create(Monsters.byKey.orc, 1, 6, 6, 1) + salamander.hp, salamander.maxHp = 40, 40 + salamander.resist = { fire = Resistances.IMMUNE } + game.level.entities = { salamander } + local success = game:throwAt(#game.player.inventory, salamander) + ok(success, "the throw itself still leaves the thrower's hand") + eq(salamander.hp, 40, "a fire-immune target takes no damage from a thrown flaming weapon") + + local rangedGame = stagedGame(1, arena(21, 11), 3, 6) + rangedGame.player.bow = Items.bow("longbow", 3, 6) + rangedGame.player.inventory[#rangedGame.player.inventory + 1] = Items.ammo("arrow", 5) + local armored = Monsters.create(Monsters.byKey.orc, 1, 6, 6, 1) + armored.hp, armored.maxHp = 40, 40 + armored.resist = { physical = Resistances.RESIST } + rangedGame.level.entities = { armored } + rangedGame:fireAt(armored) + eq(armored.hp, 37, "physical resistance halves the arrow's already-defense-reduced damage") +end + +function tests.equippingCinderplateAndFrostbandGrantsTheirResist() + local wearer = Entity.new({ x = 1, y = 1, glyph = "@", name = "you", kind = "player", hp = 20, maxHp = 20, attack = 5, defense = 1 }) + Items.equipArmor(Artifacts.create("cinderplate"), wearer) + eq(Resistances.tierOf(wearer, Resistances.FIRE), Resistances.RESIST, "cinderplate grants fire resist while worn") + Items.equipRing(Artifacts.create("frostband"), wearer) + eq(Resistances.tierOf(wearer, Resistances.COLD), Resistances.RESIST, "frostband grants cold resist while worn") + eq(Resistances.tierOf(wearer, Resistances.POISON), Resistances.NORMAL, "neither artifact grants a resist it was not built for") +end + +function tests.disenchantingAnElementalAffixClearsItsDamageType() + 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) + eq(weapon.damageType, "poison", "venomed tags the weapon as poison-typed") + Crafting.disenchant(game.player, weapon) + eq(weapon.damageType, nil, "disenchanting clears the elemental typing along with the affix") +end + +function tests.bossBreathDamageTypeMatchesTheMonsterAndCanBeShruggedOff() + local game = stagedGame(1, arena(21, 11), 3, 6) + local dragon = Monsters.create(Monsters.byKey.dragon, 1, 6, 6, 1) + game.level.entities = { dragon } + eq(Resistances.tierOf(dragon, Resistances.FIRE), Resistances.IMMUNE, "the bestiary dragon is immune to its own breath element") + eq(Resistances.tierOf(dragon, Resistances.COLD), Resistances.WEAK, "the bestiary dragon is innately weak to cold") + + game.player.resist = { fire = Resistances.IMMUNE } + local hpBefore = game.player.hp + game:monsterShoot(dragon) + eq(game.player.hp, hpBefore, "a fire-immune player takes no damage from dragon breath") + eq(Status.has(game.player, Status.BURNING), false, "a fire-immune player never catches alight from the breath") +end + +function tests.resistFieldsSurviveEntitySerialization() + local dragon = Monsters.create(Monsters.byKey.dragon, 1, 5, 5, 6) + local restored = Entity.fromData(dragon:toData()) + eq(Resistances.tierOf(restored, Resistances.FIRE), Resistances.IMMUNE, "a restored entity keeps its innate resist profile") +end + +function tests.factionsTierBoundariesReflectReputationScore() + eq(Factions.tier(0), "neutral", "a fresh reputation reads neutral") + eq(Factions.tier(-10), "unfriendly", "the unfriendly ceiling is inclusive") + eq(Factions.tier(-9), "neutral", "one point above the unfriendly ceiling is still neutral") + eq(Factions.tier(-30), "hostile", "the hostile ceiling is inclusive") + eq(Factions.tier(25), "friendly", "the friendly floor is inclusive") + eq(Factions.tier(60), "honored", "the honored floor is inclusive") +end + +function tests.factionsAdjustAccumulatesAndClampsToTheHundredBounds() + local player = {} + Factions.adjust(player, Factions.GOBLINOID, -40) + eq(Factions.reputationFor(player, Factions.GOBLINOID), -40, "a first adjustment lands from the implicit zero baseline") + Factions.adjust(player, Factions.GOBLINOID, -80) + eq(Factions.reputationFor(player, Factions.GOBLINOID), -100, "reputation floors at -100") + Factions.adjust(player, Factions.GOBLINOID, 500) + eq(Factions.reputationFor(player, Factions.GOBLINOID), 100, "reputation caps at 100") + eq(Factions.reputationFor(player, Factions.MERCHANT), 0, "an untouched faction still reads neutral zero") +end + +function tests.factionsRegisterKillSoursOnlyTheFallensFaction() + local player = {} + local goblin = Monsters.create(Monsters.byKey.goblin, 1, 1, 1, 2) + Factions.registerKill(player, goblin) + eq(Factions.reputationFor(player, Factions.GOBLINOID), -5, "felling a goblin sours goblinoid standing") + eq(Factions.reputationFor(player, Factions.VERMIN), 0, "an unrelated faction is left untouched") +end + +function tests.factionsRegisterKillPenalizesABossFarMoreThanARegularKill() + local player = {} + local dragon = Monsters.create(Monsters.byKey.dragon, 1, 1, 1, 6) + dragon.isBoss = true + Factions.registerKill(player, dragon) + eq(Factions.reputationFor(player, Factions.DRAKE), -15, "a boss kill costs three times a rank-and-file kill") +end + +function tests.factionsRegisterKillIgnoresAFactionlessTarget() + local player = {} + Factions.registerKill(player, { faction = nil }) + eq(player.reputation, nil, "a factionless kill never even creates a reputation table") +end + +function tests.factionsPriceMultiplierScalesFromHostileToHonored() + local player = {} + eq(Factions.priceMultiplier(player, Factions.MERCHANT), 1.0, "a fresh, neutral standing charges catalog price") + Factions.adjust(player, Factions.MERCHANT, -40) + eq(Factions.priceMultiplier(player, Factions.MERCHANT), 1.5, "a hostile guild marks its prices up") + Factions.adjust(player, Factions.MERCHANT, 100) + eq(Factions.priceMultiplier(player, Factions.MERCHANT), 0.65, "an honored guild discounts its prices") +end + +function tests.factionsIsWaryTrueForAFreshGoblinoidAndFalseOnceProvoked() + local player = {} + local kobold = Monsters.create(Monsters.byKey.kobold, 1, 1, 1, 1) + eq(Factions.isWary(player, kobold), true, "an unprovoked goblinoid starts wary of a stranger") + kobold.provoked = true + eq(Factions.isWary(player, kobold), false, "a provoked goblinoid is no longer wary") +end + +function tests.factionsIsWaryIsFalseForNonTribalFactionsAndOnceTheTribeGoesHostile() + local player = {} + local rat = Monsters.create(Monsters.byKey.rat, 1, 1, 1, 1) + eq(Factions.isWary(player, rat), false, "vermin were never a wary tribe to begin with") + local kobold = Monsters.create(Monsters.byKey.kobold, 2, 1, 1, 1) + Factions.adjust(player, Factions.GOBLINOID, -40) + eq(Factions.isWary(player, kobold), false, "a tribe hated into hostility stops holding back even for a fresh member") +end + +function tests.factionsAppeaseSpendsGoldAndRaisesReputationOrFailsWithoutFunds() + local player = { gold = 10 } + local success, cost = Factions.appease(player, Factions.GOBLINOID) + eq(success, false, "an empty purse cannot buy any goodwill") + eq(cost, 20, "the failure reports the base tribute cost at neutral standing") + eq(player.gold, 10, "a failed tribute spends nothing") + player.gold = 100 + local success2, cost2 = Factions.appease(player, Factions.GOBLINOID) + eq(success2, true, "a well-stocked purse can pay the tribute") + eq(cost2, 20, "the neutral tribute costs its base amount") + eq(player.gold, 80, "the tribute's cost is deducted") + eq(Factions.reputationFor(player, Factions.GOBLINOID), 15, "the tribute raises standing by its fixed amount") +end + +function tests.factionsAppeaseCostsMoreTheDeeperTheGrudge() + local player = { gold = 1000 } + Factions.adjust(player, Factions.BANDIT, -30) + local _, cost = Factions.appease(player, Factions.BANDIT) + eq(cost, 80, "the tribute cost climbs with how negative the standing already is") +end + +function tests.factionsAppeaseFailsOnceAFactionIsAlreadyMaxedOut() + local player = { gold = 1000, reputation = { [Factions.GOBLINOID] = 100 } } + local success, cost = Factions.appease(player, Factions.GOBLINOID) + eq(success, false, "an already-maxed relationship has nothing left to buy") + eq(cost, 0, "the failure reports there was no cost, not an unaffordable one") + eq(player.gold, 1000, "nothing is spent when there is nothing to buy") end local order = {