Combat rework: damage types, races with variants, armour weights, tailoring

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-10-20 14:16:12 +01:00
parent a35bcd6512
commit ba7556de4d
7 changed files with 715 additions and 254 deletions

View file

@ -730,9 +730,13 @@ class TierTest(unittest.TestCase):
self.assertIn(f"{key}_{piece}", game.ITEMS)
self.assertIn(f"smith_{key}_{piece}", game.ACTION_BY_ID)
self.assertIn(f"make_{key}_shield", game.ACTION_BY_ID) # builders make shields
for *_, prefix, _label, _level, _armour in game.LEATHERS:
for *_, prefix, _label, _level in game.LEATHERS:
for piece in ("cap", "legs", "body"):
self.assertIn(f"{prefix}_{piece}", game.ITEMS)
for *_, prefix, _label in game.CLOTHS:
for piece in ("hood", "legs", "body"):
self.assertIn(f"{prefix}_{piece}", game.ITEMS)
self.assertIn(f"sew_{prefix}_{piece}", game.ACTION_BY_ID)
self.assertEqual(game.ITEMS["rune_body"]["plural"], "Rune cuirasses")
def test_gear_needs_levels_to_wear(self):
@ -758,7 +762,7 @@ class TierTest(unittest.TestCase):
self.assertEqual(game.build_cost("walls", 5)["mithril_brackets"], 60)
self.assertEqual(game.build_cost("walls", 5)["steel_bar"], 180)
def test_dragons_drop_dragonhide(self):
def test_dragons_drop_their_areas_reagents(self):
world = world_with(("wil", "commoner"))
p = world["players"]["wil"]
p["xp"]["combat"] = game.XP_TABLE[80]
@ -767,7 +771,7 @@ class TierTest(unittest.TestCase):
p["bank"] = {"cooked_swordfish": 200}
game.start_action(world, p, "fight_green_dragon", T0)
game.process_player(world, p, T0 + 3600 * 2, random.Random(3))
self.assertGreater(p["bank"].get("green_dragonhide", 0), 30)
self.assertGreater(sum(p["bank"].get(game.reagent(weight, 3), 0) for weight in game.WEIGHTS), 30)
@ -1345,28 +1349,30 @@ class WeaponStyleTest(unittest.TestCase):
p["equip"].update(weapon=weapon, shield=shield, body="iron_body", helmet="iron_helmet", legs="iron_legs")
return world, p
def test_each_trade_fights_its_own_monster_best(self):
def test_no_trade_is_funnelled_into_one_race(self):
# Every race mixes its weaknesses across its variants, so no weapon has one easy target.
styles = {"miner": "iron_warhammer", "builder": "iron_sword", "homesteader": "oak_staff", "ranger": "oak_bow"}
prey = {"miner": "fight_clay_golem", "builder": "fight_cave_troll", "homesteader": "fight_will_o_wisp",
"ranger": "fight_giant_bat"}
races = ["fight_clay_golems", "fight_cave_trolls", "fight_restless_dead", "fight_harpies"]
for cls, weapon in styles.items():
_world, p = self.armed(cls, weapon, "iron_shield" if cls == "builder" else None)
times = {target: game.fight_odds(p, game.ACTION_BY_ID[target])["kill_time"] for target in prey.values()}
self.assertEqual(min(times, key=times.get), prey[cls], cls)
times = [game.fight_odds(p, game.ACTION_BY_ID[race])["kill_time"] for race in races]
self.assertLess(max(times) / min(times), 1.45, cls)
def test_style_traits(self):
_w, swordsman = self.armed("builder", "iron_sword", "iron_shield")
_w, smith = self.armed("miner", "iron_warhammer")
_w, archer = self.armed("ranger", "oak_bow")
spec = lambda p: game.fighter(p, game.NO_BLESSING, {}, game.ACTION_BY_ID["fight_clay_golem"]["enemy"])
brute = game.ACTION_BY_ID["fight_clay_golems"]["enemy"]["variants"][0] # vulnerable to blunt, resists piercing
spec = lambda p: game.fighter(p, game.NO_BLESSING, {}, brute)
self.assertEqual(spec(swordsman)["block"], game.BLOCK_CHANCE)
self.assertEqual(spec(smith)["crit"], 0.10)
self.assertEqual(spec(smith)["weakness"], 1.5)
self.assertEqual(spec(archer)["weakness"], 0.5)
self.assertEqual(spec(smith)["affinity"], game.VULNERABLE)
self.assertEqual(spec(archer)["affinity"], game.RESISTANT)
self.assertEqual(spec(swordsman)["affinity"], 1)
_w, other = self.armed("builder", "iron_warhammer")
self.assertGreater(spec(smith)["max_hit"], spec(other)["max_hit"]) # the trade's own weapon hits harder
world, archer = self.armed("ranger", "oak_bow")
fight = game.simulate_fight(world, archer, game.ACTION_BY_ID["fight_giant_bat"], random.Random(1))
fight = game.simulate_fight(world, archer, game.ACTION_BY_ID["fight_harpies"], random.Random(1))
self.assertEqual(fight["events"][0][1], "you") # the bow strikes first
def test_two_handed_weapons_leave_no_room_for_a_shield(self):
@ -1377,13 +1383,40 @@ class WeaponStyleTest(unittest.TestCase):
def test_weapons_past_copper_need_trophies(self):
for action_id, trophy in (("smith_iron_warhammer", "clay_core"), ("smith_iron_sword", "cave_troll_hide"),
("make_iron_shield", "cave_troll_hide"), ("brew_oak_staff", "wisp_essence"),
("lw_oak_bow", "bat_sinew"), ("smith_rune_warhammer", "runic_core")):
("make_iron_shield", "cave_troll_hide"), ("brew_oak_staff", "grave_dust"),
("lw_oak_bow", "harpy_sinew"), ("smith_rune_warhammer", "magma_core")):
self.assertIn(trophy, game.ACTION_BY_ID[action_id]["inputs"], action_id)
for action_id in ("smith_copper_warhammer", "smith_copper_sword", "make_copper_shield",
"brew_wooden_staff", "lw_wooden_bow"):
self.assertFalse(any(item in game.TROPHIES for item in game.ACTION_BY_ID[action_id]["inputs"]), action_id)
def test_armour_past_copper_needs_its_areas_reagent(self):
self.assertEqual(game.ACTION_BY_ID["smith_iron_body"]["inputs"], {"iron_bar": 5, "watchtower_plating": 3})
self.assertEqual(game.ACTION_BY_ID["lw_hard_leather_cap"]["inputs"], {"hard_leather": 2, "watchtower_lacing": 1})
self.assertEqual(game.ACTION_BY_ID["sew_moonweave_legs"]["inputs"], {"moonweave": 3, "ironhill_thread": 2})
for action_id in ("smith_copper_body", "lw_cap", "sew_linen_body"):
self.assertFalse(any(item in game.REAGENTS for item in game.ACTION_BY_ID[action_id]["inputs"]), action_id)
harpies = game.ACTION_BY_ID["fight_harpies"]["drops"]
self.assertIn(["watchtower_thread", 1, game.REAGENT_ODDS], harpies)
def test_heavy_stops_blows_light_stops_magic(self):
_w, plate = self.armed("miner", "iron_warhammer")
_w, cloth = self.armed("miner", "iron_warhammer")
cloth["equip"].update(helmet="cotton_hood", body="cotton_body", legs="cotton_legs")
self.assertGreater(game.armour(plate), game.armour(cloth))
self.assertGreater(game.magic_ward(cloth), game.magic_ward(plate))
shaper = next(v for v in game.ACTION_BY_ID["fight_clay_golems"]["enemy"]["variants"] if v["name"] == "Mud shaper")
brute = game.ACTION_BY_ID["fight_clay_golems"]["enemy"]["variants"][0]
hurt = lambda p, foe: game._variant_odds(p, foe, game.NO_BLESSING, {})["damage"]
self.assertLess(hurt(cloth, shaper), hurt(plate, shaper))
self.assertLess(hurt(plate, brute), hurt(cloth, brute))
def test_each_kill_is_one_of_the_variants(self):
world, p = self.armed("miner", "iron_warhammer", level=30)
seen = {game.simulate_fight(world, p, game.ACTION_BY_ID["fight_goblins"], random.Random(seed))["enemy"]
for seed in range(60)}
self.assertEqual(seen, {"Goblin brute", "Goblin rogue", "Goblin warlock"})
def test_shield_smiths_stop_after_the_move(self):
world = world_with(("kay", "miner"))
world["version"] = 13
@ -1463,3 +1496,24 @@ class IconTest(unittest.TestCase):
if __name__ == "__main__":
unittest.main()
class CombatReworkMigrationTest(unittest.TestCase):
def test_old_monsters_trophies_and_dragonhide_carry_over(self):
world = world_with(("kay", "ranger"))
world["version"] = 15
p = world["players"]["kay"]
del p["xp"]["tailoring"]
p["bank"] = {"bat_sinew": 3, "harpy_sinew": 2, "green_dragonhide": 4, "red_dragon_leather": 1}
p["equip"]["body"] = "blue_dhide_body"
p["action"] = {"id": "fight_giant_bat", "start": T0}
world["players"]["kay"] = p
game.migrate(world, T0)
self.assertEqual(p["bank"], {"harpy_sinew": 3, "spider_silk": 2, "ironhill_lacing": 4, "mammoth_leather": 1})
self.assertEqual(p["equip"]["body"], "sabrecat_leather_body")
self.assertEqual(p["action"]["id"], "fight_harpies")
self.assertEqual(p["xp"]["tailoring"], 0)
def test_homesteaders_tailor(self):
self.assertIn("tailoring", game.CLASSES["homesteader"]["fast"])
self.assertIn("tailoring", game.CLASSES["homesteader"]["craft"])