Dungeons: run on while anyone watches

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-10-27 21:49:49 +00:00
parent 13371ec269
commit 25ecb885f5
2 changed files with 29 additions and 12 deletions

20
game.py
View file

@ -1008,7 +1008,7 @@ COMBAT_ACTIONS = [
# cast what has to be interrupted, hit what has to be braced for, call in help, and lose patience. # cast what has to be interrupted, hit what has to be braced for, call in help, and lose patience.
DUNGEON_SIZE = 4 # the most in a run DUNGEON_SIZE = 4 # the most in a run
DUNGEON_WIPES = 3 # on one pull, and the run is over DUNGEON_WIPES = 3 # on one pull, and the run is over
DUNGEON_AWAY = 90 # real seconds without looking before a run waits for you DUNGEON_AWAY = 90 # real seconds without looking before you count as away (a run waits if everyone is)
TRASH = (1.5, 1.5) # a pull's HP (and XP) and hits against the open world's TRASH = (1.5, 1.5) # a pull's HP (and XP) and hits against the open world's
MINI_BOSS = 5.0 # a mini-boss's HP (and XP, and loot odds) against its race's elite MINI_BOSS = 5.0 # a mini-boss's HP (and XP, and loot odds) against its race's elite
FINAL_BOSS = (2.5, 1.3) # the area boss's (and its adds') HP and hits against the open-world one FINAL_BOSS = (2.5, 1.3) # the area boss's (and its adds') HP and hits against the open-world one
@ -3476,9 +3476,9 @@ def process_party(world: dict, pid: str, now: float, rng: random.Random) -> None
break break
members = [world["players"][uid] for uid in party["members"]] members = [world["players"][uid] for uid in party["members"]]
if action.get("dungeon"): if action.get("dungeon"):
# A run waits for its leader to start it, then for anyone who's looked away: it's played together. # A run waits for its leader to start it, then for someone to be watching. Whoever's
away = [p for p in members if p["last_active"] < party["start"] - DUNGEON_AWAY] # looked away fights on auto.
if "run" not in party or away: if "run" not in party or run_away(world, party):
party["start"] = max(party["start"], now) party["start"] = max(party["start"], now)
for player in members: for player in members:
player["action"]["start"] = party["start"] player["action"]["start"] = party["start"]
@ -3593,6 +3593,12 @@ def _clear_bonus(world: dict, player: dict, spec: dict, now: float, rng: random.
summary["items"][item] = summary["items"].get(item, 0) + kept summary["items"][item] = summary["items"].get(item, 0) + kept
def run_away(world: dict, party: dict) -> list:
"""Everyone in the run, if no one's been looking: it waits for one of them to come back."""
members = [world["players"][uid] for uid in party["members"]]
return members if all(p["last_active"] < party["start"] - DUNGEON_AWAY for p in members) else []
def run_of(world: dict, player: dict) -> dict | None: def run_of(world: dict, player: dict) -> dict | None:
"""Where your group's dungeon run stands, if you're in one.""" """Where your group's dungeon run stands, if you're in one."""
party = party_of(world, player) party = party_of(world, player)
@ -3600,10 +3606,9 @@ def run_of(world: dict, player: dict) -> dict | None:
return None return None
spec = DUNGEONS[ACTION_BY_ID[party["action"]]["dungeon"]] spec = DUNGEONS[ACTION_BY_ID[party["action"]]["dungeon"]]
run = party.get("run", {"pull": 0, "wipes": 0}) run = party.get("run", {"pull": 0, "wipes": 0})
away = [world["players"][uid]["name"] for uid in party["members"]
if world["players"][uid]["last_active"] < party["start"] - DUNGEON_AWAY]
return {"dungeon": spec["name"], "pull": run["pull"] + 1, "of": len(spec["pulls"]), "wipes": run["wipes"], return {"dungeon": spec["name"], "pull": run["pull"] + 1, "of": len(spec["pulls"]), "wipes": run["wipes"],
"next": spec["pulls"][min(run["pull"], len(spec["pulls"]) - 1)][0], "away": away, "next": spec["pulls"][min(run["pull"], len(spec["pulls"]) - 1)][0],
"away": [p["name"] for p in run_away(world, party)],
"leader": party["members"][0], "size": len(party["members"]), "started": "run" in party} "leader": party["members"][0], "size": len(party["members"]), "started": "run" in party}
@ -6275,6 +6280,7 @@ ORDER_RECIPES, ORDER_RAW = _order_sources()
# line here (new / balance / fix), and a new release a new entry on top. # line here (new / balance / fix), and a new release a new entry on top.
PATCH_NOTES = [ PATCH_NOTES = [
{"date": "2026-10-27", "title": "Prayer", "notes": [ {"date": "2026-10-27", "title": "Prayer", "notes": [
("new", "Dungeons: runs go on while anyone is watching. Members who look away fight on auto."),
("new", "Group stays in lobby after finishing dungeon"), ("new", "Group stays in lobby after finishing dungeon"),
("new", "Added \"Drink all\" option to potions in the bank."), ("new", "Added \"Drink all\" option to potions in the bank."),
("new", "Firekeeping: Tend all fires. Each log goes on the lowest fire with room in your pile."), ("new", "Firekeeping: Tend all fires. Each log goes on the lowest fire with room in your pile."),

View file

@ -1083,15 +1083,26 @@ class DungeonTest(unittest.TestCase):
game.start_action(self.world, self.wil, "dungeon_watchtower", T0) game.start_action(self.world, self.wil, "dungeon_watchtower", T0)
self.assertEqual(game.start_run(self.world, self.wil, T0), "Not in a dungeon") self.assertEqual(game.start_run(self.world, self.wil, T0), "Not in a dungeon")
def test_a_run_waits_for_whoever_looked_away(self): def test_a_run_goes_on_while_someone_watches(self):
game.start_action(self.world, self.wil, "dungeon_watchtower", T0) game.start_action(self.world, self.wil, "dungeon_watchtower", T0)
game.join_fight(self.world, self.kay, "wil", T0) game.join_fight(self.world, self.kay, "wil", T0)
game.start_run(self.world, self.wil, T0) game.start_run(self.world, self.wil, T0)
self.online(T0 + 300, [self.wil]) # kay went to make tea now = T0
self.online(T0 + 1200, [self.wil]) while self.party()["run"]["pull"] < 2: # kay went to make tea: on auto, wil plays on
now += 2
self.online(now, [self.wil])
self.assertLess(now, T0 + 3600)
self.assertEqual(game.run_of(self.world, self.wil)["away"], [])
self.assertIn("kay", [m["id"] for m in self.party()["fight"]["members"]])
def test_a_run_waits_when_no_one_watches(self):
game.start_action(self.world, self.wil, "dungeon_watchtower", T0)
game.join_fight(self.world, self.kay, "wil", T0)
game.start_run(self.world, self.wil, T0)
game.catch_up(self.world, T0 + 1200, random.Random(1)) # both went to make tea
self.assertIsNone(self.party()["fight"]) self.assertIsNone(self.party()["fight"])
self.assertEqual(game.run_of(self.world, self.wil)["away"], ["Kay"]) self.assertEqual(game.run_of(self.world, self.wil)["away"], ["Wil", "Kay"])
self.online(T0 + 1210) self.online(T0 + 1210, [self.kay])
self.assertIsNotNone(self.party()["fight"]) self.assertIsNotNone(self.party()["fight"])
def test_three_wipes_on_a_pull_end_the_run(self): def test_three_wipes_on_a_pull_end_the_run(self):