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

691
game.py
View file

@ -64,8 +64,8 @@ ITEMS = {
"bones": {"name": "Bones", "icon": "🦴", "value": 1},
"big_bones": {"name": "Big bones", "icon": "🦴", "value": 4},
"dragon_bones": {"name": "Dragon bones", "icon": "🦴", "value": 15},
"studded_body": {"name": "Studded jerkin", "icon": "🧥", "value": 40, "slot": "body", "armour": 6,
"req": ["combat", 10]},
"studded_body": {"name": "Studded jerkin", "icon": "🧥", "value": 40, "slot": "body", "armour": 5, "ward": 5,
"weight": "medium", "req": ["combat", 10]},
# Building materials from every trade: thatch and flax from the fields, rope
# twisted from flax, glue boiled from hides, and provisions to feed the builders.
"thatch": {"name": "Thatch", "icon": "🌾", "value": 2},
@ -100,6 +100,23 @@ PIECES = [
("legs", "legs", "greaves", 4, 9, None, [3, 6, 9, 12, 16, 21]),
("body", "body", "cuirass", 5, 11, None, [4, 8, 12, 16, 22, 28]),
]
# Armour weights: a piece's rating, split into armour (against physical blows) and
# ward (against magic). Heavy plate from Smithing, medium leather from
# Leatherworking, light cloth from Tailoring; each adds up to about the same.
WEIGHTS = {
"heavy": {"name": "Heavy", "armour": 1.0, "ward": 0.2},
"medium": {"name": "Medium", "armour": 0.6, "ward": 0.6},
"light": {"name": "Light", "armour": 0.2, "ward": 1.0},
}
ARMOUR_RATING = {slot: rating for piece, slot, _noun, _bars, _offset, _power, rating in PIECES if piece != "shield" and rating}
def _armour_stats(weight: str, slot: str, tier: int) -> dict:
rating = ARMOUR_RATING[slot][tier]
return {"armour": round(rating * WEIGHTS[weight]["armour"]), "ward": round(rating * WEIGHTS[weight]["ward"]),
"weight": weight}
PLURALS = {"sword": "swords", "shield": "shields", "helmet": "helmets", "greaves": "greaves", "cuirass": "cuirasses"}
PIECE_ICONS = {"sword": "⚔️", "shield": "🛡️", "helmet": "🪖", "legs": "🦿", "body": "🦾"}
COPPER_ICONS = {"sword": "🗡️", "helmet": "⛑️", "legs": "🦵", "body": "🥋"}
@ -118,22 +135,34 @@ TREES = [
("magic_logs", "Enchanted logs", "🔮", 55, "cut_magic", "Cut enchanted trees"),
]
# raw hide (id, name, icon, value), leather (id, name, value), armour id prefix, armour name prefix,
# leatherworking level, armour by piece
# Every tier's hide comes from Hunting. raw hide (id, name, icon, value), leather (id, name, value),
# armour id prefix, armour name prefix, leatherworking level
LEATHERS = [
(("hide", "Hide", "🟫", 3), ("leather", "Leather", 5), "leather", "Leather", 1, (1, 2, 3)),
(("boar_hide", "Boar hide", "🐗", 8), ("hard_leather", "Hard leather", 12), "hard_leather", "Hard leather", 20, (2, 4, 6)),
(("bear_pelt", "Bear pelt", "🐻", 16), ("bear_leather", "Bear leather", 24), "bear_leather", "Bear leather", 35, (4, 6, 9)),
(("green_dragonhide", "Green dragonhide", "🐉", 35), ("green_dragon_leather", "Green dragon leather", 50),
"green_dhide", "Green dragonhide", 50, (6, 9, 13)),
(("blue_dragonhide", "Blue dragonhide", "🐲", 60), ("blue_dragon_leather", "Blue dragon leather", 85),
"blue_dhide", "Blue dragonhide", 65, (8, 12, 17)),
(("red_dragonhide", "Red dragonhide", "🔥", 100), ("red_dragon_leather", "Red dragon leather", 140),
"red_dhide", "Red dragonhide", 80, (10, 15, 21)),
(("hide", "Hide", "🟫", 3), ("leather", "Leather", 5), "leather", "Leather", 1),
(("boar_hide", "Boar hide", "🐗", 8), ("hard_leather", "Hard leather", 12), "hard_leather", "Hard leather", 20),
(("bear_pelt", "Bear pelt", "🐻", 16), ("bear_leather", "Bear leather", 24), "bear_leather", "Bear leather", 35),
(("elk_hide", "Elk hide", "🦌", 35), ("elk_leather", "Elk leather", 50), "elk_leather", "Elk leather", 50),
(("sabrecat_pelt", "Sabrecat pelt", "🐆", 60), ("sabrecat_leather", "Sabrecat leather", 85),
"sabrecat_leather", "Sabrecat leather", 65),
(("mammoth_hide", "Mammoth hide", "🦣", 100), ("mammoth_leather", "Mammoth leather", 140),
"mammoth_leather", "Mammoth leather", 80),
]
# piece suffix, slot, noun, leather used, level above the tier, icon
LEATHER_PIECES = [("cap", "helmet", "cap", 2, 2, "🧢"), ("legs", "legs", "trousers", 3, 7, "👖"),
("body", "body", "jerkin", 4, 11, "🦺")]
# Tailoring: a fibre from Farming, the cloth woven from it, and the light armour sewn from that.
# fibre (id, name, icon, value), farming level, seconds, xp, cloth (id, name, value), armour id prefix, armour name prefix
CLOTHS = [
(("flax", "Flax", "🌿", 2), 1, 35, 11, ("linen", "Linen", 5), "linen", "Linen"),
(("cotton", "Cotton", "☁️", 4), 15, 45, 20, ("cotton_cloth", "Cotton cloth", 12), "cotton", "Cotton"),
(("silk_cocoons", "Silk cocoons", "🐛", 8), 30, 55, 32, ("silk", "Silk", 24), "silk", "Silk"),
(("moonpetal", "Moonpetal", "🌙", 15), 45, 60, 45, ("moonweave", "Moonweave", 45), "moonweave", "Moonweave"),
(("starbloom", "Starbloom", "⭐", 28), 60, 70, 60, ("starweave", "Starweave", 80), "starweave", "Starweave"),
(("spiritvine", "Spiritvine", "🌀", 50), 75, 80, 80, ("spiritweave", "Spiritweave", 135), "spiritweave", "Spiritweave"),
]
FIBRE_VERBS = {"silk_cocoons": "Raise silkworms"}
CLOTH_PIECES = [("hood", "helmet", "hood", 2, 2, "🧢"), ("legs", "legs", "leggings", 3, 7, "👖"),
("body", "body", "robe", 4, 11, "👘")]
# raw (id, name, icon, value), cooked (id, name, icon, value, heal, food), fishing level, fish action, verb
FISH = [
@ -159,8 +188,10 @@ for _tier, (_key, _name, _ore, _coal, _bar_icon, _bar_value, _smithy, _wood) in
"req": ["combat", EQUIP_LEVELS[_tier]]}
if _power:
_item["power"] = _power[_tier]
if _armour:
if _armour and _piece == "shield":
_item["armour"] = _armour[_tier]
elif _armour:
_item.update(_armour_stats("heavy", _slot, _tier))
ITEMS[f"{_key}_{_piece}"] = {k: v for k, v in _item.items() if v is not None}
for _skill, (_tool, _icon) in TOOLS.items():
ITEMS[f"{_key}_{_tool.replace(' ', '_')}"] = {
@ -202,16 +233,26 @@ for _tier, _metal in enumerate(METALS):
ITEMS[f"{WOODS[_tier]}_staff"] = {"name": STAVES[_tier], "plural": STAVES[_tier].replace("staff", "staves"),
"icon": "🪄", "style": "staff", **_weapon}
ITEMS[f"{WOODS[_tier]}_bow"] = {"name": BOWS[_tier], "icon": "🏹", "style": "bow", **_weapon}
for _tier, (_raw, _leather, _prefix, _label, _level, _armour) in enumerate(LEATHERS):
for _tier, (_raw, _leather, _prefix, _label, _level) in enumerate(LEATHERS):
ITEMS[_raw[0]] = {"name": _raw[1], "icon": _raw[2], "value": _raw[3]}
ITEMS[_leather[0]] = {"name": _leather[1], "icon": "🪢", "value": _leather[2]}
for (_suffix, _slot, _noun, _used, _offset, _icon), _points in zip(LEATHER_PIECES, _armour):
for _suffix, _slot, _noun, _used, _offset, _icon in LEATHER_PIECES:
ITEMS[f"{_prefix}_{_suffix}"] = {
"name": f"{_label} {_noun}", "icon": _icon, "value": round(_used * _leather[2] * 1.3),
"slot": _slot, "armour": _points, "tier": _tier, "req": ["combat", EQUIP_LEVELS[_tier]],
"slot": _slot, "tier": _tier, "req": ["combat", EQUIP_LEVELS[_tier]], **_armour_stats("medium", _slot, _tier),
}
if _noun == "trousers":
ITEMS[f"{_prefix}_{_suffix}"]["plural"] = ITEMS[f"{_prefix}_{_suffix}"]["name"]
for _tier, (_fibre, _level, _time, _xp, _cloth, _prefix, _label) in enumerate(CLOTHS):
ITEMS.setdefault(_fibre[0], {"name": _fibre[1], "icon": _fibre[2], "value": _fibre[3]})
ITEMS[_cloth[0]] = {"name": _cloth[1], "icon": "🧶", "value": _cloth[2]}
for _suffix, _slot, _noun, _used, _offset, _icon in CLOTH_PIECES:
ITEMS[f"{_prefix}_{_suffix}"] = {
"name": f"{_label} {_noun}", "icon": _icon, "value": round(_used * _cloth[2] * 1.3),
"slot": _slot, "tier": _tier, "req": ["combat", EQUIP_LEVELS[_tier]], **_armour_stats("light", _slot, _tier),
}
if _noun == "leggings":
ITEMS[f"{_prefix}_{_suffix}"]["plural"] = ITEMS[f"{_prefix}_{_suffix}"]["name"]
for _raw, _cooked, _level, _action_id, _verb in FISH:
ITEMS[_raw[0]] = {"name": _raw[1], "icon": _raw[2], "value": _raw[3]}
ITEMS[_cooked[0]] = {"name": _cooked[1], "icon": _cooked[2], "value": _cooked[3], "heal": _cooked[4], "food": _cooked[5]}
@ -245,12 +286,14 @@ POTION_LINES = [
("fishing", "Fishing potion", "fishing", "speed", SPEEDS, [fish[0][0] for fish in FISH]),
("farming", "Farming potion", "farming", "speed", SPEEDS,
["wheat", "vegetables", "guam_leaf", "marrentill", "tarromin", "harralander"]),
("hunting", "Hunting potion", "hunting", "speed", SPEEDS[:4], ["feathers", "raw_meat", "boar_hide", "bear_pelt"]),
("hunting", "Hunting potion", "hunting", "speed", SPEEDS,
["feathers", "raw_meat", "boar_hide", "bear_pelt", "elk_hide", "sabrecat_pelt"]),
("construction", "Construction potion", "construction", "speed", SPEEDS, [plank for plank, _name in PLANKS]),
("smithing", "Smithing potion", "smithing", "keep", SPEEDS, [f"{metal[0]}_bar" for metal in METALS]),
("cooking", "Cooking potion", "cooking", "keep", SPEEDS, [fish[1][0] for fish in FISH]),
("leatherworking", "Leatherworking potion", "leatherworking", "keep", SPEEDS,
[leather[1][0] for leather in LEATHERS]),
("tailoring", "Tailoring potion", "tailoring", "keep", SPEEDS, [cloth[4][0] for cloth in CLOTHS]),
("prayer", "Prayer potion", "prayer", "bones", [0.25, 0.5, 0.75], ["bones", "big_bones", "dragon_bones"]),
("strength", "Strength potion", "combat", "power", [4, 7, 11, 15, 20, 26], [leather[0][0] for leather in LEATHERS]),
("defence", "Defence potion", "combat", "reduce", [0.10, 0.14, 0.18, 0.22, 0.26, 0.30],
@ -312,6 +355,7 @@ SKILLS = [
{"id": "construction", "name": "Construction", "icon": "🏗️", "group": "Crafting"},
{"id": "cooking", "name": "Cooking", "icon": "🍳", "group": "Crafting"},
{"id": "leatherworking", "name": "Leatherworking", "icon": "🪡", "group": "Crafting"},
{"id": "tailoring", "name": "Tailoring", "icon": "🧵", "group": "Crafting"},
{"id": "herblore", "name": "Herblore", "icon": "🧪", "group": "Crafting"},
{"id": "combat", "name": "Combat", "icon": "⚔️", "group": "Combat"},
{"id": "prayer", "name": "Prayer", "icon": "🙏", "group": "Combat"},
@ -319,7 +363,7 @@ SKILLS = [
]
SKILL_IDS = [skill["id"] for skill in SKILLS]
GATHERING = {"mining", "woodcutting", "farming", "hunting", "fishing"}
CRAFTING = {"smithing", "construction", "cooking", "leatherworking", "herblore"}
CRAFTING = {"smithing", "construction", "cooking", "leatherworking", "herblore", "tailoring"}
# A class is a whole trade, from gathering to the finished goods: its skills are
# 20% faster, 1 in 10 of its crafts make double, and it has one bonus of its own.
@ -340,9 +384,10 @@ CLASSES = {
"combat": "Sword: +15% damage",
},
"homesteader": {
"name": "Homesteader", "icon": "🌾", "fast": ["cooking", "herblore"], "craft": ["cooking", "herblore"],
"bonus": "Cooking and herblore 20% faster, two farm plots",
"masterwork": "Masterwork: 10% double cooking and herblore crafts",
"name": "Homesteader", "icon": "🌾", "fast": ["cooking", "herblore", "tailoring"],
"craft": ["cooking", "herblore", "tailoring"],
"bonus": "Cooking, herblore and tailoring 20% faster, two farm plots",
"masterwork": "Masterwork: 10% double cooking, herblore and tailoring crafts",
"signature": "Steady hand: potions last 25% longer",
"combat": "Staff: +15% damage",
},
@ -391,13 +436,16 @@ def _tiered_actions() -> list[dict]:
for piece, _slot, noun, bars, offset, _power, _armour in PIECES:
if piece == "shield":
continue # builders make shields now, in Construction
inputs = {f"{key}_bar": bars, **({trophy("troll", tier): tier} if piece == "sword" and tier else {})}
inputs = {f"{key}_bar": bars}
if tier:
inputs.update({trophy("sword", tier): tier} if piece == "sword" else
{reagent("heavy", tier): REAGENT_COUNTS[_slot]})
actions.append(_action(f"smith_{key}_{piece}", "smithing", f"{name} {noun}", TIER_LEVELS[tier] + offset,
25 + 5 * bars + 2 * tier, bars * piece_xp[tier], inputs,
{f"{key}_{piece}": 1}, group=name, city=city))
actions.append(_action(f"smith_{key}_warhammer", "smithing", f"{name} warhammer", TIER_LEVELS[tier] + 6,
45 + 2 * tier, 4 * piece_xp[tier],
{f"{key}_bar": 3, PLANKS[tier][0]: 1, **({trophy("golem", tier): 2 + tier} if tier else {})},
{f"{key}_bar": 3, PLANKS[tier][0]: 1, **({trophy("hammer", tier): 2 + tier} if tier else {})},
{f"{key}_warhammer": 1}, group=name, city=city))
for tool, _icon in TOOLS.values():
tool_id = f"{key}_{tool.replace(' ', '_')}"
@ -407,14 +455,26 @@ def _tiered_actions() -> list[dict]:
for tier, (log, _name, _icon, _value, action_id, verb) in enumerate(TREES):
actions.append(_action(action_id, "woodcutting", verb, TIER_LEVELS[tier], [20, 35, 40, 50, 60, 75][tier],
[5, 15, 24, 36, 52, 75][tier], outputs={log: 1}))
for tier, (raw, leather, prefix, label, level, _armour) in enumerate(LEATHERS):
for tier, (raw, leather, prefix, label, level) in enumerate(LEATHERS):
actions.append(_action("tan_leather" if tier == 0 else f"tan_{leather[0]}", "leatherworking",
f"Tan {leather[1].lower()}", level, 25 + 3 * tier, bar_xp[tier],
{raw[0]: 1}, {leather[0]: 1}, group="Tanning"))
for suffix, _slot, noun, used, offset, _icon in LEATHER_PIECES:
for suffix, slot, noun, used, offset, _icon in LEATHER_PIECES:
actions.append(_action(f"lw_{suffix}" if tier == 0 else f"lw_{prefix}_{suffix}", "leatherworking",
f"{label} {noun}", level + offset, 25 + 5 * used + 2 * tier, used * piece_xp[tier],
{leather[0]: used}, {f"{prefix}_{suffix}": 1}, group=label))
{leather[0]: used, **({reagent("medium", tier): REAGENT_COUNTS[slot]} if tier else {})},
{f"{prefix}_{suffix}": 1}, group=label))
for tier, (fibre, level, seconds, fibre_xp, cloth, prefix, label) in enumerate(CLOTHS):
if tier: # flax is grown with the other building materials
actions.append(_action(f"grow_{fibre[0]}", "farming", FIBRE_VERBS.get(fibre[0], f"Grow {fibre[1].lower()}"),
level, seconds, fibre_xp, outputs={fibre[0]: 2}, group="Fibres"))
actions.append(_action(f"weave_{cloth[0]}", "tailoring", f"Weave {cloth[1].lower()}", TIER_LEVELS[tier],
25 + 3 * tier, bar_xp[tier], {fibre[0]: 2}, {cloth[0]: 1}, group="Weaving"))
for suffix, slot, noun, used, offset, _icon in CLOTH_PIECES:
actions.append(_action(f"sew_{prefix}_{suffix}", "tailoring", f"{label} {noun}", TIER_LEVELS[tier] + offset,
25 + 5 * used + 2 * tier, used * piece_xp[tier],
{cloth[0]: used, **({reagent("light", tier): REAGENT_COUNTS[slot]} if tier else {})},
{f"{prefix}_{suffix}": 1}, group=label))
for raw, cooked, level, fish_id, verb in FISH:
tier = TIER_LEVELS.index(level) if level in TIER_LEVELS else 3
actions.append(_action(fish_id, "fishing", verb, level, [25, 35, 45, 55, 65, 80][tier],
@ -426,38 +486,224 @@ def _tiered_actions() -> list[dict]:
# --- combat -----------------------------------------------------------------
# A fight is played out swing by swing: you and the enemy each have HP, a swing
# timer, accuracy and a max hit, and every swing hits, misses or crits. Combat
# is split into the Frontier (bandits and the city's troubles) and expeditions:
# one area per tier, each with a golem, a troll, a spirit and a flyer that drop
# the trophies higher weapons need, and a boss.
# timer, accuracy and a max hit, and every swing hits, misses or crits. Every
# blow has a damage type: weapons deal one, and each enemy is vulnerable to some
# types and resists others. Armour stops physical blows, ward stops magic.
# Combat is split into the Frontier (bandits and the city's troubles) and
# expeditions: one area per tier, each with four races (each feeding one weapon
# line with its trophy, and every armour line with the area's reagents) and a boss.
CRIT_CHANCE = 0.05
CRIT_MULT = 1.5
RESPAWN = 2 # seconds after a kill before the next enemy turns up
MAX_FIGHT = 600 # a fight that drags on longer is broken off
FLEE_BELOW = 0.2 # out of food and under this share of HP: get out
DAMAGE_TYPES = {
"slash": {"name": "Slashing", "glyph": "sword-slice", "tint": "#d4dae2"},
"blunt": {"name": "Blunt", "glyph": "hammer-drop", "tint": "#d1a46a"},
"pierce": {"name": "Piercing", "glyph": "arrowhead", "tint": "#5fae8a"},
"magic": {"name": "Magic", "glyph": "magic-swirl", "tint": "#a77be8"},
}
VULNERABLE = 1.25 # damage an enemy takes from a type it's vulnerable to
RESISTANT = 0.75 # ...and from one it resists
# How each kind of weapon fights. Two-handed weapons carry the shield's power themselves.
STYLES = {
"sword": {"name": "sword", "interval": 2.4, "hit": 1.0, "accuracy": 1.0, "crit": 0.05},
"hammer": {"name": "warhammer", "interval": 3.6, "hit": 1.6, "accuracy": 0.95, "crit": 0.10},
"staff": {"name": "staff", "interval": 3.0, "hit": 1.15, "accuracy": 1.25, "crit": 0.05, "pierce": True},
"bow": {"name": "bow", "interval": 1.8, "hit": 0.75, "accuracy": 1.05, "crit": 0.05, "first": True},
"sword": {"name": "sword", "damage": "slash", "interval": 2.4, "hit": 1.0, "accuracy": 1.0, "crit": 0.05},
"hammer": {"name": "warhammer", "damage": "blunt", "interval": 3.6, "hit": 1.6, "accuracy": 0.95, "crit": 0.10},
"staff": {"name": "staff", "damage": "magic", "interval": 3.0, "hit": 1.2, "accuracy": 1.25, "crit": 0.05},
"bow": {"name": "bow", "damage": "pierce", "interval": 1.8, "hit": 0.75, "accuracy": 1.05, "crit": 0.05, "first": True},
}
CLASS_STYLE = {"miner": "hammer", "builder": "sword", "homesteader": "staff", "ranger": "bow"}
CLASS_STYLE_BONUS = 1.15 # damage with your own trade's weapon
BLOCK_CHANCE = 0.15 # a shield turns aside this share of hits
# Damage each style does to each kind of monster (1 when not listed).
WEAKNESS = {
"golem": {"hammer": 1.5, "bow": 0.5},
"troll": {"sword": 1.5},
"spirit": {"hammer": 0.5, "sword": 0.5, "bow": 0.5, "staff": 1.5},
"flyer": {"hammer": 0.5, "sword": 0.5, "bow": 1.5},
# Each armour piece past copper needs reagents from its tier's expedition area.
REAGENT_COUNTS = {"helmet": 1, "legs": 2, "body": 3}
REAGENT_NOUNS = {"heavy": "plating", "medium": "lacing", "light": "thread"}
REAGENT_ODDS = 0.10 # each of the area's three reagents, from any of its races
TROPHY_ODDS = 0.03 # rare: higher-tier weapons are hunted for
BOSS_TROPHY_ODDS = 0.06 # each of the four lines, so a boss is worth the longer fight
def _slug(name: str) -> str:
return "".join(c if c.isalnum() else "_" for c in name.lower().replace("'", "")).strip("_")
# Archetypes: HP, seconds between swings, accuracy, evasion, max hit (multipliers), how often it turns up.
ARCHETYPES = {
"brute": (1.3, 3.0, 1.0, 0.85, 1.2, 4),
"skirmisher": (0.85, 2.2, 1.1, 1.25, 0.85, 4),
"caster": (0.85, 2.6, 1.15, 1.0, 1.25, 3),
"elite": (1.7, 2.8, 1.1, 1.1, 1.3, 1),
"boss": (1.4, 3.0, 1.0, 1.0, 1.35, 1),
"dragon": (2.5, 3.0, 1.1, 1.0, 1.5, 1),
}
def trophy(kind: str, tier: int) -> str:
"""The trophy a tier's monster of this kind drops (tier 1 is the Old Watchtower)."""
return _slug(AREA_MONSTERS[kind][2][tier - 1])
def _types(value) -> list:
return [value] if isinstance(value, str) else list(value)
def monster(name: str, archetype: str, level: int, attack, vulnerable=(), resists=(), boss: bool = False) -> dict:
hp, interval, accuracy, evasion, hit, weight = ARCHETYPES[archetype]
hp = round((20 + 6 * level) * hp * (2.5 if boss else 1))
return {"name": name, "archetype": archetype, "level": level, "boss": boss, "weight": weight,
"hp": hp, "interval": interval, "xp": round(hp * 0.4 + level * 1.5),
"accuracy": round((12 + 2.2 * level) * accuracy), "evasion": round((10 + 2 * level) * evasion),
"max_hit": max(1, round((2 + level / 3) * hit * (1.2 if boss else 1))),
"attack": _types(attack), "vulnerable": _types(vulnerable), "resists": _types(resists)}
def _encounter(id: str, name: str, group: str, level: int, roster: list, gold: int = 0, drops=(),
boss: bool = False, **extra) -> dict:
"""A combat action: each kill is one of the roster's variants, picked by how often each turns up.
An elite is a couple of levels above the rest."""
variants = [monster(vname, archetype, level + (2 if archetype == "elite" else 0), attack, vulnerable, resists, boss)
for vname, archetype, attack, vulnerable, resists in roster]
total = sum(v["weight"] for v in variants)
xp = round(sum(v["xp"] * v["weight"] for v in variants) / total)
enemy = {"name": name, "level": level, "boss": boss, "variants": variants}
return _action(id, "combat", extra.pop("label", None) or name, level, RESPAWN, xp, gold=gold,
drops=[list(drop) for drop in drops], enemy=enemy, group=group, **extra)
def _boss(id: str, name: str, group: str, level: int, archetype: str, attack, vulnerable, resists, **extra) -> dict:
return _encounter(id, name, group, level, [(name, archetype, attack, vulnerable, resists)],
boss=extra.pop("is_boss", True), **extra)
# Rosters: (variant, archetype, attacks with, vulnerable to, resists).
BANDITS = [("Bandit brute", "brute", "blunt", "slash", "blunt"),
("Bandit archer", "skirmisher", "pierce", "blunt", "pierce"),
("Hedge mage", "caster", "magic", "pierce", "magic")]
WOLVES = [("Grey wolf", "skirmisher", "slash", "pierce", "blunt"),
("Timber wolf", "brute", "slash", "slash", "pierce"),
("Wolf alpha", "elite", "slash", "blunt", "slash")]
GOBLINS = [("Goblin brute", "brute", "blunt", "blunt", "slash"),
("Goblin rogue", "skirmisher", "pierce", "magic", "blunt"),
("Goblin warlock", "caster", "magic", "slash", "magic")]
# Expedition areas, tiers II to VI: name, level, the reagent prefix, and four races.
# Each race: (name, trophy, weapon line its trophy goes into, level offset, roster).
AREAS = [
("Old Watchtower", 10, "Watchtower", [
("Clay golems", "Clay core", "hammer", 0, [
("Clay brute", "brute", "blunt", "blunt", "pierce"),
("Mud shaper", "caster", "magic", "pierce", "magic"),
("Clay colossus", "elite", "blunt", "magic", "blunt")]),
("Cave trolls", "Cave troll hide", "sword", 2, [
("Troll bruiser", "brute", "blunt", "slash", "blunt"),
("Troll rockhurler", "skirmisher", "pierce", "blunt", "pierce"),
("Troll hexer", "caster", "magic", "pierce", "slash")]),
("Restless dead", "Grave dust", "staff", 1, [
("Skeleton warrior", "brute", "slash", "blunt", "pierce"),
("Wisp", "skirmisher", "magic", "magic", "slash"),
("Necromancer", "caster", "magic", "slash", "blunt")]),
("Harpies", "Harpy sinew", "bow", 0, [
("Harpy slasher", "skirmisher", "slash", "pierce", "blunt"),
("Harpy windcaster", "caster", "magic", ("slash", "pierce"), "magic"),
("Harpy matriarch", "elite", "slash", "magic", "pierce")]),
]),
("Sunken Barrows", 20, "Barrow", [
("Stone guardians", "Stone core", "hammer", 0, [
("Stone guardian", "brute", "blunt", "blunt", "slash"),
("Barrow sentry", "skirmisher", "pierce", "magic", "pierce"),
("Runebound guardian", "elite", "magic", "slash", "magic")]),
("Lizardfolk", "Lizard scale", "sword", 2, [
("Lizard warrior", "brute", "slash", "blunt", "slash"),
("Lizard spearman", "skirmisher", "pierce", "slash", "pierce"),
("Lizard shaman", "caster", "magic", "pierce", "magic")]),
("Bog wraiths", "Wraith essence", "staff", 1, [
("Bog wraith", "caster", "magic", "magic", "slash"),
("Drowned knight", "brute", "slash", "blunt", "pierce"),
("Marsh wisp", "skirmisher", "magic", "pierce", "blunt")]),
("Giant spiders", "Spider silk", "bow", 0, [
("Spider hunter", "skirmisher", "pierce", "slash", "pierce"),
("Venom weaver", "caster", "magic", "blunt", "magic"),
("Brood mother", "elite", "pierce", "magic", "blunt")]),
]),
("Iron Hills", 30, "Ironhill", [
("Iron golems", "Iron core", "hammer", 0, [
("Iron golem", "brute", "blunt", "blunt", "slash"),
("Forge golem", "caster", "magic", "pierce", "magic"),
("Iron colossus", "elite", "blunt", "magic", "pierce")]),
("Orcs", "Orc tusk", "sword", 2, [
("Orc berserker", "brute", "slash", "pierce", "blunt"),
("Orc archer", "skirmisher", "pierce", "slash", "pierce"),
("Orc shaman", "caster", "magic", "blunt", "magic")]),
("Mine ghosts", "Ghost essence", "staff", 1, [
("Lost miner", "brute", "blunt", "magic", "slash"),
("Lantern ghost", "caster", "magic", "blunt", "magic"),
("Shaft howler", "skirmisher", "magic", "pierce", "blunt")]),
("Wyverns", "Wyvern sinew", "bow", 0, [
("Wyvern", "brute", "slash", "pierce", "slash"),
("Wyvern hatchling", "skirmisher", "pierce", "blunt", "magic"),
("Venomspitter", "caster", "magic", "slash", "pierce")]),
]),
("Frostpeak", 40, "Frostpeak", [
("Ice elementals", "Frozen core", "hammer", 0, [
("Ice brute", "brute", "blunt", "blunt", "pierce"),
("Frost wisp", "skirmisher", "magic", "slash", "magic"),
("Glacier colossus", "elite", "blunt", "magic", "slash")]),
("Frost trolls", "Frost troll hide", "sword", 2, [
("Frost troll", "brute", "blunt", "slash", "blunt"),
("Troll icehurler", "skirmisher", "pierce", "magic", "pierce"),
("Troll rimecaller", "caster", "magic", "blunt", "magic")]),
("Banshees", "Banshee essence", "staff", 1, [
("Banshee", "caster", "magic", "magic", "slash"),
("Wailing knight", "brute", "slash", "blunt", "pierce"),
("Keening shade", "skirmisher", "magic", "pierce", "blunt")]),
("Griffins", "Griffin sinew", "bow", 0, [
("Griffin", "brute", "slash", "pierce", "blunt"),
("Griffin rider", "skirmisher", "pierce", "slash", "pierce"),
("Storm griffin", "caster", "magic", "blunt", "magic")]),
]),
("Dragon's Spine", 50, "Dragonspine", [
("Magma constructs", "Magma core", "hammer", 0, [
("Magma brute", "brute", "blunt", "blunt", "slash"),
("Cinder golem", "skirmisher", "pierce", "magic", "pierce"),
("Fire binder", "caster", "magic", "pierce", "magic")]),
("Dragonkin", "Dragonkin scale", "sword", 2, [
("Drakeguard", "brute", "slash", "blunt", "slash"),
("Drake lancer", "skirmisher", "pierce", "magic", "pierce"),
("Flame priest", "caster", "magic", "slash", "magic")]),
("Shades", "Shade essence", "staff", 1, [
("Shade", "caster", "magic", "magic", "pierce"),
("Shadow blade", "skirmisher", "slash", "blunt", "slash"),
("Void knight", "elite", "magic", "pierce", "blunt")]),
("Rocs", "Roc sinew", "bow", 0, [
("Roc", "brute", "slash", "pierce", "blunt"),
("Roc hatchling", "skirmisher", "pierce", "slash", "pierce"),
("Thunder roc", "caster", "magic", "blunt", "magic")]),
]),
]
AREA_BONES = ["bones", "big_bones", "big_bones", "dragon_bones", "dragon_bones"]
TROPHIES = {} # id: (name, weapon line, tier)
REAGENTS = {} # id: (weight, tier)
for _tier, (_area, _level, _prefix, _races) in enumerate(AREAS, start=1):
for _race, _trophy, _line, _offset, _roster in _races:
TROPHIES[_slug(_trophy)] = (_trophy, _line, _tier)
ITEMS[_slug(_trophy)] = {"name": _trophy, "icon": "🏆", "value": 15 * _tier, "trophy": _line, "tier": _tier}
for _weight, _noun in REAGENT_NOUNS.items():
_id = f"{_slug(_prefix)}_{_noun}"
REAGENTS[_id] = (_weight, _tier)
ITEMS[_id] = {"name": f"{_prefix} {_noun}", "icon": "✨", "value": 12 * _tier, "reagent": _weight, "tier": _tier}
def trophy(line: str, tier: int) -> str:
"""The trophy for a weapon line from a tier's area (tier 1 is the Old Watchtower)."""
return next(item for item, (_name, kind, t) in TROPHIES.items() if kind == line and t == tier)
def reagent(weight: str, tier: int) -> str:
"""The reagent an armour weight needs at a tier, from that tier's area."""
return next(item for item, (kind, t) in REAGENTS.items() if kind == weight and t == tier)
def _area_drops(tier: int, odds: float = REAGENT_ODDS) -> list[tuple]:
return [(reagent(weight, tier), 1, odds) for weight in WEIGHTS]
def _weapon_actions() -> list[dict]:
@ -465,118 +711,69 @@ def _weapon_actions() -> list[dict]:
actions = []
for tier, (key, name, *_rest) in enumerate(METALS):
plank, leather, herb = PLANKS[tier][0], LEATHERS[tier][1][0], HERBS[tier][0]
extra = lambda kind, count: {trophy(kind, tier): count} if tier else {}
extra = lambda line, count: {trophy(line, tier): count} if tier else {}
actions.append(_action(f"make_{key}_shield", "construction", f"{name} shield", TIER_LEVELS[tier] + 5,
40 + 2 * tier, [10, 22, 36, 52, 70, 92][tier],
{plank: 3, leather: 2, f"{key}_bar": 1, **extra("troll", 1 + tier)},
{plank: 3, leather: 2, f"{key}_bar": 1, **extra("sword", 1 + tier)},
{f"{key}_shield": 1}, group="Shields"))
actions.append(_action(f"brew_{WOODS[tier]}_staff", "herblore", STAVES[tier], TIER_LEVELS[tier] + 5,
45 + 2 * tier, [12, 26, 42, 60, 80, 105][tier],
{plank: 2, herb: 3, **extra("spirit", 2 + tier)}, {f"{WOODS[tier]}_staff": 1},
{plank: 2, herb: 3, **extra("staff", 2 + tier)}, {f"{WOODS[tier]}_staff": 1},
group="Staves"))
actions.append(_action(f"lw_{WOODS[tier]}_bow", "leatherworking", BOWS[tier], LEATHERS[tier][4] + 4,
40 + 2 * tier, [12, 26, 42, 60, 80, 105][tier],
{plank: 2, leather: 2, **extra("flyer", 2 + tier)}, {f"{WOODS[tier]}_bow": 1},
{plank: 2, leather: 2, **extra("bow", 2 + tier)}, {f"{WOODS[tier]}_bow": 1},
group="Bows"))
return actions
# kind: HP, seconds between swings, accuracy, evasion, max hit (multipliers), damage it shrugs off
MONSTER_KINDS = {
"human": (1.0, 2.6, 1.0, 1.0, 1.0, 0.0),
"beast": (0.9, 2.2, 1.0, 1.0, 0.9, 0.0),
"golem": (1.5, 3.2, 1.0, 0.7, 1.25, 0.2),
"troll": (1.4, 3.0, 0.9, 1.0, 1.4, 0.0),
"spirit": (0.8, 2.4, 1.0, 1.35, 0.9, 0.0),
"flyer": (0.8, 2.0, 1.0, 1.2, 0.8, 0.0),
"dragon": (2.5, 3.0, 1.1, 1.0, 1.5, 0.15),
}
def monster(name: str, kind: str, level: int, boss: bool = False) -> dict:
hp, interval, accuracy, evasion, hit, shrug = MONSTER_KINDS[kind]
return {"name": name, "kind": kind, "level": level, "boss": boss,
"hp": round((20 + 6 * level) * hp * (2.5 if boss else 1)), "interval": interval,
"accuracy": round((12 + 2.2 * level) * accuracy), "evasion": round((10 + 2 * level) * evasion),
"max_hit": max(1, round((2 + level / 3) * hit * (1.2 if boss else 1))), "reduction": shrug}
def _fight(id: str, enemy: dict, group: str, gold: int = 0, drops=(), name: str | None = None, **extra) -> dict:
"""A combat action: XP per kill, needs the enemy's level in combat."""
xp = round(enemy["hp"] * 0.4 + enemy["level"] * 1.5)
return _action(id, "combat", name or enemy["name"], enemy["level"], RESPAWN, xp, gold=gold,
drops=[list(drop) for drop in drops], enemy=enemy, group=group, **extra)
# Expedition areas, tiers II to VI: name, level, and the trophy each kind of monster drops there.
AREAS = ["Old Watchtower", "Sunken Barrows", "Iron Hills", "Frostpeak", "Dragon's Spine"]
AREA_LEVELS = [10, 20, 30, 40, 50]
AREA_MONSTERS = { # kind: (level offset, monsters, trophies)
"golem": (0, ["Clay golem", "Stone golem", "Iron golem", "Obsidian golem", "Runic golem"],
["Clay core", "Stone core", "Iron core", "Obsidian core", "Runic core"]),
"troll": (2, ["Cave troll", "Hill troll", "Rock troll", "Frost troll", "Elder troll"],
["Cave troll hide", "Hill troll hide", "Rock troll hide", "Frost troll hide", "Elder troll hide"]),
"spirit": (1, ["Will-o'-wisp", "Bog wraith", "Mine ghost", "Banshee", "Shade"],
["Wisp essence", "Wraith essence", "Ghost essence", "Banshee essence", "Shade essence"]),
"flyer": (0, ["Giant bat", "Harpy", "Wyvern", "Griffin", "Roc"],
["Bat sinew", "Harpy sinew", "Wyvern sinew", "Griffin sinew", "Roc sinew"]),
}
TROPHY_ODDS = 0.03 # rare: higher-tier weapons are hunted for
BOSS_TROPHY_ODDS = 0.06 # each of the four kinds, so a boss is worth the longer fight
AREA_BONES = ["bones", "big_bones", "big_bones", "dragon_bones", "dragon_bones"]
def _slug(name: str) -> str:
return "".join(c if c.isalnum() else "_" for c in name.lower().replace("'", "")).strip("_")
TROPHIES = {} # id: (name, kind, tier)
for _kind, (_offset, _monsters, _trophies) in AREA_MONSTERS.items():
for _tier, _trophy in enumerate(_trophies, start=1):
TROPHIES[_slug(_trophy)] = (_trophy, _kind, _tier)
ITEMS[_slug(_trophy)] = {"name": _trophy, "icon": "🏆", "value": 15 * _tier, "trophy": _kind, "tier": _tier}
def _area_fights() -> list[dict]:
fights = []
for index, (area, level) in enumerate(zip(AREAS, AREA_LEVELS)):
for kind, (offset, monsters, trophies) in AREA_MONSTERS.items():
enemy = monster(monsters[index], kind, level + offset)
fights.append(_fight(f"fight_{_slug(monsters[index])}", enemy, area, gold=level + offset,
drops=[(AREA_BONES[index], 1, 1.0), (_slug(trophies[index]), 1, TROPHY_ODDS)]))
for tier, (area, level, _prefix, races) in enumerate(AREAS, start=1):
for race, trophy_name, _line, offset, roster in races:
fights.append(_encounter(f"fight_{_slug(race)}", race, area, level + offset, roster, gold=level + offset,
drops=[(AREA_BONES[tier - 1], 1, 1.0), (_slug(trophy_name), 1, TROPHY_ODDS),
*_area_drops(tier)]))
return fights
def _boss_trophies(tier: int) -> list[tuple]:
return [(_slug(trophies[tier - 1]), 1, BOSS_TROPHY_ODDS) for _offset, _monsters, trophies in AREA_MONSTERS.values()]
def _boss_drops(tier: int, reagents: float = 0.3) -> list[tuple]:
return [*[(trophy(line, tier), 1, BOSS_TROPHY_ODDS) for line in STYLES], *_area_drops(tier, reagents)]
COMBAT_ACTIONS = [
# The Frontier: the city's own troubles.
_fight("fight_scouts", monster("Bandit scouts", "human", 10), "Frontier", gold=10,
drops=[("copper_bar", 1, 0.3), ("arrows", 5, 0.4), ("bones", 1, 1.0)]),
_fight("sabotage", monster("Camp guards", "human", 12), "Frontier", name="Sabotage the bandit camp",
special="sabotage"),
_fight("fight_stronghold", monster("Bandit stronghold", "human", 40, boss=True), "Frontier", gold=60,
drops=[("steel_bar", 1, 0.4), ("coal", 3, 0.5), ("iron_shield", 1, 0.04), ("big_bones", 1, 1.0)]),
_encounter("fight_scouts", "Bandit scouts", "Frontier", 10, BANDITS, gold=10,
drops=[("copper_bar", 1, 0.3), ("arrows", 5, 0.4), ("bones", 1, 1.0)]),
_encounter("sabotage", "Camp guards", "Frontier", 12, BANDITS, label="Sabotage the bandit camp", special="sabotage"),
_boss("fight_stronghold", "Bandit warlord", "Frontier", 40, "boss", ("slash", "magic"), ("blunt", "magic"),
("slash", "pierce"), label="Bandit stronghold", gold=60,
drops=[("steel_bar", 1, 0.4), ("coal", 3, 0.5), ("iron_shield", 1, 0.04), ("big_bones", 1, 1.0)]),
# Greenwood: where everyone starts.
_fight("fight_wolves", monster("Wolves", "beast", 1), "Greenwood", gold=2, name="Wolves in the woods",
drops=[("raw_meat", 1, 0.6), ("hide", 1, 0.5), ("bones", 1, 1.0)]),
_fight("fight_goblins", monster("Cave goblins", "human", 5), "Greenwood", gold=4,
drops=[("bones", 1, 1.0), ("copper_ore", 2, 0.4), ("feathers", 5, 0.4)]),
_encounter("fight_wolves", "Wolves", "Greenwood", 1, WOLVES, gold=2, label="Wolves in the woods",
drops=[("raw_meat", 1, 0.6), ("hide", 1, 0.5), ("bones", 1, 1.0)]),
_encounter("fight_goblins", "Cave goblins", "Greenwood", 5, GOBLINS, gold=4,
drops=[("bones", 1, 1.0), ("copper_ore", 2, 0.4), ("feathers", 5, 0.4)]),
*_area_fights(),
_fight("fight_ruins", monster("Watchtower ogre", "troll", 18, boss=True), "Old Watchtower", gold=25,
drops=[("iron_bar", 2, 0.5), ("iron_sword", 1, 0.05), ("big_bones", 1, 1.0), *_boss_trophies(1)]),
_fight("fight_barrow_king", monster("Barrow king", "spirit", 28, boss=True), "Sunken Barrows", gold=40,
drops=[("steel_bar", 2, 0.5), ("coal", 4, 0.5), ("big_bones", 2, 1.0), *_boss_trophies(2)]),
_fight("fight_green_dragon", monster("Green dragon", "dragon", 38), "Iron Hills", gold=80,
drops=[("green_dragonhide", 1, 0.9), ("mithril_ore", 2, 0.3), ("dragon_bones", 1, 1.0), *_boss_trophies(3)]),
_fight("fight_blue_dragon", monster("Blue dragon", "dragon", 48), "Frostpeak", gold=110,
drops=[("blue_dragonhide", 1, 0.9), ("adamantite_ore", 2, 0.3), ("dragon_bones", 1, 1.0), *_boss_trophies(4)]),
_fight("fight_red_dragon", monster("Red dragon", "dragon", 60), "Dragon's Spine", gold=150,
drops=[("red_dragonhide", 1, 0.9), ("runite_ore", 1, 0.25), ("dragon_bones", 1, 1.0), *_boss_trophies(5)]),
_fight("fight_black_dragon", monster("Black dragon", "dragon", 75, boss=True), "Dragon's Spine", gold=220,
drops=[("red_dragonhide", 2, 0.9), ("rune_bar", 1, 0.1), ("dragon_bones", 2, 1.0), *_boss_trophies(5)]),
_boss("fight_ruins", "Watchtower ogre", "Old Watchtower", 18, "boss", ("blunt", "magic"), ("slash", "magic"),
("blunt", "pierce"), gold=25,
drops=[("iron_bar", 2, 0.5), ("iron_sword", 1, 0.05), ("big_bones", 1, 1.0), *_boss_drops(1)]),
_boss("fight_barrow_king", "Barrow king", "Sunken Barrows", 28, "boss", ("slash", "magic"), ("blunt", "magic"),
("pierce", "slash"), gold=40,
drops=[("steel_bar", 2, 0.5), ("coal", 4, 0.5), ("big_bones", 2, 1.0), *_boss_drops(2)]),
# Dragons: the prize hunt, dropping their area's reagents far more often than anything else there.
_boss("fight_green_dragon", "Green dragon", "Iron Hills", 38, "dragon", ("slash", "magic"), ("pierce", "magic"),
("slash", "blunt"), is_boss=False, gold=80,
drops=[("mithril_ore", 2, 0.3), ("dragon_bones", 1, 1.0), *_boss_drops(3, 0.6)]),
_boss("fight_blue_dragon", "Blue dragon", "Frostpeak", 48, "dragon", ("slash", "magic"), ("blunt", "pierce"),
("magic", "slash"), is_boss=False, gold=110,
drops=[("adamantite_ore", 2, 0.3), ("dragon_bones", 1, 1.0), *_boss_drops(4, 0.6)]),
_boss("fight_red_dragon", "Red dragon", "Dragon's Spine", 60, "dragon", ("slash", "magic"), ("slash", "magic"),
("pierce", "blunt"), is_boss=False, gold=150,
drops=[("runite_ore", 1, 0.25), ("dragon_bones", 1, 1.0), *_boss_drops(5, 0.6)]),
_boss("fight_black_dragon", "Black dragon", "Dragon's Spine", 75, "dragon", ("slash", "magic"), ("blunt", "magic"),
("slash", "pierce"), gold=220,
drops=[("rune_bar", 1, 0.1), ("dragon_bones", 2, 1.0), *_boss_drops(5, 0.8)]),
]
ACTIONS = [
@ -593,6 +790,9 @@ ACTIONS = [
_action("hunt_deer", "hunting", "Hunt deer", 8, 40, 13, outputs={"raw_meat": 2, "hide": 1}),
_action("hunt_boar", "hunting", "Hunt boar", 25, 50, 30, outputs={"raw_meat": 2, "boar_hide": 1}),
_action("hunt_bear", "hunting", "Hunt bears", 40, 60, 48, outputs={"raw_meat": 3, "bear_pelt": 1}),
_action("hunt_elk", "hunting", "Hunt elk", 50, 70, 64, outputs={"raw_meat": 3, "elk_hide": 1}),
_action("hunt_sabrecat", "hunting", "Track sabrecats", 65, 80, 84, outputs={"raw_meat": 3, "sabrecat_pelt": 1}),
_action("hunt_mammoth", "hunting", "Hunt mammoths", 80, 90, 108, outputs={"raw_meat": 5, "mammoth_hide": 1}),
*_tiered_actions(),
_action("smith_arrows", "smithing", "Arrows", 5, 30, 10,
{"copper_bar": 1, "logs": 1, "feathers": 2}, {"arrows": 15}, group="Copper"),
@ -601,7 +801,7 @@ ACTIONS = [
_action("labour", "construction", "Work on the building site", 1, 30, 10, special="labour"),
_action("twist_rope", "construction", "Twist rope", 5, 30, 10, {"flax": 3}, {"rope": 1}, group="Supplies"),
_action("grow_thatch", "farming", "Cut thatch reeds", 3, 30, 7, outputs={"thatch": 3}, group="Materials"),
_action("grow_flax", "farming", "Grow flax", 10, 35, 11, outputs={"flax": 3}, group="Materials"),
_action("grow_flax", "farming", "Grow flax", 1, 35, 11, outputs={"flax": 3}, group="Fibres"),
_action("boil_glue", "leatherworking", "Boil hide glue", 5, 30, 10, {"hide": 1, "logs": 1}, {"hide_glue": 2},
group="Tanning"),
*[_action(f"saw_{plank}", "construction", f"Saw {name.lower()}s", TIER_LEVELS[tier], 25 + 3 * tier,
@ -743,7 +943,7 @@ RAID_TRAITS = ["lightly armoured, heavy on archers", "mounted and fast, few bows
# --- world ---------------------------------------------------------------
WORLD_VERSION = 15
WORLD_VERSION = 16
def new_world(now: float, speed: float = 1.0) -> dict:
@ -878,9 +1078,67 @@ def migrate(world: dict, now: float | None = None) -> dict:
for player in world["players"].values():
player["seen_threat"] = latest["id"] if latest else 0
world["version"] = 15
if world["version"] < 16:
_migrate_combat_rework(world)
world["version"] = 16
return world
# The combat rework: the old area monsters became races, their trophies carry over by
# weapon line and tier, dragonhide became the dragons' reagents, and the leathers past
# bear are hunted now.
RENAMED_ITEMS = {
"bat_sinew": "harpy_sinew", "harpy_sinew": "spider_silk", "wisp_essence": "grave_dust",
"hill_troll_hide": "lizard_scale", "rock_troll_hide": "orc_tusk", "elder_troll_hide": "dragonkin_scale",
"obsidian_core": "frozen_core", "runic_core": "magma_core",
"green_dragonhide": "ironhill_lacing", "blue_dragonhide": "frostpeak_lacing", "red_dragonhide": "dragonspine_lacing",
"green_dragon_leather": "elk_leather", "blue_dragon_leather": "sabrecat_leather", "red_dragon_leather": "mammoth_leather",
**{f"{old}_{suffix}": f"{new}_{suffix}" for old, new in (("green_dhide", "elk_leather"), ("blue_dhide", "sabrecat_leather"),
("red_dhide", "mammoth_leather")) for suffix in ("cap", "legs", "body")},
}
RENAMED_ACTIONS = {
**{f"fight_{_slug(old)}": f"fight_{_slug(new)}" for old, new in (
("Clay golem", "Clay golems"), ("Cave troll", "Cave trolls"), ("Will-o'-wisp", "Restless dead"),
("Giant bat", "Harpies"), ("Stone golem", "Stone guardians"), ("Hill troll", "Lizardfolk"),
("Bog wraith", "Bog wraiths"), ("Harpy", "Giant spiders"), ("Iron golem", "Iron golems"), ("Rock troll", "Orcs"),
("Mine ghost", "Mine ghosts"), ("Wyvern", "Wyverns"), ("Obsidian golem", "Ice elementals"),
("Frost troll", "Frost trolls"), ("Banshee", "Banshees"), ("Griffin", "Griffins"),
("Runic golem", "Magma constructs"), ("Elder troll", "Dragonkin"), ("Shade", "Shades"), ("Roc", "Rocs"))},
**{f"tan_{old}_dragon_leather": f"tan_{new}_leather" for old, new in
(("green", "elk"), ("blue", "sabrecat"), ("red", "mammoth"))},
**{f"lw_{old}_{suffix}": f"lw_{new}_{suffix}" for old, new in (("green_dhide", "elk_leather"),
("blue_dhide", "sabrecat_leather"), ("red_dhide", "mammoth_leather")) for suffix in ("cap", "legs", "body")},
}
def _renamed(stock: dict) -> dict:
out = {}
for item, qty in stock.items():
new = RENAMED_ITEMS.get(item, item)
out[new] = out.get(new, 0) + qty
return out
def _migrate_combat_rework(world: dict) -> None:
city = world["city"]
city["stockpile"] = _renamed(city["stockpile"])
for building in city["buildings"].values():
building["fund"] = _renamed(building["fund"])
for player in world["players"].values():
player["xp"].setdefault("tailoring", 0)
player["bank"] = _renamed(player["bank"])
for slot, item in list(player["equip"].items()):
if item:
player["equip"][slot] = RENAMED_ITEMS.get(item, item)
action = player["action"]
if action:
for key in ("id", "resume"):
if key in action:
action[key] = RENAMED_ACTIONS.get(action[key], action[key])
if action["id"] not in ACTION_BY_ID or action.get("resume", "") and action["resume"] not in ACTION_BY_ID:
stop(player, "Combat has changed: pick your fight again")
def last_threat(world: dict) -> dict | None:
"""The most recent raid or dragon attack that has played out."""
return next((e for e in reversed(world["events"]) if e["status"] == "done" and e["kind"] in THREATS
@ -1143,16 +1401,30 @@ def bank_take(player: dict, item: str, qty: int) -> None:
def armour(player: dict) -> int:
"""Protection from physical blows: slashing, blunt and piercing."""
return sum(ITEMS[item].get("armour", 0) for item in player["equip"].values() if item)
def damage_reduction(player: dict) -> float:
points = armour(player)
def magic_ward(player: dict) -> int:
"""Protection from magic."""
return sum(ITEMS[item].get("ward", 0) for item in player["equip"].values() if item)
def reduction(points: float) -> float:
return points / (points + 25)
def damage_reduction(player: dict) -> float:
return reduction(armour(player))
def defence_bonus(player: dict) -> int:
"""What all your armour adds to power and evasion, whatever its weight."""
return round((armour(player) + magic_ward(player)) * 0.4)
def combat_power(player: dict) -> int:
power = skill_level(player, "combat") + armour(player) // 2
power = skill_level(player, "combat") + defence_bonus(player)
for item in player["equip"].values():
if item:
power += ITEMS[item].get("power", 0)
@ -1178,20 +1450,32 @@ def weapon_style(player: dict) -> str:
return ITEMS[weapon].get("style", "sword") if weapon else "sword"
def affinity(enemy: dict, damage: str) -> float:
"""How hard a damage type lands on this enemy."""
return VULNERABLE if damage in enemy["vulnerable"] else RESISTANT if damage in enemy["resists"] else 1
def fighter(player: dict, blessed: dict, brew: dict, enemy: dict | None = None) -> dict:
"""Your side of a fight: power from level and gear (plus prayers and potions) aims and hits, shaped by
the weapon's style, your trade and the enemy's weakness; armour dodges and soaks, a shield blocks."""
the weapon's style, your trade and how the enemy takes its damage type. Armour soaks physical blows,
ward soaks magic, both help you dodge, and a shield blocks."""
style_id = weapon_style(player)
style = STYLES[style_id]
power = combat_power(player) + blessed["power"] + brew.get("power", 0)
damage = style["hit"] * (CLASS_STYLE_BONUS if CLASS_STYLE.get(player["cls"]) == style_id else 1)
weakness = WEAKNESS.get(enemy["kind"], {}).get(style_id, 1) if enemy else 1
return {"style": style_id, "interval": style["interval"], "accuracy": (10 + 2 * power) * style["accuracy"],
"max_hit": max(2, round((2 + power / 3) * damage * weakness)), "crit": style["crit"],
"pierce": style.get("pierce", False), "first": style.get("first", False), "weakness": weakness,
lands = affinity(enemy, style["damage"]) if enemy else 1
taken = blessed["taken"] * (1 - brew.get("reduce", 0))
return {"style": style_id, "damage": style["damage"], "interval": style["interval"],
"accuracy": (10 + 2 * power) * style["accuracy"],
"max_hit": max(2, round((2 + power / 3) * damage * lands)), "crit": style["crit"],
"first": style.get("first", False), "affinity": lands,
"block": BLOCK_CHANCE if player["equip"].get("shield") else 0,
"evasion": 10 + 2 * (skill_level(player, "combat") + armour(player) // 2),
"taken": (1 - damage_reduction(player)) * blessed["taken"] * (1 - brew.get("reduce", 0))}
"evasion": 10 + 2 * (skill_level(player, "combat") + defence_bonus(player)),
"taken": {"physical": (1 - damage_reduction(player)) * taken, "magic": (1 - reduction(magic_ward(player))) * taken}}
def _taken(me: dict, damage: str) -> float:
return me["taken"]["magic" if damage == "magic" else "physical"]
def hit_chance(accuracy: float, evasion: float) -> float:
@ -1202,26 +1486,38 @@ def _average_hit(max_hit: float, crit: float = CRIT_CHANCE) -> float:
return (1 - crit) * (max_hit + 1) / 2 + crit * max_hit * CRIT_MULT
def fight_odds(player: dict, action: dict) -> dict:
"""What a fight against this enemy looks like on average, for the cards."""
foe = action["enemy"]
me = fighter(player, blessing(player), potion_effect(player, "combat"), foe)
def _variant_odds(player: dict, foe: dict, blessed: dict, brew: dict) -> dict:
me = fighter(player, blessed, brew, foe)
hit, their_hit = hit_chance(me["accuracy"], foe["evasion"]), hit_chance(foe["accuracy"], me["evasion"])
shrug = 0 if me["pierce"] else foe["reduction"]
dps = hit * _average_hit(me["max_hit"], me["crit"]) * (1 - shrug) / me["interval"]
kill = foe["hp"] / dps
hurt = their_hit * (1 - me["block"]) * _average_hit(foe["max_hit"]) * me["taken"] / foe["interval"] * kill
return {"hit": round(hit, 2), "their_hit": round(their_hit, 2), "max_hit": me["max_hit"], "style": me["style"],
"weakness": me["weakness"], "their_max": max(1, round(foe["max_hit"] * me["taken"])),
"kill_time": round(kill, 1), "damage": round(hurt, 1)}
kill = foe["hp"] / (hit * _average_hit(me["max_hit"], me["crit"]) / me["interval"])
taken = sum(_taken(me, kind) for kind in foe["attack"]) / len(foe["attack"])
hurt = their_hit * (1 - me["block"]) * _average_hit(foe["max_hit"]) * taken / foe["interval"] * kill
return {"hit": hit, "their_hit": their_hit, "max_hit": me["max_hit"], "affinity": me["affinity"],
"their_max": max(1, round(foe["max_hit"] * max(_taken(me, kind) for kind in foe["attack"]))),
"kill_time": kill, "damage": hurt}
def fight_odds(player: dict, action: dict) -> dict:
"""What a fight in this encounter looks like on average, for the cards, and against each variant."""
blessed, brew = blessing(player), potion_effect(player, "combat")
variants = action["enemy"]["variants"]
rows = [_variant_odds(player, foe, blessed, brew) for foe in variants]
total = sum(foe["weight"] for foe in variants)
mean = lambda key: sum(row[key] * foe["weight"] for row, foe in zip(rows, variants)) / total
return {"hit": round(mean("hit"), 2), "their_hit": round(mean("their_hit"), 2), "style": weapon_style(player),
"max_hit": max(row["max_hit"] for row in rows), "their_max": max(row["their_max"] for row in rows),
"kill_time": round(mean("kill_time"), 1), "damage": round(mean("damage"), 1),
"variants": [{"affinity": row["affinity"], "kill_time": round(row["kill_time"], 1),
"damage": round(row["damage"], 1)} for row in rows]}
def simulate_fight(world: dict, player: dict, action: dict, rng: random.Random) -> dict:
"""Play out one fight swing by swing, both swing timers starting empty. Nothing changes
until it's over: the result is applied then."""
foe, blessed, brew = action["enemy"], blessing(player), potion_effect(player, "combat")
variants = action["enemy"]["variants"]
foe = rng.choices(variants, [v["weight"] for v in variants])[0]
blessed, brew = blessing(player), potion_effect(player, "combat")
me = fighter(player, blessed, brew, foe)
shrug = 0 if me["pierce"] else foe["reduction"]
hit, their_hit = hit_chance(me["accuracy"], foe["evasion"]), hit_chance(foe["accuracy"], me["evasion"])
top, hp, foe_hp = max_hp(player), player["hp"], foe["hp"]
pantry = {item: player["bank"][item] for item in edible(player)}
@ -1230,6 +1526,7 @@ def simulate_fight(world: dict, player: dict, action: dict, rng: random.Random)
pace = speed(world)
# A bow looses its first arrow before the enemy closes in.
mine, theirs, result, t = (0.5 if me["first"] else me["interval"]), foe["interval"], None, 0.0
swings = 0 # an enemy with more than one attack takes turns with them
while result is None:
t = min(mine, theirs)
if t > MAX_FIGHT:
@ -1239,23 +1536,27 @@ def simulate_fight(world: dict, player: dict, action: dict, rng: random.Random)
mine += me["interval"]
crit = rng.random() < me["crit"]
landed = rng.random() < hit
dealt = max(1, round((me["max_hit"] * CRIT_MULT if crit else rng.randint(1, me["max_hit"]))
* (1 - shrug))) if landed else 0
dealt = max(1, round(me["max_hit"] * CRIT_MULT if crit else rng.randint(1, me["max_hit"]))) if landed else 0
foe_hp = max(0, foe_hp - dealt)
events.append([round(t / pace, 2), "you", ("crit" if crit else "hit") if landed else "miss", dealt, round(hp), foe_hp])
events.append([round(t / pace, 2), "you", ("crit" if crit else "hit") if landed else "miss", dealt, round(hp),
foe_hp, me["damage"]])
if not foe_hp:
result = "won"
continue
theirs += foe["interval"]
kind = foe["attack"][swings % len(foe["attack"])]
swings += 1
crit = rng.random() < CRIT_CHANCE
landed = rng.random() < their_hit
if landed and rng.random() < me["block"]:
events.append([round(t / pace, 2), "foe", "block", 0, max(0, round(hp)), foe_hp])
continue
dealt = max(1, round((foe["max_hit"] * CRIT_MULT if crit else rng.randint(1, foe["max_hit"])) * me["taken"])) if landed else 0
dealt = max(1, round((foe["max_hit"] * CRIT_MULT if crit else rng.randint(1, foe["max_hit"])) * _taken(me, kind))) \
if landed else 0
hp -= dealt
hurt += dealt
events.append([round(t / pace, 2), "foe", ("crit" if crit else "hit") if landed else "miss", dealt, max(0, round(hp)), foe_hp])
events.append([round(t / pace, 2), "foe", ("crit" if crit else "hit") if landed else "miss", dealt, max(0, round(hp)),
foe_hp, kind])
while hp < top * EAT_BELOW and any(pantry.values()):
food = next(item for item, left in pantry.items() if left)
pantry[food] -= 1
@ -1272,7 +1573,8 @@ def simulate_fight(world: dict, player: dict, action: dict, rng: random.Random)
hp, result = 1, "lost"
elif starving:
result = "fled"
return {"enemy": foe["name"], "enemy_hp": foe["hp"], "top": top, "start_hp": round(player["hp"]),
return {"enemy": foe["name"], "enemy_hp": foe["hp"], "top": top, "start_hp": round(player["hp"]), "xp": foe["xp"],
"foe": {key: foe[key] for key in ("archetype", "attack", "vulnerable", "resists")},
"hp": hp, "result": result, "end": round(t / pace, 2), "duration": t / pace,
"interval": me["interval"] / pace, "enemy_interval": foe["interval"] / pace, "events": events, "style": me["style"],
"eaten": eaten, "damage": hurt, "pp": blessed["cost"],
@ -1536,6 +1838,7 @@ def _complete(world: dict, player: dict, action: dict, now: float, rng: random.R
outing = player["action"]
fight = outing.pop("fight", None) or simulate_fight(world, player, action, rng)
outing.pop("carry", None) # from a short-lived version that ran the swing timer on
xp = fight.get("xp", action["xp"]) * mods["xp"] # the variant you met
if player["prayers"] and not fight["pp"]:
summary["prayer_out"] = True
paid = min(player["pp"], fight["pp"])
@ -2453,9 +2756,10 @@ def _resolve_dragon(world: dict, event: dict, now: float, rng: random.Random) ->
player["favour"] += 40
gain_xp(world, player, "combat", round(attack * 6), now)
bank_add(world, player, "dragon_bones", 2, now, empty_summary())
bank_add(world, player, "green_dragonhide", 1, now, empty_summary())
for weight in WEIGHTS:
bank_add(world, player, reagent(weight, 3), 1, now, empty_summary())
if garrison:
lines.append("Everyone in the garrison earned 40 favour, combat experience, dragon bones and a hide.")
lines.append("Everyone in the garrison earned 40 favour, combat experience, dragon bones and Ironhill reagents.")
log(world, now, "raid", report["title"])
else:
severity = min(1.0, 1 - guard["total"] / attack)
@ -2556,10 +2860,10 @@ PROBLEMS = {
}
MINOR_THREATS = ("collapse", "wolves", "bear", "goblins", "rockslide", "flood", "boars")
PROBLEM_ACTIONS = [
_fight("clear_wolves", monster("Wolf pack", "beast", 4), "Frontier", name="Drive off the wolves",
special="problem", problem="wolves"),
_fight("catch_goblins", monster("Goblin thieves", "human", 5), "Frontier", name="Catch the goblin thieves",
special="problem", problem="goblins"),
_encounter("clear_wolves", "Wolf pack", "Frontier", 4, WOLVES, label="Drive off the wolves",
special="problem", problem="wolves"),
_encounter("catch_goblins", "Goblin thieves", "Frontier", 5, GOBLINS, label="Catch the goblin thieves",
special="problem", problem="goblins"),
_action("drive_bear", "hunting", "Drive off the bear", 1, 45, 14, special="problem", problem="bear"),
_action("cull_boars", "hunting", "Cull the boar herd", 1, 45, 14, special="problem", problem="boars"),
_action("catch_rats", "hunting", "Catch the rats", 1, 30, 8, special="problem", problem="rats"),
@ -2600,7 +2904,7 @@ FITTINGS = [
"+1 prayer point per bone, ward +15", "star-altar", "#6f9a4a"),
("enchanted_chests", "Enchanted chests", "storehouse", 4, 75, {"magic_plank": 40, "rune_lock": 6},
"+500 stack size", "open-treasure-chest", "#a77be8"),
("enchanted_hearths", "Enchanted hearths", "houses", 4, 80, {"magic_plank": 50, "red_dragon_leather": 5, "adamant_chain": 6},
("enchanted_hearths", "Enchanted hearths", "houses", 4, 80, {"magic_plank": 50, "mammoth_leather": 5, "adamant_chain": 6},
"HP recovers twice as fast", "fireplace", "#a77be8"),
]
# Builders' shields, homesteaders' staves and rangers' bows, after each skill's other tabs.
@ -3182,6 +3486,11 @@ PIECE_GLYPHS = {"sword": "pointy-sword", "shield": "round-shield", "helmet": "vi
TOOL_GLYPHS = {"pickaxe": "war-pick", "axe": "wood-axe", "hoe": "sickle", "fishing rod": "fishing-pole",
"hunting knife": "bowie-knife", "hammer": "claw-hammer"}
LEATHER_GLYPHS = {"cap": "hood", "legs": "trousers", "body": "leather-vest"}
CLOTH_GLYPHS = {"hood": "pointy-hat", "legs": "trousers", "body": "robe"}
FIBRE_GLYPHS = {"cotton": "cotton-flower", "silk_cocoons": "butterfly", "moonpetal": "lotus-flower",
"starbloom": "spiral-bloom", "spiritvine": "vine-flower"}
CLOTH_TINTS = ["#d8d0b8", "#f0f0f0", "#e8c8e0", "#a8c8f0", "#f0e080", "#9be0c8"]
REAGENT_GLYPHS = {"heavy": "chain-mail", "medium": "scale-mail", "light": "yarn"}
WEAPON_GLYPHS = {"hammer": "warhammer", "staff": "wizard-staff", "bow": "pocket-bow"}
HARDWARE_GLYPHS = ["screw", "nails", "door-handle", "metal-plate", "crossed-chains", "padlock"]
GLYPHS = {
@ -3206,7 +3515,7 @@ SKILL_GLYPHS = {
"construction": ("hammer-nails", "#d1a46a"),
"cooking": ("cooking-pot", "#e0703a"), "leatherworking": ("sewing-needle", "#d6a26e"),
"herblore": ("cauldron", "#7fbf5a"), "combat": ("crossed-swords", "#e05a5a"), "prayer": ("prayer", "#e9dfc6"),
"reconnaissance": ("spyglass", "#6f8fec"),
"reconnaissance": ("spyglass", "#6f8fec"), "tailoring": ("sewing-machine", "#c8a0e0"),
}
CLASS_GLYPHS = {"miner": ("miner", "#d07a3c"), "builder": ("crane", "#d1a46a"),
"homesteader": ("farmer", "#8fbf6a"), "ranger": ("bow-arrow", "#5fae8a")}
@ -3225,24 +3534,31 @@ ACTION_GLYPHS = {
"labour": ("hammer-nails", "#d1a46a"), "clear_wolves": ("wolf-head", "#9aa1ab"),
"catch_goblins": ("goblin-head", "#8fbf6a"), "drive_bear": ("bear-head", "#8a5a32"), "cull_boars": ("boar", "#a06a4a"),
"catch_rats": ("rat", "#9aa1ab"), "clear_rockslide": ("falling-rocks", "#a8a29e"),
"hunt_elk": ("deer", "#a07a50"), "hunt_sabrecat": ("saber-toothed-cat-head", "#e0a050"),
"hunt_mammoth": ("mammoth", "#8a6a50"),
}
PRAYER_GLYPHS = {"thick_skin": ("shield", "#a8a29e"), "burst_of_strength": ("biceps", "#e06a6a"),
"rapid_heal": ("heart-plus", "#e06a8a"), "steel_skin": ("armor-upgrade", "#d4dae2"),
"superhuman_strength": ("biceps", "#ff8a3a"), "second_wind": ("wind-hole", "#8fd0e0"),
"protection": ("aura", "#f0d060"), "piety": ("angel-wings", "#f0e6c0")}
TROPHY_GLYPHS = {"golem": "crystal-growth", "troll": "animal-hide", "spirit": "magic-swirl", "flyer": "sewing-string"}
TROPHY_GLYPHS = {"hammer": "crystal-growth", "sword": "animal-hide", "staff": "spiral-bloom", "bow": "sewing-string"}
TIER_TINTS = ["#9aa1ab", "#d4dae2", "#6f8fec", "#4cb878", "#3fd0e0"] # trophies of areas II to VI
MONSTER_GLYPHS = {"golem": ["rock-golem"] * 5, "troll": ["troll"] * 5,
"spirit": ["fairy", "spectre", "ghost", "spectre", "ghost"],
"flyer": ["bat", "harpy", "wyvern", "griffin-symbol", "eagle-head"]}
RACE_GLYPHS = {
"Clay golems": "rock-golem", "Cave trolls": "troll", "Restless dead": "skeleton", "Harpies": "harpy",
"Stone guardians": "golem-head", "Lizardfolk": "lizardman", "Bog wraiths": "spectre", "Giant spiders": "spider-face",
"Iron golems": "robot-golem", "Orcs": "orc-head", "Mine ghosts": "ghost", "Wyverns": "wyvern",
"Ice elementals": "ice-golem", "Frost trolls": "troll", "Banshees": "screaming", "Griffins": "griffin-symbol",
"Magma constructs": "lava", "Dragonkin": "dragon-spiral", "Shades": "ghost-ally", "Rocs": "eagle-head",
}
ACTION_GLYPHS.update({
"fight_goblins": ("goblin-head", "#8fbf6a"), "fight_ruins": ("ogre", "#a08060"),
"fight_barrow_king": ("crowned-skull", "#c8d0e0"), "drill_militia": ACTION_GLYPHS["drill_militia"],
})
for _kind, (_offset, _monsters, _trophies) in AREA_MONSTERS.items():
for _index, _name in enumerate(_monsters):
ACTION_GLYPHS[f"fight_{_slug(_name)}"] = (MONSTER_GLYPHS[_kind][_index], TIER_TINTS[_index])
UI_GLYPHS = ["two-coins", "sparkles", "hearts", "prayer", "checked-shield"]
for _index, (_area, _level, _prefix, _races) in enumerate(AREAS):
for _race, *_rest in _races:
ACTION_GLYPHS[f"fight_{_slug(_race)}"] = (RACE_GLYPHS[_race], TIER_TINTS[_index])
UI_GLYPHS = ["two-coins", "sparkles", "hearts", "prayer", "checked-shield", "magic-shield",
*(kind["glyph"] for kind in DAMAGE_TYPES.values())]
def _assign_glyphs() -> None:
@ -3269,8 +3585,16 @@ def _assign_glyphs() -> None:
ITEMS[leather[0]].update(glyph="rolled-cloth", tint=tint)
for suffix, glyph in LEATHER_GLYPHS.items():
ITEMS[f"{prefix}_{suffix}"].update(glyph=glyph, tint=tint)
for trophy, (_name, kind, tier) in TROPHIES.items():
ITEMS[trophy].update(glyph=TROPHY_GLYPHS[kind], tint=TIER_TINTS[tier - 1])
for trophy, (_name, line, tier) in TROPHIES.items():
ITEMS[trophy].update(glyph=TROPHY_GLYPHS[line], tint=TIER_TINTS[tier - 1])
for item, (weight, tier) in REAGENTS.items():
ITEMS[item].update(glyph=REAGENT_GLYPHS[weight], tint=TIER_TINTS[tier - 1])
for (fibre, _level, _time, _xp, cloth, prefix, _label), tint in zip(CLOTHS, CLOTH_TINTS):
if fibre[0] in FIBRE_GLYPHS:
ITEMS[fibre[0]].update(glyph=FIBRE_GLYPHS[fibre[0]], tint=tint)
ITEMS[cloth[0]].update(glyph="rolled-cloth", tint=tint)
for suffix, glyph in CLOTH_GLYPHS.items():
ITEMS[f"{prefix}_{suffix}"].update(glyph=glyph, tint=tint)
for herb, tint in HERB_TINTS.items():
ITEMS[herb].update(glyph="herbs-bundle", tint=tint)
flasks = ["round-bottom-flask", "round-bottom-flask", "standing-potion", "standing-potion",
@ -3324,6 +3648,7 @@ def content() -> dict:
"prayers": [{**prayer, "text": describe_prayer(prayer)} for prayer in PRAYERS], "max_prayers": MAX_PRAYERS,
"potion_charges": POTION_CHARGES, "problems": PROBLEMS, "fittings": list(FITTING_BY_ID.values()),
"equip_slots": EQUIP_SLOTS, "eat_below": EAT_BELOW, "class_speed": CLASS_SPEED,
"styles": STYLES, "weakness": WEAKNESS, "class_style": CLASS_STYLE, "block_chance": BLOCK_CHANCE,
"styles": STYLES, "damage_types": DAMAGE_TYPES, "weights": WEIGHTS, "vulnerable": VULNERABLE,
"resistant": RESISTANT, "class_style": CLASS_STYLE, "block_chance": BLOCK_CHANCE,
"regen_per_hour": REGEN_PER_HOUR,
}