Prose wins over the field; the play sheet carries avoid; the guard is testable
Still unreleased, but module.json is at 0.6.5 now. initiativeWarning checked the field before the prose, so a hand-edited file carrying both printed the warning twice — the exact fault the migration was meant to end. Prose is checked first now and always wins. The play sheet was the only surface that did not render `avoid`, so three legacy rosters whose warning lived there — church, forest and temple at nightmare — lost it entirely: the helper suppressed its fallback on finding prose the play sheet never showed. It renders `avoid` now, which it should have anyway; it is how the table gets past a fight without having one, which is table-facing by definition. All five surfaces now render both prose fields, so "the prose already says it" means the same thing everywhere. Replayed all 64 rosters in their pre-field form through every surface: 26 need a warning, 130 of 130 checks say it exactly once. The current packs give the same result. commit() no longer mutates st before the write — it passes an incremented copy, so a failed write really does leave the state as it was, which is what the retry warning promises. The single-flight guard moved into stage.mjs as singleFlight() and has four tests: overlapping calls never run together, the second is refused rather than queued, the guard clears afterwards, and a throw does not wedge it. It closes the window within one client only; two GM browsers still race, and that needs a world-level lock rather than a module variable. 41 core tests, 60 adapter tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
1b2f9aa385
commit
c1e467c91b
@@ -17,16 +17,13 @@
|
||||
import { coinSeed, Rng } from './core/rng.mjs';
|
||||
import { newWorkingFile, outstanding, readyToPlay } from './core/authoring.mjs';
|
||||
import { DelveForgeApp, raiseDungeon, listDungeons, removeDungeon, removeDungeonDialog, setThemes } from './forge-app.mjs';
|
||||
import { stageArea } from './stage.mjs';
|
||||
import { stageArea, singleFlight } from './stage.mjs';
|
||||
|
||||
const MOD = 'vanity-delve';
|
||||
const FLAG = 'state';
|
||||
|
||||
let PACK = null;
|
||||
let seamsPresent = false;
|
||||
// enter() is bound to a button a GM can double-press, and it awaits the Forge for seconds at a
|
||||
// time. Two overlapping calls would both read the same st.at and stage the same area twice.
|
||||
let staging = false;
|
||||
let loadPack = async () => null;
|
||||
|
||||
/** The geometries VANITY's Forge can actually build — mirrors validate-pack.mjs. */
|
||||
@@ -122,11 +119,10 @@ async function draft(params = {}) {
|
||||
return load(d);
|
||||
}
|
||||
|
||||
async function enter() {
|
||||
if (staging) return ui.notifications.warn('DELVE: already raising an area — wait for it to finish.');
|
||||
staging = true;
|
||||
try { return await enterOnce(); } finally { staging = false; }
|
||||
}
|
||||
const enter = singleFlight(
|
||||
() => enterOnce(),
|
||||
() => ui.notifications.warn('DELVE: already raising an area — wait for it to finish.'),
|
||||
);
|
||||
|
||||
async function enterOnce() {
|
||||
const st = getState();
|
||||
@@ -152,7 +148,9 @@ async function enterOnce() {
|
||||
...(seamsPresent ? { hoard: false, post: false, folderId: st.folderId } : {}),
|
||||
}),
|
||||
hoard: size => game.vanity.forge.hoard({ size, ...(seamsPresent ? { post: false } : {}) }),
|
||||
commit: async () => { st.at += 1; st.turn += 1; await setState(st); },
|
||||
// An incremented copy, never a mutation of st: a failed write must leave the in-memory state
|
||||
// exactly as it was, or the retry advice in the warning is wrong.
|
||||
commit: () => setState({ ...st, at: st.at + 1, turn: st.turn + 1 }),
|
||||
readAloud: readAloudCard,
|
||||
gm: gmCard,
|
||||
warn: m => ui.notifications.warn(m),
|
||||
|
||||
Reference in New Issue
Block a user