Dungeons wait for the leader to start
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
e43611e618
commit
cf91cbce3c
4 changed files with 60 additions and 15 deletions
2
app.py
2
app.py
|
|
@ -369,6 +369,8 @@ class Handler(BaseHTTPRequestHandler):
|
|||
return game.learn_pattern(player, str(body.get("item")))
|
||||
if path == "/api/pull":
|
||||
return game.pull_now(world, player, now)
|
||||
if path == "/api/run":
|
||||
return game.start_run(world, player, now)
|
||||
if path == "/api/manual":
|
||||
return game.set_manual(world, player, str(body.get("id")), bool(body.get("on")), now)
|
||||
if path == "/api/order":
|
||||
|
|
|
|||
33
game.py
33
game.py
|
|
@ -3177,7 +3177,7 @@ def joinable(world: dict, player: dict) -> bool:
|
|||
action = ACTION_BY_ID[current["id"]]
|
||||
if action.get("dungeon"):
|
||||
party = world["parties"].get(current.get("party"))
|
||||
return not party or (not party.get("run", {}).get("pull") and len(party["members"]) < DUNGEON_SIZE)
|
||||
return not party or ("run" not in party and len(party["members"]) < DUNGEON_SIZE)
|
||||
return "enemy" in action and not action.get("special")
|
||||
|
||||
|
||||
|
|
@ -3198,7 +3198,7 @@ def join_fight(world: dict, player: dict, uid: str, now: float) -> str | None:
|
|||
return "No one to join"
|
||||
run = party_of(world, host)
|
||||
if run and ACTION_BY_ID[run["action"]].get("dungeon") and run is not party_of(world, player):
|
||||
if run.get("run", {}).get("pull") or (run["fight"] and now >= run["start"] + run["fight"]["rest"] / run["fight"]["pace"]):
|
||||
if "run" in run:
|
||||
return "The run has started"
|
||||
if len(run["members"]) >= DUNGEON_SIZE:
|
||||
return "The group is full"
|
||||
|
|
@ -3282,14 +3282,14 @@ def process_party(world: dict, pid: str, now: float, rng: random.Random) -> None
|
|||
break
|
||||
members = [world["players"][uid] for uid in party["members"]]
|
||||
if action.get("dungeon"):
|
||||
# A run waits for anyone who's looked away: it's played together.
|
||||
# A run waits for its leader to start it, then for anyone who's looked away: it's played together.
|
||||
away = [p for p in members if p["last_active"] < party["start"] - DUNGEON_AWAY]
|
||||
if away:
|
||||
if "run" not in party or away:
|
||||
party["start"] = max(party["start"], now)
|
||||
for player in members:
|
||||
player["action"]["start"] = party["start"]
|
||||
break
|
||||
party["fight"] = draw_pull(world, members, action, party.setdefault("run", {"pull": 0, "wipes": 0}),
|
||||
party["fight"] = draw_pull(world, members, action, party["run"],
|
||||
rng, party["start"])
|
||||
else:
|
||||
party["fight"] = draw_fight(world, members, action, rng, party["start"])
|
||||
|
|
@ -3402,7 +3402,27 @@ def run_of(world: dict, player: dict) -> dict | None:
|
|||
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"],
|
||||
"next": spec["pulls"][min(run["pull"], len(spec["pulls"]) - 1)][0], "away": away,
|
||||
"leader": party["members"][0], "size": len(party["members"])}
|
||||
"leader": party["members"][0], "size": len(party["members"]), "started": "run" in party}
|
||||
|
||||
|
||||
def start_run(world: dict, player: dict, now: float) -> str | None:
|
||||
"""The leader takes the group in: no one else can join from here on."""
|
||||
party = party_of(world, player)
|
||||
if not party or not ACTION_BY_ID[party["action"]].get("dungeon"):
|
||||
return "Not in a dungeon"
|
||||
if party["members"][0] != player["id"]:
|
||||
return "Only the leader starts"
|
||||
if "run" in party:
|
||||
return "Already started"
|
||||
if len(party["members"]) < GROUP_SIZE:
|
||||
return f"Needs {GROUP_SIZE} or more"
|
||||
party["run"] = {"pull": 0, "wipes": 0}
|
||||
party["start"] = now
|
||||
for uid in party["members"]:
|
||||
world["players"][uid]["action"]["start"] = now
|
||||
spec = DUNGEONS[ACTION_BY_ID[party["action"]]["dungeon"]]
|
||||
log(world, now, "party", f"{player['name']} led {len(party['members'])} into {spec['name']}", player["id"])
|
||||
return None
|
||||
|
||||
|
||||
def pull_now(world: dict, player: dict, now: float) -> str | None:
|
||||
|
|
@ -5978,6 +5998,7 @@ PATCH_NOTES = [
|
|||
{"date": "2026-10-25", "title": "Titles", "notes": [
|
||||
("new", "Fishing 50 also earns a second title: Master baiter."),
|
||||
("balance", "Boiling hide glue now uses the kitchen fire: while it's lit, glue costs no logs."),
|
||||
("fix", "Dungeons wait for the leader to press Start, so groups of 3 and 4 can form. No one can join once it starts."),
|
||||
]},
|
||||
{"date": "2026-10-24", "title": "Dungeon loot", "notes": [
|
||||
("new", "Masterwork patterns: every dungeon boss can drop one of 7 (15% from a mini-boss, 35% from the last boss, "
|
||||
|
|
|
|||
|
|
@ -916,11 +916,15 @@ function combatFrame() {
|
|||
</div>`;
|
||||
const you = unit(S.uid, esc(me.name)) + others.map((m) => unit(m.id, esc(m.name))).join("");
|
||||
const run = a?.run;
|
||||
// A dungeon run: where you are in it, and (for the leader, while resting) the button to go in early.
|
||||
// A dungeon run: where you are in it, and (for the leader) the button to start it, or to go in early while resting.
|
||||
const lead = run?.leader === S.uid;
|
||||
const stage = run ? `<div class="run-head"><b>${esc(run.dungeon)}</b>
|
||||
<span class="muted small">${run.pull} / ${run.of} · ${esc(run.next)}${run.wipes ? ` · wipes ${run.wipes} / ${C.dungeon_wipes}` : ""}</span>
|
||||
${f && run.leader === S.uid ? `<button class="btn sm" data-pull data-pull-btn>Pull now</button>` : ""}</div>` : "";
|
||||
let foe = `<div class="foe-idle muted">${run?.away?.length ? `Waiting for ${run.away.map(esc).join(", ")} to come back`
|
||||
${!run.started && lead ? `<button class="btn sm primary" data-start-run title="No one can join once you start">Start</button>`
|
||||
: f && lead ? `<button class="btn sm" data-pull data-pull-btn>Pull now</button>` : ""}</div>` : "";
|
||||
const group = run && [S.uid, ...(a.with || [])].map((uid) => uid === S.uid ? "You" : esc(playerName(uid))).join(", ");
|
||||
let foe = `<div class="foe-idle muted">${run && !run.started ? `👥 ${run.size} / ${C.dungeon_size} · ${group}<br>${lead ? "Start when everyone's in" : `${esc(playerName(run.leader))} starts the run`}`
|
||||
: run?.away?.length ? `Waiting for ${run.away.map(esc).join(", ")} to come back`
|
||||
: run ? `Ready: ${esc(run.next)}` : waiting ? `👥 Waiting for a group · 1 / ${C.group_size}` : "Not fighting."}</div>`;
|
||||
if (f) {
|
||||
const mine = C.styles[me.fighter.style || "sword"]?.damage || me.fighter.damage;
|
||||
|
|
@ -2341,6 +2345,7 @@ document.addEventListener("click", (e) => {
|
|||
}
|
||||
if (d.mode) return act("/api/manual", { id: d.mode, on: !S.me.manual.includes(d.mode) });
|
||||
if ("pull" in d) return act("/api/pull", {});
|
||||
if ("startRun" in d) return act("/api/run", {});
|
||||
if (d.learn) return act("/api/learn", { item: d.learn });
|
||||
if (d.eat !== undefined) return act("/api/eat", { below: Number(d.eat) });
|
||||
if (d.aim) {
|
||||
|
|
@ -2603,7 +2608,7 @@ document.addEventListener("visibilitychange", () => { if (!document.hidden) refr
|
|||
return;
|
||||
}
|
||||
setInterval(() => {
|
||||
if (!document.hidden && Date.now() - polledAt >= (S?.me?.action?.fight ? FIGHT_POLL_MS : POLL_MS)) refresh();
|
||||
if (!document.hidden && Date.now() - polledAt >= (S?.me?.action?.fight || S?.me?.action?.run ? FIGHT_POLL_MS : POLL_MS)) refresh();
|
||||
}, 500);
|
||||
requestAnimationFrame(loop);
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -884,6 +884,7 @@ class DungeonTest(unittest.TestCase):
|
|||
self.online(T0 + 600)
|
||||
self.assertNotIn("fight", self.wil["action"]) # alone, it waits
|
||||
self.assertIsNone(game.join_fight(self.world, self.kay, "wil", T0 + 600))
|
||||
self.assertIsNone(game.start_run(self.world, self.wil, T0 + 600))
|
||||
names = []
|
||||
now = T0 + 600
|
||||
while game.party_of(self.world, self.wil):
|
||||
|
|
@ -901,21 +902,36 @@ class DungeonTest(unittest.TestCase):
|
|||
self.assertGreaterEqual(p["gold"], game.DUNGEONS["dungeon_watchtower"]["bonus_gold"])
|
||||
self.assertIn("cleared Watchtower Depths", self.world["ledger"][-1]["text"])
|
||||
|
||||
def test_joining_only_before_the_first_pull_and_up_to_four(self):
|
||||
def test_a_group_waits_for_its_leader_and_holds_four(self):
|
||||
game.start_action(self.world, self.wil, "dungeon_watchtower", T0)
|
||||
self.assertIsNone(game.join_fight(self.world, self.kay, "wil", T0))
|
||||
self.online(T0 + 1)
|
||||
self.assertIsNone(game.join_fight(self.world, self.ann, "wil", T0 + 1)) # still resting: in time
|
||||
self.party()["run"]["pull"] = 1
|
||||
self.online(T0 + 600)
|
||||
self.assertIsNone(self.party()["fight"]) # two in: still waiting for more
|
||||
self.assertFalse(game.run_of(self.world, self.kay)["started"])
|
||||
self.assertIsNone(game.join_fight(self.world, self.ann, "wil", T0 + 600))
|
||||
extra = game.new_player("bo", "Bo", "ranger", T0)
|
||||
extra["xp"]["combat"] = game.XP_TABLE[60]
|
||||
self.world["players"]["bo"] = extra
|
||||
self.assertEqual(game.join_fight(self.world, extra, "wil", T0 + 2), "The run has started")
|
||||
self.assertIsNone(game.join_fight(self.world, extra, "wil", T0 + 600))
|
||||
self.assertEqual(len(self.party()["members"]), 4)
|
||||
self.assertFalse(game.joinable(self.world, self.wil))
|
||||
self.assertEqual(game.start_run(self.world, self.kay, T0 + 601), "Only the leader starts")
|
||||
self.assertIsNone(game.start_run(self.world, self.wil, T0 + 601))
|
||||
self.online(T0 + 602)
|
||||
self.assertEqual(len(self.party()["fight"]["members"]), 4)
|
||||
game.leave_party(self.world, extra, self.party()["id"], T0 + 603)
|
||||
extra["action"] = None
|
||||
self.assertFalse(game.joinable(self.world, self.wil))
|
||||
self.assertEqual(game.join_fight(self.world, extra, "wil", T0 + 604), "The run has started")
|
||||
|
||||
def test_no_run_alone(self):
|
||||
game.start_action(self.world, self.wil, "dungeon_watchtower", T0)
|
||||
self.assertEqual(game.start_run(self.world, self.wil, T0), "Not in a dungeon")
|
||||
|
||||
def test_a_run_waits_for_whoever_looked_away(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)
|
||||
self.online(T0 + 300, [self.wil]) # kay went to make tea
|
||||
self.online(T0 + 1200, [self.wil])
|
||||
self.assertIsNone(self.party()["fight"])
|
||||
|
|
@ -940,6 +956,7 @@ class DungeonTest(unittest.TestCase):
|
|||
def test_the_leader_can_pull_early(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)
|
||||
self.kay["hp"] = 1
|
||||
self.online(T0 + 0.5)
|
||||
fight = self.party()["fight"]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue