diff --git a/README.md b/README.md index e51fea7..bfc1b02 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,14 @@ Alpha. One season, bandit raids plus two friendly events, six buildings. one area per tier (Old Watchtower, Sunken Barrows, Iron Hills, Frostpeak, Dragon's Spine), each with a golem, a troll, a spirit and a flyer that drop trophies for stronger weapons, and a boss (the dragons at the top). +- **Weapons:** four styles, one per trade, and anyone can use any of them. + Sword and shield (2.4s swings, the shield blocks 15% of hits; swords from + Smithing, shields from Construction), warhammer (Smithing; slow, heavy, + twice the crits), staff (Herblore; accurate, ignores armour) and bow + (Leatherworking; fast, strikes first). The last three take both hands and + carry a shield's power. Your trade's weapon does 15% more damage. Golems + break under hammers, trolls fall to swords, spirits only fear staves, and + flyers fall to bows. Past copper, each weapon needs its tier's trophies. - **Tiers:** six RuneScape-style tiers (copper, iron, steel, mithril, adamant, rune at levels 1–75) of ore, trees (up to enchanted) and hides (deer, boar and bear from hunting; green, blue and red dragonhide from fighting diff --git a/app.py b/app.py index 0adebe8..32706fd 100644 --- a/app.py +++ b/app.py @@ -409,6 +409,13 @@ class Handler(BaseHTTPRequestHandler): problem = game.equip_problem(player, item) if problem: return problem + shield = player["equip"].get("shield") + if spec.get("two_handed") and shield: + # Both hands on the weapon: the shield goes back to the bank. + if game.bank_room(self.store.world, player, shield) < 1: + return "No room in the bank for your shield" + player["bank"][shield] = player["bank"].get(shield, 0) + 1 + player["equip"]["shield"] = None game.bank_take(player, item, 1) current = player["equip"][spec["slot"]] if current: diff --git a/game.py b/game.py index aa92c6c..6e5541f 100644 --- a/game.py +++ b/game.py @@ -186,6 +186,22 @@ PLANKS = [("plank", "Plank"), ("oak_plank", "Oak plank"), ("willow_plank", "Will for (_log, _name, _icon, _value, _action_id, _verb), (_plank, _plank_name) in zip(TREES, PLANKS): ITEMS[_log] = {"name": _name, "icon": _icon, "value": _value} ITEMS[_plank] = {"name": _plank_name, "icon": "🪚", "value": _value * 2 + 1} +# Weapons: four styles, one per trade. The sword pairs with a shield; the +# warhammer, staff and bow take both hands, so they carry the shield's power too. +SWORD_POWER = next(piece[5] for piece in PIECES if piece[0] == "sword") +SHIELD_POWER = next(piece[5] for piece in PIECES if piece[0] == "shield") +TWO_HANDED_POWER = [sword + shield for sword, shield in zip(SWORD_POWER, SHIELD_POWER)] +WOODS = ["wooden", "oak", "willow", "maple", "yew", "magic"] +STAVES = ["Wooden staff", "Oak staff", "Willow staff", "Maple staff", "Yew staff", "Enchanted staff"] +BOWS = ["Shortbow", "Oak bow", "Willow bow", "Maple bow", "Yew bow", "Enchanted bow"] +for _tier, _metal in enumerate(METALS): + ITEMS[f"{_metal[0]}_sword"]["style"] = "sword" + _weapon = {"slot": "weapon", "power": TWO_HANDED_POWER[_tier], "tier": _tier, "two_handed": True, + "req": ["combat", EQUIP_LEVELS[_tier]], "value": round(5 * _metal[5] * 1.2)} + ITEMS[f"{_metal[0]}_warhammer"] = {"name": f"{_metal[1]} warhammer", "icon": "🔨", "style": "hammer", **_weapon} + 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): ITEMS[_raw[0]] = {"name": _raw[1], "icon": _raw[2], "value": _raw[3]} ITEMS[_leather[0]] = {"name": _leather[1], "icon": "🪢", "value": _leather[2]} @@ -314,18 +330,21 @@ CLASSES = { "bonus": "Mining and smithing 20% faster", "masterwork": "Masterwork: 1 in 10 smithing crafts make double", "signature": "Prospector: 1 in 10 mining trips bring back double", + "combat": "Warhammer: +15% damage with one. Slow, heavy, crushes golems", }, "builder": { "name": "Builder", "icon": "🏗️", "fast": ["woodcutting", "construction"], "craft": ["construction"], "bonus": "Woodcutting and construction (masonry included) 20% faster", "masterwork": "Masterwork: 1 in 10 construction crafts make double", "signature": "Foreman: +1 labour every time you work a building site", + "combat": "Sword and shield: +15% damage with a sword. The shield blocks, and trolls fall to blades", }, "homesteader": { "name": "Homesteader", "icon": "🌾", "fast": ["cooking", "herblore"], "craft": ["cooking", "herblore"], "bonus": "Cooking and herblore 20% faster. Two plots: farm one crop 30% faster, or two crops at once", "masterwork": "Masterwork: 1 in 10 cooking and herblore crafts make double", "signature": "Steady hand: potions you drink last 25% longer", + "combat": "Staff: +15% damage with one. Rarely misses, ignores armour, and spirits fear it", }, "ranger": { "name": "Ranger", "icon": "🏹", "fast": ["hunting", "fishing", "leatherworking", "cooking"], @@ -333,6 +352,7 @@ CLASSES = { "bonus": "Hunting, fishing, leatherworking and cooking 20% faster", "masterwork": "Masterwork: 1 in 10 leatherworking and cooking crafts make double", "signature": "Pathfinder: your scouting gathers 50% more reconnaissance", + "combat": "Bow: +15% damage with one. Fast, strikes first, and brings down flyers", }, } # Classes from before the trades, as a starting point until their player re-picks. @@ -369,9 +389,16 @@ def _tiered_actions() -> list[dict]: bars * piece_xp[tier], {f"{key}_bar": bars}, {hardware: made}, group="Hardware", city=city)) 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 {})} actions.append(_action(f"smith_{key}_{piece}", "smithing", f"{name} {noun}", TIER_LEVELS[tier] + offset, - 25 + 5 * bars + 2 * tier, bars * piece_xp[tier], {f"{key}_bar": bars}, + 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}_warhammer": 1}, group=name, city=city)) for tool, _icon in TOOLS.values(): tool_id = f"{key}_{tool.replace(' ', '_')}" actions.append(_action(f"smith_{tool_id}", "smithing", f"{name} {tool}", TIER_LEVELS[tier] + 3, @@ -406,10 +433,54 @@ def _tiered_actions() -> list[dict]: CRIT_CHANCE = 0.05 CRIT_MULT = 1.5 RESPAWN = 3 # seconds before the next enemy turns up -PLAYER_INTERVAL = 2.4 # seconds between your swings 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 +# 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}, +} +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}, +} + + +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 _weapon_actions() -> list[dict]: + """Shields (Construction), staves (Herblore) and bows (Leatherworking); trophies past the first tier.""" + 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 {} + 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)}, + {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}, + 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}, + 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), @@ -581,9 +652,9 @@ BUILDINGS = { "barracks": { "name": "Barracks", "icon": "⛺", "max": 5, "gold": 300, "base": {"logs": 300, "bricks": 200, "thatch": 200, "leather": 80, "copper_sword": 25, "provisions": 80, "copper_rivets": 60}, - "advanced": {"iron_sword": 20, "hard_leather": 40}, - "late": {"steel_sword": 12, "oak_plank": 30, "hide_glue": 40}, - "final": {"mithril_sword": 8, "willow_plank": 25, "bear_leather": 40}, + "advanced": {"iron_bar": 60, "hard_leather": 40}, + "late": {"steel_bar": 40, "oak_plank": 30, "hide_glue": 40}, + "final": {"mithril_bar": 25, "willow_plank": 25, "bear_leather": 40}, "effect": "Trains townsfolk as militia: more defence per citizen", }, "smithy": { @@ -671,7 +742,7 @@ RAID_TRAITS = ["lightly armoured, heavy on archers", "mounted and fast, few bows # --- world --------------------------------------------------------------- -WORLD_VERSION = 13 +WORLD_VERSION = 14 def new_world(now: float, speed: float = 1.0) -> dict: @@ -792,6 +863,14 @@ def migrate(world: dict, now: float | None = None) -> dict: _fold_masonry(world) _return_overfunding(world) world["version"] = 13 + if world["version"] < 14: + # Shields moved from Smithing to Construction: anyone smithing them stops. + for player in world["players"].values(): + action = player["action"] + if action and (action["id"] not in ACTION_BY_ID or action.get("resume", "") and action["resume"] not in ACTION_BY_ID): + stop(player, "Shields are made in Construction now") + _return_overfunding(world) + world["version"] = 14 return world @@ -1073,10 +1152,23 @@ def regen(world: dict, player: dict, now: float, boost: float = 1) -> None: player["hp_at"] = now -def fighter(player: dict, blessed: dict, brew: dict) -> dict: - """Your side of a fight: power from level and gear (plus prayers and potions) aims and hits; armour dodges and soaks.""" +def weapon_style(player: dict) -> str: + weapon = player["equip"].get("weapon") + return ITEMS[weapon].get("style", "sword") if weapon else "sword" + + +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.""" + style_id = weapon_style(player) + style = STYLES[style_id] power = combat_power(player) + blessed["power"] + brew.get("power", 0) - return {"interval": PLAYER_INTERVAL, "accuracy": 10 + 2 * power, "max_hit": max(2, round(2 + power / 3)), + 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, + "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))} @@ -1085,50 +1177,59 @@ def hit_chance(accuracy: float, evasion: float) -> float: return min(0.95, max(0.05, accuracy / (accuracy + evasion) + 0.15)) -def _average_hit(max_hit: float) -> float: - return (1 - CRIT_CHANCE) * (max_hit + 1) / 2 + CRIT_CHANCE * max_hit * CRIT_MULT +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.""" - me, foe = fighter(player, blessing(player), potion_effect(player, "combat")), action["enemy"] + foe = action["enemy"] + me = fighter(player, blessing(player), potion_effect(player, "combat"), foe) hit, their_hit = hit_chance(me["accuracy"], foe["evasion"]), hit_chance(foe["accuracy"], me["evasion"]) - dps = hit * _average_hit(me["max_hit"]) * (1 - foe["reduction"]) / me["interval"] + 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 * _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"], - "their_max": max(1, round(foe["max_hit"] * me["taken"])), "kill_time": round(kill, 1), "damage": round(hurt, 1)} + 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)} def simulate_fight(world: dict, player: dict, action: dict, rng: random.Random) -> dict: """Play out one fight swing by swing. Nothing changes until it's over: the result is applied then.""" foe, blessed, brew = action["enemy"], blessing(player), potion_effect(player, "combat") - me = fighter(player, blessed, brew) + 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)} eaten, events, hurt = {}, [], 0 wind = blessed["wind"] and not (player["action"] or {}).get("wind_used") pace = speed(world) - mine, theirs, result, t = me["interval"], foe["interval"], None, 0.0 + # 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 while result is None: t = min(mine, theirs) if t > MAX_FIGHT: result = "fled" break - crit = rng.random() < CRIT_CHANCE if mine <= theirs: 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 - foe["reduction"]))) if landed else 0 + * (1 - shrug))) 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]) if not foe_hp: result = "won" continue theirs += foe["interval"] + 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 hp -= dealt hurt += dealt @@ -1151,7 +1252,7 @@ def simulate_fight(world: dict, player: dict, action: dict, rng: random.Random) result = "fled" return {"enemy": foe["name"], "enemy_hp": foe["hp"], "top": top, "start_hp": round(player["hp"]), "hp": hp, "result": result, "end": round(t / pace, 2), "duration": (t + RESPAWN) / pace, - "interval": me["interval"] / pace, "enemy_interval": foe["interval"] / pace, "events": events, + "interval": me["interval"] / pace, "enemy_interval": foe["interval"] / pace, "events": events, "style": me["style"], "eaten": eaten, "damage": hurt, "pp": blessed["cost"], "wind_used": blessed["wind"] and not wind and not (player["action"] or {}).get("wind_used")} @@ -1887,6 +1988,9 @@ def _wear_lines(broken: dict, arrows: int) -> list[str]: def equip_problem(player: dict, item: str) -> str | None: """Why the player can't wear or use this yet, or None.""" + weapon = player["equip"].get("weapon") + if ITEMS[item].get("slot") == "shield" and weapon and ITEMS[weapon].get("two_handed"): + return f"Your {ITEMS[weapon]['name'].lower()} takes both hands" req = ITEMS[item].get("req") if req and skill_level(player, req[0]) < req[1]: name = next(s["name"] for s in SKILLS if s["id"] == req[0]) @@ -2463,6 +2567,10 @@ FITTINGS = [ ("enchanted_hearths", "Enchanted hearths", "houses", 4, 80, {"magic_plank": 50, "red_dragon_leather": 5, "adamant_chain": 6}, "Everyone recovers HP twice as fast", "fireplace", "#a77be8"), ] +# Builders' shields, homesteaders' staves and rangers' bows, after each skill's other tabs. +for _weapon in _weapon_actions(): + ACTIONS.append(_weapon) + ACTION_BY_ID[_weapon["id"]] = _weapon FITTING_BY_ID = {} for _id, _name, _building, _blevel, _level, _inputs, _effect, _glyph, _tint in FITTINGS: FITTING_BY_ID[_id] = {"id": _id, "name": _name, "building": _building, "level": _blevel, "effect": _effect} @@ -3038,6 +3146,7 @@ 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"} +WEAPON_GLYPHS = {"hammer": "warhammer", "staff": "wizard-staff", "bow": "pocket-bow"} HARDWARE_GLYPHS = ["screw", "nails", "door-handle", "metal-plate", "crossed-chains", "padlock"] GLYPHS = { "stone": ("stone-block", "#a8a29e"), "wheat": ("wheat", "#e3c060"), "vegetables": ("carrot", "#f08a3c"), @@ -3109,6 +3218,9 @@ def _assign_glyphs() -> None: ITEMS[ore[0]].update(glyph="ore", tint=tint) ITEMS[f"{key}_bar"].update(glyph="metal-bar", tint=tint) ITEMS[HARDWARE[tier][0]].update(glyph=HARDWARE_GLYPHS[tier], tint=tint) + ITEMS[f"{key}_warhammer"].update(glyph=WEAPON_GLYPHS["hammer"], tint=tint) + ITEMS[f"{WOODS[tier]}_staff"].update(glyph=WEAPON_GLYPHS["staff"], tint=WOOD_TINTS[tier]) + ITEMS[f"{WOODS[tier]}_bow"].update(glyph=WEAPON_GLYPHS["bow"], tint=WOOD_TINTS[tier]) for piece, glyph in PIECE_GLYPHS.items(): ITEMS[f"{key}_{piece}"].update(glyph=glyph, tint=tint) for tool, glyph in TOOL_GLYPHS.items(): @@ -3176,5 +3288,6 @@ 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, "regen_per_hour": REGEN_PER_HOUR, } diff --git a/static/app.js b/static/app.js index 9a1442e..7f69d97 100644 --- a/static/app.js +++ b/static/app.js @@ -441,6 +441,17 @@ function toolPanel(skill) { : `No tool. A smith can make you one that speeds this up.`}`; } +// What a monster is weak to and shrugs off, and how your weapon fares against it. +function weaknessLine(kind, odds) { + const table = C.weakness[kind]; + if (!table) return ""; + const names = (pick) => Object.entries(table).filter(([, m]) => pick(m)).map(([s]) => `${C.styles[s].name}s`).join(", "); + const weak = names((m) => m > 1), tough = names((m) => m < 1); + const yours = odds.weakness > 1 ? ` · your ${C.styles[odds.style].name} hits it hard` + : odds.weakness < 1 ? ` · your ${C.styles[odds.style].name} does half damage` : ""; + return `
Fighting with ${weapon ? `a ${esc(st.name)}` : "your fists"}: ${traits[styleId]}. + ${own === styleId ? `Your trade's weapon: +15% damage.` : `As a ${esc(C.classes[me.cls].name.toLowerCase())} you hit 15% harder with a ${esc(C.styles[own].name)}.`}
`; +} + function combatPanel() { const me = S.me; const stat = (id) => [item(id).power && `+${item(id).power} power`, item(id).armour && `+${item(id).armour} armour`].filter(Boolean).join(", "); @@ -524,6 +549,7 @@ function combatPanel() {Power ${me.power} (level + weapon + shield + half your armour) · armour ${me.armour} takes ${Math.round(me.reduction * 100)}% off the damage you take. Equip gear from the bank.
+ ${styleNote(me)}${esc(cls.bonus)}.
${esc(cls.masterwork)}.
${esc(cls.signature)}.
+${esc(cls.combat)}.