What was on git could not be installed. foundry-module/ referenced core/ rather than containing it, so an installed copy had no generator at all — it only ever worked here because the live module directory had core/ copied in by hand. core/ is now vendored into foundry-module/module/core/, minus cli.mjs and test.mjs which are desk tools. module.json points manifest and download at release assets, the way mapwright distributes, so a remote server can install by URL without reaching into the repo tree. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
164 lines
7.9 KiB
JavaScript
164 lines
7.9 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';
|
||
|
||
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} · ${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}.`);
|
||
}
|
||
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');
|
||
}
|