Still unreleased. Answering the three blockers. The worksheet and play sheet never rendered the new field, and stripping the prose meant they said nothing at all — the two surfaces the whole two-surface design is about. All five renderers now go through one helper, core/roster.mjs, which also recovers the warning for delve files generated before the field existed and refuses to add a line the prose already carries. Measured across four themes: 40 of 40 delves carrying a roster now warn on worksheet, play sheet and markdown alike. Twenty-three was the wrong set. The spelling was never the semantics: barrow's nightmare Wraith, cave's nightmare Troll and forest's battle Troll carry the same non-discoverable restriction without the word ONLY. Marked, so 26. I audited all 64 rosters rather than trusting the count — the ones left unmarked are discoverable by trying (an Ogre with 12 Grit, mooks that keep coming until the necromancer stops), and damage works on all of them. commit() was unprotected, so a failed settings write threw out of stageArea with the scene already real. It is caught now and says plainly that a retry will raise the area twice. enter() also has a single-flight guard: it is bound to a button, it awaits the Forge for seconds, and two overlapping calls both read the same index. Also: packById checks the geometry is one VANITY can build rather than just truthy; foeSection is exported and the suite holds the two renderers to agreement on all four kinds; and the module README and the seams warning said 0.10.5 where everything else says 0.10.4. 41 core tests, 54 adapter tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
166 lines
8.1 KiB
JavaScript
166 lines
8.1 KiB
JavaScript
/**
|
||
* Two renders of one delve.
|
||
*
|
||
* WORKSHEET is for the desk: everything the generator knows, labelled, with the reroll commands
|
||
* beside each component and an explicit prompt where only a human can write.
|
||
*
|
||
* PLAY is for the table: the GM's own prose, the live leverage point, the decision on one line,
|
||
* the numbers. Nothing else. Four reviews said the same thing — "the handwritten delve tells the
|
||
* GM what matters faster" — and the only way to be fast is to carry less.
|
||
*/
|
||
import { outstanding, isLocked, REROLLABLE } from './authoring.mjs';
|
||
import { initiativeWarning } from './roster.mjs';
|
||
|
||
const cap = s => (s ? s[0].toUpperCase() + s.slice(1) : s);
|
||
const cost = c => {
|
||
if (!c) return 'free';
|
||
const b = [];
|
||
if (c.bane) b.push(`+${c.bane} Bane${c.bane > 1 ? 's' : ''}`);
|
||
if (c.vice) b.push(`the Vice ${c.vice}`);
|
||
return b.length ? b.join(', ') : 'free';
|
||
};
|
||
const au = (d, i) => d.authored?.areas?.[i] ?? {};
|
||
|
||
/* ------------------------------------------------------------- worksheet */
|
||
|
||
export function renderWorksheet(d) {
|
||
const L = [];
|
||
const sk = d.skeleton, ap = sk.appeasement;
|
||
const todo = outstanding(d);
|
||
|
||
L.push(`# WORKSHEET — ${d.authored?.title ?? sk.placeName}`);
|
||
L.push('');
|
||
L.push(`\`seed ${d.seed} · ${d.params.theme} · ${d.params.areas} areas · depth ${d.params.depth} · generator ${d.generatorVersion}\``);
|
||
L.push('');
|
||
L.push(todo.length
|
||
? `**${todo.length} things still need you.** This is a first draft, not a play document. Finish it, then export the play sheet.`
|
||
: `**Finished.** Export the play sheet.`);
|
||
L.push('');
|
||
if (todo.length) { for (const t of todo) L.push(`- [ ] **${t.where}** — ${t.what}`); L.push(''); }
|
||
L.push('---');
|
||
L.push('');
|
||
L.push('## The kernel');
|
||
L.push('');
|
||
L.push(`| | |`);
|
||
L.push(`|---|---|`);
|
||
L.push(`| **Transgression** | ${sk.knot.transgression} |`);
|
||
L.push(`| **Bottom problem** | ${sk.bottomProblem.label} — ${sk.bottomProblem.failureState} |`);
|
||
L.push(`| **Prize** | ${sk.prize.label} — ${sk.prize.function} |`);
|
||
L.push(`| **Motif / danger** | ${sk.motif.id} · ${sk.motif.danger} — ${sk.motif.dangerLine} |`);
|
||
L.push(`| **Appeasement** | ${ap.move} \`[${ap.attribute} ${ap.successes}]\` — +${ap.gain.vanity} Vanity, +${ap.cost.bane} Bane, repeatable |`);
|
||
L.push(`| **It asks** | ${d.ending.question} |`);
|
||
L.push('');
|
||
L.push(`**WRITE ▸ an opening paragraph.** ${d.authored?.intro ? '✔ written' : '_(nothing yet — set `authored.intro`)_'}`);
|
||
if (d.authored?.intro) { L.push(''); L.push(`> ${d.authored.intro}`); }
|
||
L.push('');
|
||
L.push('---');
|
||
L.push('');
|
||
|
||
for (const a of d.areas) {
|
||
const w = au(d, a.index);
|
||
const locks = (w.locked ?? []);
|
||
const rerollable = REROLLABLE.filter(c => !locks.includes(c) && (c !== 'temptation' || a.hoard));
|
||
L.push(`## Area ${a.index} — ${w.nameOverride ?? a.name}`);
|
||
L.push(`\`${a.role} · ${a.facet}${a.encounter ? ` · ${a.encounter.heat}` : ''}${a.hoard ? ` · ${a.hoard}` : ''}\``);
|
||
L.push(`*reroll:* ${rerollable.map(c => `\`--reroll=${a.index}:${c}\``).join(' ')}${locks.length ? ` · *locked:* ${locks.join(', ')}` : ''}`);
|
||
L.push('');
|
||
L.push(`**IMAGE** ${a.cueFragments.join(' · ')}`);
|
||
if (a.situation) {
|
||
L.push(`**SITUATION** ${cap(a.situation.occupant)} — ${a.situation.doing}. On arrival, ${a.situation.onArrival}.`);
|
||
L.push(`**WHY** ${cap(a.situation.because)}. **OFFERS** ${a.situation.offer}.`);
|
||
}
|
||
L.push(`**DECISION** ${cap(a.decision.cue)} — *${a.decision.cost}*`);
|
||
const rv = a.decision.resolve ?? {};
|
||
if (rv.roll) L.push(` · \`[${rv.roll}]\` → ${rv.success}`);
|
||
if (rv.failure) L.push(` · **miss** → ${rv.failure}`);
|
||
if (rv.orElse) L.push(` · **or** ${rv.orElse}`);
|
||
if (a.encounter?.roster) {
|
||
const R = a.encounter.roster;
|
||
L.push(`**ENCOUNTER** ${R.line} — ${R.foes.map(f => `${f.n}× ${f.name} atk${f.atk}/def${f.def}/G${f.grit}`).join(', ')}`);
|
||
L.push(` · harmed by ${R.harmedBy}${initiativeWarning(R) ? ` · ${initiativeWarning(R)}` : ''} · ${R.avoid}`);
|
||
}
|
||
if (a.temptation) {
|
||
const t = a.temptation;
|
||
L.push(`**TEMPTATION** ${cap(t.id)} — ${t.cue}: ${t.benefit}`);
|
||
L.push(` · take ${cost(t.acceptanceCost)} · use ${cost(t.useCost)} · standing: ${t.standingDrawback}`);
|
||
}
|
||
L.push(`**PRESSURE** trigger ${a.trigger}${a.baneBeat ? ` · bane beat ${a.baneBeat}` : ''} · fallback ${a.fallback.route}`);
|
||
L.push('');
|
||
L.push(`**WRITE ▸ read-aloud** — ≤3 sentences, built from the IMAGE and SITUATION above.`);
|
||
L.push(w.readAloud ? `> ${w.readAloud}` : `> _(nothing yet)_`);
|
||
if (w.notes) { L.push(''); L.push(`**Notes.** ${w.notes}`); }
|
||
L.push('');
|
||
L.push('---');
|
||
L.push('');
|
||
}
|
||
|
||
const we = d.authored?.ending ?? {};
|
||
L.push(`## Ending — ${d.ending.name}`);
|
||
L.push(d.ending.authored ? '`AUTHORED SLOT — DELVE stops here on purpose`' : '`generated`');
|
||
L.push('');
|
||
L.push(`**IMAGE** ${d.ending.cueFragments.join(' · ')}`);
|
||
L.push(`**IT WANTS** ${sk.bottomProblem.wantNow}. **IT ASKS** ${d.ending.question}`);
|
||
L.push(`**APPEASED EVEN ONCE** it listens, and will trade ${sk.prize.label}.`);
|
||
L.push('');
|
||
L.push('**WRITE ▸ read-aloud**');
|
||
L.push(we.readAloud ? `> ${we.readAloud}` : `> _(nothing yet)_`);
|
||
L.push('');
|
||
L.push('**WRITE ▸ the climax.** How it goes, what harms it, what it will take instead of a fight.');
|
||
L.push(we.notes ? we.notes : `_(nothing yet — this is the seam; DELVE will not write it for you)_`);
|
||
L.push('');
|
||
return L.join('\n');
|
||
}
|
||
|
||
/* ------------------------------------------------------------ play sheet */
|
||
|
||
export function renderPlay(d) {
|
||
const L = [];
|
||
const sk = d.skeleton, ap = sk.appeasement;
|
||
const todo = outstanding(d);
|
||
|
||
L.push(`# ${d.authored?.title ?? sk.placeName}`);
|
||
L.push('');
|
||
if (todo.length) {
|
||
L.push(`> ⚠ **${todo.length} unfinished** — ${todo.slice(0, 3).map(t => t.where).join(', ')}${todo.length > 3 ? '…' : ''}.`);
|
||
L.push('> Fragments are shown where nothing was written. Run the worksheet first.');
|
||
L.push('');
|
||
}
|
||
if (d.authored?.intro) { L.push(`> ${d.authored.intro}`); L.push(''); }
|
||
L.push(`**Appease:** ${ap.move} \`[${ap.attribute} ${ap.successes}]\` — +1 Vanity now, +1 Bane later. Repeatable, every area.`);
|
||
L.push(`**Clock:** 1d6 on a trigger. **1 — something comes.** **Tab:** ${d.pressure.target} by the end.`);
|
||
L.push(`**GM truth:** ${sk.knot.transgression} ${sk.bottomProblem.label} ${sk.bottomProblem.failureState}.`);
|
||
L.push('');
|
||
L.push('---');
|
||
L.push('');
|
||
|
||
for (const a of d.areas) {
|
||
const w = au(d, a.index);
|
||
const rv = a.decision.resolve ?? {};
|
||
L.push(`### ${a.index} · ${w.nameOverride ?? a.name}`);
|
||
L.push('');
|
||
L.push(w.readAloud ? `> ${w.readAloud}` : `> *(unwritten)* ${a.cueFragments.join('. ')}.`);
|
||
L.push('');
|
||
if (a.situation) L.push(`**Here:** ${cap(a.situation.occupant)}, ${a.situation.doing} — ${a.situation.onArrival}. **They can:** ${a.situation.offer}.`);
|
||
L.push(`**${cap(a.decision.cue)}** — ${rv.roll ? `\`[${rv.roll}]\` ${rv.success}` : ''}${rv.failure ? ` · **miss** ${rv.failure}` : ''}${rv.orElse ? ` · **or** ${rv.orElse}` : ''}`);
|
||
if (a.encounter?.roster) {
|
||
const R = a.encounter.roster;
|
||
L.push(`**${cap(a.encounter.heat)}:** ${R.foes.map(f => `${f.n}× ${f.name} \`${f.atk}/${f.def}/${f.grit}\``).join(' · ')} — harmed by ${R.harmedBy}.`
|
||
+ `${initiativeWarning(R) ? ` **${initiativeWarning(R)}**` : ''}`);
|
||
}
|
||
if (a.temptation) L.push(`**${cap(a.temptation.id)}:** ${a.temptation.benefit}. *Use: ${cost(a.temptation.useCost)}. ${a.temptation.standingDrawback}.*`);
|
||
if (w.notes) L.push(`**Note:** ${w.notes}`);
|
||
L.push(`\`${a.trigger}${a.baneBeat ? ` · ${a.baneBeat}` : ''} · skipped → move the image on\``);
|
||
L.push('');
|
||
}
|
||
|
||
const we = d.authored?.ending ?? {};
|
||
L.push(`### ${d.ending.name}`);
|
||
L.push('');
|
||
L.push(we.readAloud ? `> ${we.readAloud}` : `> *(unwritten)* ${d.ending.cueFragments.join('. ')}.`);
|
||
L.push('');
|
||
L.push(`**It asks:** ${d.ending.question}`);
|
||
L.push(we.notes ? we.notes : `*(the climax is unwritten — this is the authored slot)*`);
|
||
L.push('');
|
||
return L.join('\n');
|
||
}
|