Bosses: own loot each, open at area level +5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-10-22 14:08:41 +01:00
parent 988968f3b3
commit 2579be2475
3 changed files with 41 additions and 14 deletions

34
game.py
View file

@ -689,9 +689,9 @@ def monster(name: str, archetype: str, level: int, attack, vulnerable=(), resist
def _encounter(id: str, name: str, group: str, level: int, roster: list, gold: int = 0, drops=(),
boss: bool = False, **extra) -> dict:
boss: bool = False, req: int | None = None, **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."""
An elite is a couple of levels above the rest. `req` is the combat level to start, if not the enemy's own."""
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)
@ -699,13 +699,14 @@ def _encounter(id: str, name: str, group: str, level: int, roster: list, gold: i
enemy = {"name": name, "level": level, "boss": boss, "variants": variants}
# Gold per kill is a range around the encounter's worth.
purse = [max(1, round(gold * 0.7)), max(2, round(gold * 1.3))] if gold else None
return _action(id, "combat", extra.pop("label", None) or name, level, RESPAWN, xp, gold=purse,
return _action(id, "combat", extra.pop("label", None) or name, req or level, RESPAWN, xp, gold=purse,
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:
"""One big enemy: everyone who brings it down gets their own loot."""
return _encounter(id, name, group, level, [(name, archetype, attack, vulnerable, resists)],
boss=extra.pop("is_boss", True), **extra)
boss=extra.pop("is_boss", True), personal_loot=True, **extra)
# Rosters: (variant, archetype, attacks with, vulnerable to, resists).
@ -940,24 +941,25 @@ COMBAT_ACTIONS = [
_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(),
# Area bosses open at the area's level + 5, halfway through it: hard alone, comfortable together.
_boss("fight_ruins", "Watchtower ogre", "Old Watchtower", 26, "boss", ("blunt", "magic"), ("slash", "magic"),
("blunt", "pierce"), gold=25,
("blunt", "pierce"), gold=25, req=20,
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", 41, "boss", ("slash", "magic"), ("blunt", "magic"),
("pierce", "slash"), gold=40,
("pierce", "slash"), gold=40, req=35,
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", 56, "dragon", ("slash", "magic"), ("pierce", "magic"),
("slash", "blunt"), is_boss=False, gold=80,
("slash", "blunt"), is_boss=False, gold=80, req=50,
drops=[("mithril_ore", 2, 0.3), ("dragon_bones", 1, 1.0), *_boss_drops(3, 0.6)]),
_boss("fight_blue_dragon", "Blue dragon", "Frostpeak", 71, "dragon", ("slash", "magic"), ("blunt", "pierce"),
("magic", "slash"), is_boss=False, gold=110,
("magic", "slash"), is_boss=False, gold=110, req=65,
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", 85, "dragon", ("slash", "magic"), ("slash", "magic"),
("pierce", "blunt"), is_boss=False, gold=150,
("pierce", "blunt"), is_boss=False, gold=150, req=80,
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", 95, "dragon", ("slash", "magic"), ("blunt", "magic"),
("slash", "pierce"), gold=220,
("slash", "pierce"), gold=220, req=90,
drops=[("rune_bar", 1, 0.1), ("dragon_bones", 2, 1.0), *_boss_drops(5, 0.8)]),
]
@ -2598,7 +2600,8 @@ def share_loot(loot: tuple[int, dict], uids: list, rng: random.Random) -> dict:
# --- fighting together ------------------------------------------------------
# Anyone can tag into someone else's fight: from then on they're a party on that
# encounter, every enemy fought by all of them at once. Everyone gets the kill's full
# XP; its gold is split evenly and each drop goes to someone at random. The party
# XP; its gold is split evenly and each drop goes to someone at random, except on
# bosses and dragons, where everyone rolls their own loot as if alone. The party
# lasts until all but one have left (stopped, retreated, or been away too long), and
# the last one carries on alone.
@ -2737,7 +2740,10 @@ def process_party(world: dict, pid: str, now: float, rng: random.Random) -> None
player["action"]["start"] = party["start"]
results = {m["id"]: m["result"] for m in fight["members"]}
winners = [uid for uid in party["members"] if results.get(uid) == "won"]
shares = share_loot(roll_loot(action, rng), winners, rng) if winners else {}
if action.get("personal_loot"):
shares = {uid: roll_loot(action, rng) for uid in winners}
else:
shares = share_loot(roll_loot(action, rng), winners, rng) if winners else {}
for player in members:
if results.get(player["id"], "left") == "left":
continue
@ -4741,6 +4747,10 @@ 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-22", "title": "Bosses", "notes": [
("new", "Bosses and dragons: everyone in the fight gets their own loot."),
("balance", "Level requirements for area boss reduced to be area level +5."),
]},
{"date": "2026-10-21", "title": "Beta", "notes": [
("new", "Beta: completely meaningless milestone reached; every class now makes a decent amount of valuable contributions to the guild."),
("new", "Patch notes menu."),