Dungeons: stay in lobby after a run

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-10-27 21:46:07 +00:00
parent 2597a1d7e7
commit 13371ec269
3 changed files with 19 additions and 10 deletions

10
game.py
View file

@ -3097,7 +3097,7 @@ def gain_xp(world: dict, player: dict, skill: str, xp: float, now: float, summar
def empty_summary() -> dict:
return {"done": 0, "items": {}, "overflow": {}, "xp": {}, "levels": [], "eaten": {}, "used": {}, "damage": 0,
"gold": 0, "wins": 0, "losses": 0, "lucky": 0, "stopped": None, "resumed": None, "skipped": [],
"gold": 0, "wins": 0, "losses": 0, "lucky": 0, "stopped": None, "resumed": None, "run_over": None, "skipped": [],
"sold": {}, "sold_gold": 0, "pp_spent": 0, "pp_gained": 0, "prayer_out": False, "second_wind": 0,
"kept": 0, "potions_drunk": {}, "potion_out": [], "labour": 0}
@ -3574,11 +3574,10 @@ def _advance_run(world: dict, party: dict, action: dict, fight: dict, members: l
return
reason = f"Wiped {DUNGEON_WIPES} times at {spec['pulls'][run['pull']][0]}"
log(world, now, "party", f"{names}: {reason.lower()} in {spec['name']}")
del world["parties"][party["id"]]
# Back to the lobby, together: the leader can take the group in again, and others can join.
del party["run"]
for player in members:
summary = player.setdefault("away", empty_summary())
if not finish_once(world, player, now, reason, summary):
summary["stopped"] = reason
player.setdefault("away", empty_summary())["run_over"] = reason
def _clear_bonus(world: dict, player: dict, spec: dict, now: float, rng: random.Random) -> None:
@ -6276,6 +6275,7 @@ ORDER_RECIPES, ORDER_RAW = _order_sources()
# line here (new / balance / fix), and a new release a new entry on top.
PATCH_NOTES = [
{"date": "2026-10-27", "title": "Prayer", "notes": [
("new", "Group stays in lobby after finishing dungeon"),
("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", "Added resource dumps for Leatherworking: Guard Jerkins"),

View file

@ -190,6 +190,7 @@ function showSummary(sum) {
if (sold.length) toast(`📦 Bank full: sold ${sold.map(([id, q]) => `${fmt(q)} ${item(id).name.toLowerCase()}`).join(", ")} for ${fmt(sum.sold_gold)} gold`);
for (const [skill, level] of sum.levels) toast(`🎉 ${skillOf(skill).name} level ${level}!`, "level");
if (sum.stopped) toast(`Stopped: ${sum.stopped}`, "bad");
if (sum.run_over) toast(`🏁 ${sum.run_over}`, sum.run_over.startsWith("Cleared") ? "level" : "bad");
if (sum.skipped?.length) toast(`⏭️ Skipped: ${skippedNames(sum)}`, "bad");
for (const id of sum.potion_out || []) toast(`🧪 Your ${item(id).name.toLowerCase()} ran out`, "bad");
for (const id of Object.keys(sum.potions_drunk || {})) toast(`🧪 Drank another ${item(id).name.toLowerCase()}`);
@ -223,6 +224,7 @@ function awayModal(sum) {
${spilled ? `<p class="muted small">Bank full, sent to the stockpile:</p><ul>${spilled}</ul>` : ""}
${sum.skipped?.length ? `<p class="tag bad">⏭️ Skipped: ${esc(skippedNames(sum))}</p>` : ""}
${sum.stopped ? `<p class="tag bad">Stopped: ${esc(sum.stopped)}</p>` : ""}
${sum.run_over ? `<p class="tag">🏁 ${esc(sum.run_over)}</p>` : ""}
<p><button class="btn primary" data-close>Back to work</button></p>`);
}

View file

@ -1036,7 +1036,7 @@ class DungeonTest(unittest.TestCase):
self.assertIsNone(game.start_run(self.world, self.wil, T0 + 600))
names = []
now = T0 + 600
while game.party_of(self.world, self.wil):
while "run" in self.party():
fight = self.party()["fight"] or {}
if fight.get("stage"):
names.append(fight["enemy"])
@ -1046,10 +1046,16 @@ class DungeonTest(unittest.TestCase):
pulls = game.ACTION_BY_ID["dungeon_watchtower"]["pulls"]
self.assertEqual(list(dict.fromkeys(names)), pulls)
for p in (self.wil, self.kay):
self.assertIsNone(p["action"])
self.assertEqual(p["stopped"], "Cleared Watchtower Depths")
self.assertEqual(p["away"]["run_over"], "Cleared Watchtower Depths")
self.assertGreaterEqual(p["gold"], game.DUNGEONS["dungeon_watchtower"]["bonus_gold"])
self.assertIn("cleared Watchtower Depths", self.world["ledger"][-1]["text"])
# Back in the lobby together: waiting on the leader, open to join, ready to go again.
self.online(now + 600)
self.assertIs(game.party_of(self.world, self.kay), self.party())
self.assertIsNone(self.party()["fight"])
self.assertFalse(game.run_of(self.world, self.wil)["started"])
self.assertTrue(game.joinable(self.world, self.wil))
self.assertIsNone(game.start_run(self.world, self.wil, now + 600))
def test_a_group_waits_for_its_leader_and_holds_four(self):
game.start_action(self.world, self.wil, "dungeon_watchtower", T0)
@ -1096,11 +1102,12 @@ class DungeonTest(unittest.TestCase):
p["xp"]["combat"] = game.XP_TABLE[20]
self.party()["run"] = {"pull": 7, "wipes": 0} # the ogre, naked
now = T0
while game.party_of(self.world, self.wil):
while "run" in self.party():
now += 30
self.online(now)
self.assertLess(now, T0 + 3 * 3600)
self.assertEqual(self.wil["stopped"], "Wiped 3 times at Watchtower ogre")
self.assertEqual(self.wil["away"]["run_over"], "Wiped 3 times at Watchtower ogre")
self.assertEqual(self.party()["members"], ["wil", "kay"])
def test_the_leader_can_pull_early(self):
game.start_action(self.world, self.wil, "dungeon_watchtower", T0)