diff --git a/README.md b/README.md index 31df761..7129836 100644 --- a/README.md +++ b/README.md @@ -62,12 +62,14 @@ Beta. The core game is in place; changes from here are mostly balance and tuning while you watch: look away for 12 seconds and it's all auto again. Presses are kept with the fight, which is played again from its seed with them, so nothing that already happened changes. -- **Dungeons:** Watchtower Depths (combat 20) for 2 to 4: eight pulls in order, - sized for the group, ending in the Watchtower ogre. Trash falls on auto; the - bosses (Clay colossus, Harpy matriarch, the ogre) don't: interruptible curses - and shrieks, ground pounds to brace for, adds at half health and an enrage - timer. Join before the first pull. A wipe repeats the pull, three on one pull - end the run, and the run waits while anyone looks away. Bosses give everyone +- **Dungeons:** one per area for 2 to 4, pulls in order, sized for the group: + Goblin Warren (Greenwood, combat 8), Watchtower Depths (20), Barrow Crypts + (35), Bandit Stronghold (Frontier, 40), The Deep Forge (50), Frozen Hold (65) + and Dragon's Lair (80). Each has two mini-bosses and a final boss with its own + abilities. Trash falls on auto; the bosses don't: interruptible curses and + shrieks, slams to brace for, adds at half health and an enrage timer. Join + before the first pull. A wipe repeats the pull, three on one pull end the + run, and the run waits while anyone looks away. Bosses give everyone their own loot; clearing pays gold, a trophy and reagents. - **Armour:** three weights. Heavy plate (Smithing) is strong against blows and weak to magic, light cloth (Tailoring) the other way round, medium diff --git a/combat.py b/combat.py index 0e91e79..581a75b 100644 --- a/combat.py +++ b/combat.py @@ -176,6 +176,40 @@ ENEMY_ABILITIES = { "shriek": {"name": "Shriek", "cast": 2.0, "every": 9, "first": 4, "interrupt": True, "target": "all", "effect": ("aoe", 1.4, "magic")}, } + + +def _curse(name, school): + """A boss's must-interrupt: one target, very hard, every few seconds.""" + return {"name": name, "cast": 2.0, "every": 6, "first": 4, "interrupt": True, "target": "target", + "effect": ("hit", 3.5, school)} + + +def _storm(name, school): + """An interruptible blast at everyone.""" + return {"name": name, "cast": 2.0, "every": 9, "first": 4, "interrupt": True, "target": "all", + "effect": ("aoe", 1.4, school)} + + +def _quake(name, school): + """A slow blast at everyone that can't be stopped: brace for it.""" + return {"name": name, "cast": 3.0, "every": 18, "first": 10, "interrupt": False, "target": "all", + "effect": ("aoe", 1.6, school)} + + +# Each dungeon's bosses, named for who casts them. +ENEMY_ABILITIES.update({ + "hex_storm": _storm("Hex Storm", "magic"), "crushing_decree": _curse("Crushing Decree", "blunt"), + "royal_stomp": _quake("Royal Stomp", "blunt"), "witchfire": _storm("Witchfire", "magic"), + "cheap_shot": _curse("Cheap Shot", "pierce"), "execute": _curse("Execute", "slash"), + "whirlwind": _quake("Whirlwind", "slash"), "rune_blast": _curse("Rune Blast", "magic"), + "venom_spray": _storm("Venom Spray", "magic"), "grave_chill": _curse("Grave Chill", "magic"), + "soul_rend": _quake("Soul Rend", "magic"), "forge_blast": _curse("Forge Blast", "magic"), + "war_cry": _storm("War Cry", "slash"), "acid_spit": _curse("Acid Spit", "magic"), + "acid_breath": _quake("Acid Breath", "magic"), "deep_freeze": _curse("Deep Freeze", "magic"), + "wail": _storm("Wail", "magic"), "frost_lance": _curse("Frost Lance", "magic"), + "frost_breath": _quake("Frost Breath", "magic"), "void_bolt": _curse("Void Bolt", "magic"), + "flame_wave": _storm("Flame Wave", "magic"), "inferno": _curse("Inferno", "magic"), +}) ARCHETYPE_ABILITIES = {"brute": ["heavy_slam"], "skirmisher": ["lunge"], "caster": ["hex_bolt"], "elite": ["heavy_slam"], "boss": ["heavy_slam", "sweep"], "dragon": ["fire_breath", "tail_swipe"]} ENRAGE = {"elite": (0.3, 1.4), "boss": (0.3, 1.4), "dragon": (0.25, 1.5)} # under this share of HP, hits this much harder diff --git a/game.py b/game.py index cd26056..66b61b9 100644 --- a/game.py +++ b/game.py @@ -1005,11 +1005,17 @@ def dungeon_scale(size: int) -> tuple[float, float]: def _dungeon_foe(action_id: str, variant: str, hp: float = 1.0, hits: float = 1.0, loot: float | None = None, + level: int | None = None, archetype: str | None = None, name: str | None = None, big: bool = False, **extra) -> dict: """One of a race's variants, as a dungeon enemy: `hp` times as tough (and worth as much more XP), - hitting `hits` times as hard. It drops what its race does, `loot` (else `hp`) times as often.""" + hitting `hits` times as hard. It drops what its race does, `loot` (else `hp`) times as often. It can + stand at another `level`, as another `archetype` (`big` for a boss's size), under another `name`.""" action = next(a for a in COMBAT_ACTIONS if a["id"] == action_id) foe = next(v for v in action["enemy"]["variants"] if v["name"] == variant) + if level or archetype or big: + foe = monster(name or foe["name"], archetype or foe["archetype"], level or foe["level"], foe["attack"], + foe["vulnerable"], foe["resists"], boss=big) + foe = {**foe, "name": name or foe["name"]} return {**{key: foe[key] for key in ("name", "archetype", "level", "interval", "accuracy", "evasion", "attack", "vulnerable", "resists")}, "boss": extra.pop("boss", False), "hp": round(foe["hp"] * hp), "max_hit": max(1, round(foe["max_hit"] * hits)), @@ -1045,8 +1051,175 @@ DUNGEONS = { _dungeon_foe("fight_cave_trolls", "Troll bruiser", *FINAL_BOSS, 1, dormant=True)]), ], }, + "dungeon_goblin_warren": { + "name": "Goblin Warren", "area": "Greenwood", "tier": 0, "level": 8, "bonus_gold": 30, + "pulls": [ + ("Wolf den", [_dungeon_foe("fight_wolves", v, *TRASH, 1, level=6) for v in ("Grey wolf", "Timber wolf", "Grey wolf")]), + ("Tunnel mouth", [_dungeon_foe("fight_goblins", v, *TRASH, 1, level=6) + for v in ("Goblin brute", "Goblin rogue", "Goblin warlock")]), + ("Old One-Eye", [_dungeon_foe("fight_wolves", "Wolf alpha", MINI_BOSS, 3.0, level=7, name="Old One-Eye", boss=True, + abilities=["crushing_decree", "lunge"], enrage_at=150, + phases=[{"at": 0.5, "say": "Aaaooo!", "wake": ["e1", "e2"]}]), + _dungeon_foe("fight_wolves", "Grey wolf", *TRASH, 1, level=6, dormant=True), + _dungeon_foe("fight_wolves", "Grey wolf", *TRASH, 1, level=6, dormant=True)]), + ("Fungus cave", [_dungeon_foe("fight_goblins", v, *TRASH, 1, level=6) + for v in ("Goblin rogue", "Goblin rogue", "Goblin warlock")]), + ("Grizzik the Hexer", [_dungeon_foe("fight_goblins", "Goblin warlock", MINI_BOSS, 4.0, level=7, archetype="elite", + name="Grizzik the Hexer", boss=True, abilities=["hex_storm", "hex_bolt"], + enrage_at=150)]), + ("Throne cave", [_dungeon_foe("fight_goblins", v, *TRASH, 1, level=6) + for v in ("Goblin brute", "Goblin brute", "Goblin warlock")]), + ("Goblin king", [_dungeon_foe("fight_goblins", "Goblin brute", *FINAL_BOSS, 2, level=9, archetype="boss", + name="Goblin king", big=True, boss=True, + abilities=["crushing_decree", "royal_stomp", "heavy_slam"], enrage_at=210, + phases=[{"at": 0.5, "say": "Guards! Guards!", "wake": ["e1", "e2"]}]), + _dungeon_foe("fight_goblins", "Goblin brute", *FINAL_BOSS, 1, level=6, dormant=True), + _dungeon_foe("fight_goblins", "Goblin brute", *FINAL_BOSS, 1, level=6, dormant=True)]), + ], + }, + "dungeon_bandit_stronghold": { + "name": "Bandit Stronghold", "area": "Frontier", "tier": 2, "level": 40, "bonus_gold": 300, + "pulls": [ + ("Palisade", [_dungeon_foe("fight_scouts", v, *TRASH, 1, level=35) + for v in ("Bandit brute", "Bandit archer", "Bandit archer")]), + ("Gate", [_dungeon_foe("fight_scouts", v, *TRASH, 1, level=35) for v in ("Bandit brute", "Bandit brute", "Hedge mage")]), + ("Bandit captain", [_dungeon_foe("fight_scouts", "Bandit brute", MINI_BOSS, 3.0, level=37, archetype="elite", + name="Bandit captain", boss=True, abilities=["cheap_shot", "heavy_slam"], + enrage_at=150)]), + ("Barracks", [_dungeon_foe("fight_scouts", v, *TRASH, 1, level=35) + for v in ("Bandit brute", "Bandit archer", "Bandit archer", "Hedge mage")]), + ("Hedge witch", [_dungeon_foe("fight_scouts", "Hedge mage", MINI_BOSS, 5.5, level=37, archetype="elite", + name="Hedge witch", boss=True, abilities=["witchfire", "hex_bolt"], enrage_at=150, + phases=[{"at": 0.5, "say": "Cut them down!", "wake": ["e1", "e2"]}]), + _dungeon_foe("fight_scouts", "Bandit archer", *TRASH, 1, level=35, dormant=True), + _dungeon_foe("fight_scouts", "Bandit archer", *TRASH, 1, level=35, dormant=True)]), + ("Armoury", [_dungeon_foe("fight_scouts", v, *TRASH, 1, level=35) for v in ("Bandit brute", "Hedge mage", "Hedge mage")]), + ("Bandit warlord", [_dungeon_foe("fight_stronghold", "Bandit warlord", *FINAL_BOSS, 2, boss=True, + abilities=["execute", "whirlwind", "heavy_slam"], enrage_at=210, + phases=[{"at": 0.5, "say": "To arms!", "wake": ["e1", "e2"]}]), + _dungeon_foe("fight_scouts", "Bandit brute", *FINAL_BOSS, 1, level=35, dormant=True), + _dungeon_foe("fight_scouts", "Bandit brute", *FINAL_BOSS, 1, level=35, dormant=True)]), + ], + }, + "dungeon_barrow_crypts": { + "name": "Barrow Crypts", "area": "Sunken Barrows", "tier": 2, "level": 35, "bonus_gold": 250, + "pulls": [ + ("Barrow mound", [_dungeon_foe("fight_stone_guardians", v, *TRASH, 1) + for v in ("Stone guardian", "Barrow sentry", "Barrow sentry")]), + ("Flooded hall", [_dungeon_foe("fight_bog_wraiths", v, *TRASH, 1) + for v in ("Drowned knight", "Bog wraith", "Marsh wisp")]), + ("Runebound guardian", [_dungeon_foe("fight_stone_guardians", "Runebound guardian", MINI_BOSS, 3.0, boss=True, + abilities=["rune_blast", "shatter"], enrage_at=150)]), + ("Lizard warren", [_dungeon_foe("fight_lizardfolk", v, *TRASH, 1) + for v in ("Lizard warrior", "Lizard spearman", "Lizard spearman", "Lizard shaman")]), + ("Brood mother", [_dungeon_foe("fight_giant_spiders", "Brood mother", MINI_BOSS, 5.5, boss=True, + abilities=["venom_spray", "lunge"], enrage_at=150, + phases=[{"at": 0.5, "say": "The eggs are hatching!", "wake": ["e1", "e2"]}]), + _dungeon_foe("fight_giant_spiders", "Spider hunter", *TRASH, 1, dormant=True), + _dungeon_foe("fight_giant_spiders", "Spider hunter", *TRASH, 1, dormant=True)]), + ("Bone gallery", [_dungeon_foe("fight_bog_wraiths", v, *TRASH, 1) for v in ("Bog wraith", "Bog wraith", "Drowned knight")]), + ("Barrow king", [_dungeon_foe("fight_barrow_king", "Barrow king", *FINAL_BOSS, 2, boss=True, + abilities=["grave_chill", "soul_rend", "heavy_slam"], enrage_at=210, + phases=[{"at": 0.5, "say": "Rise, my knights!", "wake": ["e1", "e2"]}]), + _dungeon_foe("fight_bog_wraiths", "Drowned knight", *FINAL_BOSS, 1, dormant=True), + _dungeon_foe("fight_bog_wraiths", "Drowned knight", *FINAL_BOSS, 1, dormant=True)]), + ], + }, + "dungeon_deep_forge": { + "name": "The Deep Forge", "area": "Iron Hills", "tier": 3, "level": 50, "bonus_gold": 400, + "pulls": [ + ("Mine shaft", [_dungeon_foe("fight_mine_ghosts", v, *TRASH, 1) for v in ("Lost miner", "Lantern ghost", "Shaft howler")]), + ("Orc camp", [_dungeon_foe("fight_orcs", v, *TRASH, 1) for v in ("Orc berserker", "Orc archer", "Orc shaman")]), + ("Iron colossus", [_dungeon_foe("fight_iron_golems", "Iron colossus", MINI_BOSS, 3.0, boss=True, + abilities=["forge_blast", "shatter"], enrage_at=150)]), + ("Foundry", [_dungeon_foe("fight_iron_golems", v, *TRASH, 1) for v in ("Iron golem", "Forge golem", "Forge golem")]), + ("Orc warchief", [_dungeon_foe("fight_orcs", "Orc berserker", MINI_BOSS, 5.5, archetype="elite", name="Orc warchief", + boss=True, abilities=["war_cry", "lunge"], enrage_at=150, + phases=[{"at": 0.5, "say": "Blood and iron!", "wake": ["e1", "e2"]}]), + _dungeon_foe("fight_orcs", "Orc berserker", *TRASH, 1, dormant=True), + _dungeon_foe("fight_orcs", "Orc berserker", *TRASH, 1, dormant=True)]), + ("Wyvern roost", [_dungeon_foe("fight_wyverns", v, *TRASH, 1) for v in ("Wyvern", "Wyvern hatchling", "Venomspitter")]), + ("Green dragon", [_dungeon_foe("fight_green_dragon", "Green dragon", *FINAL_BOSS, 2, boss=True, + abilities=["acid_spit", "acid_breath", "tail_swipe"], enrage_at=210, + phases=[{"at": 0.5, "say": "The brood answers!", "wake": ["e1", "e2"]}]), + _dungeon_foe("fight_wyverns", "Wyvern", *FINAL_BOSS, 1, dormant=True), + _dungeon_foe("fight_wyverns", "Wyvern", *FINAL_BOSS, 1, dormant=True)]), + ], + }, + "dungeon_frozen_hold": { + "name": "Frozen Hold", "area": "Frostpeak", "tier": 4, "level": 65, "bonus_gold": 550, + "pulls": [ + ("Ice gate", [_dungeon_foe("fight_ice_elementals", v, *TRASH, 1) for v in ("Ice brute", "Frost wisp", "Frost wisp")]), + ("Troll cave", [_dungeon_foe("fight_frost_trolls", v, *TRASH, 1) + for v in ("Frost troll", "Troll icehurler", "Troll rimecaller")]), + ("Glacier colossus", [_dungeon_foe("fight_ice_elementals", "Glacier colossus", MINI_BOSS, 3.0, boss=True, + abilities=["deep_freeze", "shatter"], enrage_at=150)]), + ("Griffin eyrie", [_dungeon_foe("fight_griffins", v, *TRASH, 1) for v in ("Griffin", "Griffin rider", "Storm griffin")]), + ("Banshee queen", [_dungeon_foe("fight_banshees", "Banshee", MINI_BOSS, 5.5, archetype="elite", name="Banshee queen", + boss=True, abilities=["wail", "lunge"], enrage_at=150, + phases=[{"at": 0.5, "say": "Sing with me!", "wake": ["e1", "e2"]}]), + _dungeon_foe("fight_banshees", "Keening shade", *TRASH, 1, dormant=True), + _dungeon_foe("fight_banshees", "Keening shade", *TRASH, 1, dormant=True)]), + ("Hall of echoes", [_dungeon_foe("fight_banshees", v, *TRASH, 1) for v in ("Wailing knight", "Banshee", "Banshee")]), + ("Blue dragon", [_dungeon_foe("fight_blue_dragon", "Blue dragon", *FINAL_BOSS, 2, boss=True, + abilities=["frost_lance", "frost_breath", "tail_swipe"], enrage_at=210, + phases=[{"at": 0.5, "say": "Freeze!", "wake": ["e1", "e2"]}]), + _dungeon_foe("fight_ice_elementals", "Ice brute", *FINAL_BOSS, 1, dormant=True), + _dungeon_foe("fight_ice_elementals", "Ice brute", *FINAL_BOSS, 1, dormant=True)]), + ], + }, + "dungeon_dragons_lair": { + "name": "Dragon's Lair", "area": "Dragon's Spine", "tier": 5, "level": 80, "bonus_gold": 750, + "pulls": [ + ("Lava tubes", [_dungeon_foe("fight_magma_constructs", v, *TRASH, 1) + for v in ("Magma brute", "Cinder golem", "Fire binder")]), + ("Shadow pass", [_dungeon_foe("fight_shades", v, *TRASH, 1) for v in ("Shade", "Shadow blade", "Shadow blade")]), + ("Void knight", [_dungeon_foe("fight_shades", "Void knight", MINI_BOSS, 3.0, boss=True, + abilities=["void_bolt", "shatter"], enrage_at=150)]), + ("Roc nests", [_dungeon_foe("fight_rocs", v, *TRASH, 1) for v in ("Roc", "Roc hatchling", "Thunder roc")]), + ("Flame high priest", [_dungeon_foe("fight_dragonkin", "Flame priest", MINI_BOSS, 5.5, archetype="elite", + name="Flame high priest", boss=True, abilities=["flame_wave", "hex_bolt"], + enrage_at=150, phases=[{"at": 0.5, "say": "Burn!", "wake": ["e1", "e2"]}]), + _dungeon_foe("fight_dragonkin", "Flame priest", *TRASH, 1, dormant=True), + _dungeon_foe("fight_dragonkin", "Flame priest", *TRASH, 1, dormant=True)]), + ("Drakeguard hall", [_dungeon_foe("fight_dragonkin", v, *TRASH, 1) + for v in ("Drakeguard", "Drake lancer", "Flame priest")]), + ("Red dragon", [_dungeon_foe("fight_red_dragon", "Red dragon", *FINAL_BOSS, 2, boss=True, + abilities=["inferno", "fire_breath", "tail_swipe"], enrage_at=210, + phases=[{"at": 0.5, "say": "Guards!", "wake": ["e1", "e2"]}]), + _dungeon_foe("fight_dragonkin", "Drakeguard", *FINAL_BOSS, 1, dormant=True), + _dungeon_foe("fight_dragonkin", "Drakeguard", *FINAL_BOSS, 1, dormant=True)]), + ], + }, } +# Each boss pull's last touch, HP and hits, so that a group on auto loses it and one that plays +# wins it (tools/balance.py dungeon, and dungeon perfect). +DUNGEON_TUNING = { + ("dungeon_goblin_warren", "Old One-Eye"): (0.8, 0.8), ("dungeon_goblin_warren", "Grizzik the Hexer"): (1.5, 0.5), + ("dungeon_goblin_warren", "Goblin king"): (0.8, 1.2), + ("dungeon_bandit_stronghold", "Bandit captain"): (1.0, 1.2), ("dungeon_bandit_stronghold", "Hedge witch"): (1.25, 0.65), + ("dungeon_bandit_stronghold", "Bandit warlord"): (1.5, 1.4), + ("dungeon_barrow_crypts", "Runebound guardian"): (0.8, 1.2), ("dungeon_barrow_crypts", "Brood mother"): (1.0, 1.2), + ("dungeon_barrow_crypts", "Barrow king"): (1.5, 1.0), + ("dungeon_deep_forge", "Orc warchief"): (1.0, 1.3), ("dungeon_deep_forge", "Green dragon"): (1.25, 1.2), + ("dungeon_frozen_hold", "Glacier colossus"): (1.0, 1.2), ("dungeon_frozen_hold", "Banshee queen"): (1.25, 1.0), + ("dungeon_frozen_hold", "Blue dragon"): (1.5, 1.2), + ("dungeon_dragons_lair", "Void knight"): (1.5, 1.0), ("dungeon_dragons_lair", "Flame high priest"): (0.8, 1.2), + ("dungeon_dragons_lair", "Red dragon"): (1.5, 1.4), +} + + +def _tune_dungeons() -> None: + for key, spec in DUNGEONS.items(): + for index, (name, pack) in enumerate(spec["pulls"]): + hp, hits = DUNGEON_TUNING.get((key, name), (1, 1)) + spec["pulls"][index] = (name, [{**foe, "hp": round(foe["hp"] * hp), "max_hit": max(1, round(foe["max_hit"] * hits))} + for foe in pack]) + + +_tune_dungeons() + def _dungeon_actions() -> list[dict]: actions = [] @@ -1057,7 +1230,8 @@ def _dungeon_actions() -> list[dict]: round(sum(foe["xp"] for _name, pack in spec["pulls"] for foe in pack)), enemy={"name": spec["name"], "level": spec["level"], "boss": True, "variants": variants}, group=spec["area"], dungeon=key, together=GROUP_SIZE, personal_loot=True, - gold=[spec["bonus_gold"], spec["bonus_gold"]], drops=[list(d) for d in _boss_drops(spec["tier"])], + gold=[spec["bonus_gold"], spec["bonus_gold"]], + drops=[list(d) for d in _boss_drops(spec["tier"])] if spec["tier"] else bosses[-1].get("drops", []), pulls=[name for name, _pack in spec["pulls"]])) return actions @@ -3065,7 +3239,7 @@ def _clear_bonus(world: dict, player: dict, spec: dict, now: float, rng: random. player["gold"] += spec["bonus_gold"] summary["gold"] += spec["bonus_gold"] found = [trophy(rng.choice(list(STYLES)), spec["tier"]), *(reagent(rng.choice(list(REAGENT_NOUNS)), spec["tier"]) - for _ in range(2))] + for _ in range(2))] if spec["tier"] else [] for item in found: kept = bank_add(world, player, item, 1, now, summary) if kept: @@ -5463,7 +5637,10 @@ BUILDING_GLYPHS = { ACTION_GLYPHS = { "fight_wolves": ("wolf-head", "#9aa1ab"), "fight_scouts": ("hooded-assassin", "#a08060"), "fight_ruins": ("castle-ruins", "#a8a29e"), "fight_stronghold": ("castle", "#8a8f98"), - "dungeon_watchtower": ("watchtower", "#d1a46a"), + "dungeon_watchtower": ("watchtower", "#d1a46a"), "dungeon_goblin_warren": ("cave-entrance", "#7fbf5a"), + "dungeon_bandit_stronghold": ("guarded-tower", "#a08060"), "dungeon_barrow_crypts": ("tombstone", "#8fb8f0"), + "dungeon_deep_forge": ("anvil-impact", "#ff8a3a"), "dungeon_frozen_hold": ("frozen-orb", "#8fd0e0"), + "dungeon_dragons_lair": ("dragon-spiral", "#e05a5a"), "fight_green_dragon": ("dragon-head", "#5fae5a"), "fight_blue_dragon": ("dragon-head", "#4f86d6"), "fight_red_dragon": ("dragon-head", "#d0503c"), "fight_black_dragon": ("dragon-head", "#8a8f98"), "sabotage": ("fire-bomb", "#e0703a"), "drill_militia": ("sword-brandish", "#e0a050"), "recon_roads": ("spyglass", "#6f8fec"), "recon_camps": ("spyglass", "#8a7fd0"), @@ -5492,6 +5669,17 @@ ABILITY_GLYPHS = { "fire_breath": ("dragon-breath", "#ff8a3a"), "tail_swipe": ("spiked-tail", "#c08850"), "stone_curse": ("evil-hand", "#a77be8"), "ground_pound": ("earth-crack", "#c08850"), "shatter": ("shattered-glass", "#d1a46a"), "shriek": ("sonic-shout", "#a77be8"), + "hex_storm": ("lightning-storm", "#a77be8"), "crushing_decree": ("crown", "#e0b04a"), + "royal_stomp": ("crush", "#c08850"), "witchfire": ("witch-flight", "#5fae5a"), + "cheap_shot": ("thrown-daggers", "#d4dae2"), "execute": ("decapitation", "#e06a6a"), + "whirlwind": ("whirlwind", "#d4dae2"), "rune_blast": ("rune-stone", "#8fb8f0"), + "venom_spray": ("poison-cloud", "#5fae5a"), "grave_chill": ("tombstone", "#8fb8f0"), + "soul_rend": ("skull-bolt", "#a77be8"), "forge_blast": ("furnace", "#ff8a3a"), + "war_cry": ("shouting", "#e06a6a"), "acid_spit": ("acid-blob", "#5fae5a"), + "acid_breath": ("acid", "#5fae5a"), "deep_freeze": ("frozen-block", "#8fd0e0"), + "wail": ("screaming", "#a77be8"), "frost_lance": ("ice-spear", "#8fd0e0"), + "frost_breath": ("snowflake-2", "#8fd0e0"), "void_bolt": ("vortex", "#a77be8"), + "flame_wave": ("fire-wave", "#ff8a3a"), "inferno": ("fire-ring", "#ff6a2a"), } PRAYER_GLYPHS = {"thick_skin": ("shield", "#a8a29e"), "burst_of_strength": ("biceps", "#e06a6a"), "rapid_heal": ("heart-plus", "#e06a8a"), "steel_skin": ("armor-upgrade", "#d4dae2"), @@ -5623,6 +5811,14 @@ ORDER_RECIPES, ORDER_RAW = _order_sources() # What changed, shown on the Patch notes page. Newest first; every shipped change adds a # line here (new / balance / fix), and a new release a new entry on top. PATCH_NOTES = [ + {"date": "2026-10-24", "title": "A dungeon in every area", "notes": [ + ("new", "Six more dungeons for 2–4, seven pulls each with two mini-bosses and a final boss: Goblin Warren " + "(Greenwood, combat 8, best with 3–4), Barrow Crypts (Sunken Barrows, 35), Bandit Stronghold (Frontier, 40), " + "The Deep Forge (Iron Hills, 50), Frozen Hold (Frostpeak, 65) and Dragon's Lair (Dragon's Spine, 80)."), + ("new", "Every boss has its own: curses to interrupt, shrieks, slams to brace for, adds at half health and an " + "enrage timer. As in the Watchtower, trash falls on auto and the bosses need your own hands."), + ("new", "Clearing pays 30–750 gold by area, plus a trophy and 2 reagents (Goblin Warren: gold only)."), + ]}, {"date": "2026-10-24", "title": "Gold", "notes": [ ("balance", "Raids hit about twice as hard and grow with the guild: they add half the combat power of everyone " "active in the last 2 days. Pay the guards, drill, stand guard and sabotage the camp, or expect losses. "