@scrthq/runlog 0.0.39 → 0.0.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +1 -0
  2. package/dist/runlog.js +912 -28
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -7,6 +7,7 @@ release, and issue sealed copies to buyers.
7
7
  ```bash
8
8
  npx @scrthq/runlog validate my-game.yaml --strict
9
9
  npx @scrthq/runlog test my-game.yaml
10
+ npx @scrthq/runlog docs my-game.yaml -o dist/docs # rulebook, quick start, reference card, run log sheet, summary
10
11
  npx @scrthq/runlog keygen -o my-key.json
11
12
  npx @scrthq/runlog sign my-game.yaml --key my-key.json --as "Your Name"
12
13
  npx @scrthq/runlog issue my-game.yaml --to "Buyer" --ref order-1 --key my-key.json --seal
package/dist/runlog.js CHANGED
@@ -7369,8 +7369,8 @@ var require_dist = __commonJS({
7369
7369
 
7370
7370
  // packages/cli/src/bin.ts
7371
7371
  var import_yaml4 = __toESM(require_dist(), 1);
7372
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, existsSync as existsSync3 } from "node:fs";
7373
- import { basename, dirname, resolve as resolve3 } from "node:path";
7372
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync3, existsSync as existsSync3 } from "node:fs";
7373
+ import { basename as basename2, dirname, resolve as resolve4 } from "node:path";
7374
7374
  import { createHash } from "node:crypto";
7375
7375
 
7376
7376
  // node_modules/zod/v4/classic/external.js
@@ -17794,8 +17794,8 @@ var error43 = () => {
17794
17794
  case "not_multiple_of":
17795
17795
  return `N\xFAmero inv\xE1lido: deve ser m\xFAltiplo de ${issue2.divisor}`;
17796
17796
  case "unrecognized_keys": {
17797
- const plural = issue2.keys.length > 1 ? "s" : "";
17798
- return `Chave${plural} inv\xE1lida${plural}: ${joinValues(issue2.keys, ", ")}`;
17797
+ const plural2 = issue2.keys.length > 1 ? "s" : "";
17798
+ return `Chave${plural2} inv\xE1lida${plural2}: ${joinValues(issue2.keys, ", ")}`;
17799
17799
  }
17800
17800
  case "invalid_key":
17801
17801
  return `Entrada inv\xE1lida n${translateOriginWithArticle(issue2.origin, "definite")}`;
@@ -17937,8 +17937,8 @@ var error44 = () => {
17937
17937
  case "not_multiple_of":
17938
17938
  return `N\xFAmero inv\xE1lido: deve ser m\xFAltiplo de ${issue2.divisor}`;
17939
17939
  case "unrecognized_keys": {
17940
- const plural = issue2.keys.length > 1 ? "s" : "";
17941
- return `Chave${plural} inv\xE1lida${plural}: ${joinValues(issue2.keys, ", ")}`;
17940
+ const plural2 = issue2.keys.length > 1 ? "s" : "";
17941
+ return `Chave${plural2} inv\xE1lida${plural2}: ${joinValues(issue2.keys, ", ")}`;
17942
17942
  }
17943
17943
  case "invalid_key":
17944
17944
  return `Entrada inv\xE1lida n${translateOriginWithArticle(issue2.origin, "definite")}`;
@@ -20135,9 +20135,9 @@ function codePointLengthVar(doc, ctx, accessor, inDoubt) {
20135
20135
  doc.write(`const ${v} = typeof ${accessor} === "string" && ${inDoubt} ? ${cpLen}(${accessor}) : ${accessor}.length;`);
20136
20136
  return v;
20137
20137
  }
20138
- function numericOperand(value, label) {
20138
+ function numericOperand(value, label2) {
20139
20139
  if (typeof value !== "number" || !Number.isFinite(value)) {
20140
- throw new ZodCompileUnsupportedError(`${label} bound of type ${typeof value}`);
20140
+ throw new ZodCompileUnsupportedError(`${label2} bound of type ${typeof value}`);
20141
20141
  }
20142
20142
  return `${value}`;
20143
20143
  }
@@ -26617,7 +26617,7 @@ var Deck = external_exports.discriminatedUnion("kind", [
26617
26617
  ),
26618
26618
  resolveBy: external_exports.enum(["rank", "suit"]).default("rank").describe("Whether the draw's rank or its suit selects the table entry.")
26619
26619
  }).strict().describe(
26620
- "A standard 52-card deck. Included because a whole family of solo journalling games is built on drawing from one."
26620
+ "A standard 52-card deck. Included because a whole family of solo journaling games is built on drawing from one."
26621
26621
  )
26622
26622
  ]).describe("A deck the game draws from.");
26623
26623
 
@@ -26794,7 +26794,7 @@ var JournalDef = external_exports.object({
26794
26794
  prompt: external_exports.string().optional().describe("The question shown above the note field."),
26795
26795
  required: external_exports.boolean().default(false).describe("Require a note before a unit can be closed.")
26796
26796
  }).strict().describe(
26797
- "Free-text notes attached to units. Essential for journalling games, and useful anywhere, since a log of rolls alone does not tell you what you actually made."
26797
+ "Free-text notes attached to units. Essential for journaling games, and useful anywhere, since a log of rolls alone does not tell you what you actually made."
26798
26798
  );
26799
26799
  var EndingDef = external_exports.object({
26800
26800
  id: Id.describe("Unique among endings."),
@@ -27524,13 +27524,13 @@ function lintPack(pack) {
27524
27524
  }
27525
27525
  }
27526
27526
  const claimed = new Set(pack.capabilities);
27527
- const requireCap = (cap, used, why) => {
27528
- if (used && !claimed.has(cap)) {
27527
+ const requireCap = (cap2, used, why) => {
27528
+ if (used && !claimed.has(cap2)) {
27529
27529
  d.push(
27530
27530
  warn(
27531
27531
  "capability/undeclared",
27532
27532
  "capabilities",
27533
- `pack ${why} but does not declare the "${cap}" capability`
27533
+ `pack ${why} but does not declare the "${cap2}" capability`
27534
27534
  )
27535
27535
  );
27536
27536
  }
@@ -27638,12 +27638,12 @@ function parsePack(input2) {
27638
27638
  const unsupported = pack.capabilities.filter(
27639
27639
  (c) => !IMPLEMENTED_CAPABILITIES.includes(c)
27640
27640
  );
27641
- for (const cap of unsupported) {
27641
+ for (const cap2 of unsupported) {
27642
27642
  diagnostics.push({
27643
27643
  level: "error",
27644
27644
  code: "pack/unsupported-capability",
27645
27645
  path: "capabilities",
27646
- message: `pack requires the "${cap}" capability, which this build does not implement`
27646
+ message: `pack requires the "${cap2}" capability, which this build does not implement`
27647
27647
  });
27648
27648
  }
27649
27649
  if (hasErrors(diagnostics)) return { ok: false, pack: null, diagnostics };
@@ -27885,6 +27885,830 @@ async function verifyPack(document) {
27885
27885
  }
27886
27886
  }
27887
27887
 
27888
+ // packages/rules-schema/src/describe.ts
27889
+ function entryKeys(table) {
27890
+ switch (table.resolution) {
27891
+ case "lookup":
27892
+ return table.entries.map((e) => e.range[0] === e.range[1] ? `${e.range[0]}` : `${e.range[0]}\u2013${e.range[1]}`);
27893
+ case "bands":
27894
+ return table.entries.map(
27895
+ (e) => [e.gte !== void 0 ? `\u2265${e.gte}` : "", e.lte !== void 0 ? `\u2264${e.lte}` : ""].filter(Boolean).join(" ")
27896
+ );
27897
+ case "opposed":
27898
+ return table.entries.map((e) => `beats ${e.beats}`);
27899
+ case "keyed":
27900
+ return table.entries.map((e) => e.key);
27901
+ }
27902
+ }
27903
+ function howConsulted(table) {
27904
+ switch (table.resolution) {
27905
+ case "lookup":
27906
+ return `Roll ${table.roll}.`;
27907
+ case "bands":
27908
+ return `Roll ${table.roll} and find the band it lands in.`;
27909
+ case "opposed":
27910
+ return `Roll ${table.action}${table.addResource ? ` and add ${table.addResource}` : ""} against ${table.challenge.count} \xD7 ${table.challenge.dice}; count how many of the challenge dice your total beats.`;
27911
+ case "keyed":
27912
+ return "Find the row whose key matches what was drawn.";
27913
+ }
27914
+ }
27915
+ function nouns(pack) {
27916
+ const v = pack.vocabulary;
27917
+ return {
27918
+ run: v.run.one,
27919
+ runs: v.run.many,
27920
+ unit: v.unit.one,
27921
+ units: v.unit.many,
27922
+ subject: v.subject.one,
27923
+ subjects: v.subject.many,
27924
+ finalize: v.finalize
27925
+ };
27926
+ }
27927
+ var label = (pack, kind, id) => {
27928
+ const entry = pack[kind]?.[id];
27929
+ return entry?.label ?? entry?.title ?? id;
27930
+ };
27931
+ function targetInWords(pack, to) {
27932
+ const n = nouns(pack);
27933
+ if (to === void 0) return `this ${n.subject}`;
27934
+ if (typeof to === "object") return `the ${n.subject} chosen as \u201C${to.var}\u201D`;
27935
+ switch (to) {
27936
+ case "thisSubject":
27937
+ return `this ${n.subject}`;
27938
+ case "targetSubject":
27939
+ return `the target`;
27940
+ case "allSubjects":
27941
+ return `every ${n.subject}`;
27942
+ case "allPriorSubjects":
27943
+ return `every earlier ${n.subject}`;
27944
+ case "run":
27945
+ return `the ${n.run}`;
27946
+ }
27947
+ }
27948
+ function boundInWords(pack, b) {
27949
+ const parts = [];
27950
+ if (b.eq !== void 0) parts.push(`exactly ${b.eq}`);
27951
+ if (b.gte !== void 0 && b.lte !== void 0) parts.push(`between ${b.gte} and ${b.lte}`);
27952
+ else {
27953
+ if (b.gte !== void 0) parts.push(`${b.gte} or more`);
27954
+ if (b.lte !== void 0) parts.push(`${b.lte} or fewer`);
27955
+ }
27956
+ if (b.gteCounter !== void 0) parts.push(`at least the ${label(pack, "counters", b.gteCounter)} tally`);
27957
+ if (b.lteCounter !== void 0) parts.push(`no more than the ${label(pack, "counters", b.lteCounter)} tally`);
27958
+ return parts.join(" and ");
27959
+ }
27960
+ function predicateInWords(pack, p) {
27961
+ const n = nouns(pack);
27962
+ if ("ask" in p) return `you answer yes to \u201C${p.ask}\u201D`;
27963
+ if ("unitIndex" in p) return p.unitIndex.eq !== void 0 ? `this is ${n.unit} ${p.unitIndex.eq}` : `the ${n.unit} number is ${boundInWords(pack, p.unitIndex)}`;
27964
+ if ("subjectCount" in p) return `there are ${boundInWords(pack, p.subjectCount)} ${n.subjects}`;
27965
+ if ("eligibleTargets" in p) return `there are ${boundInWords(pack, p.eligibleTargets)} ${n.subjects} that can be targeted`;
27966
+ if ("counter" in p) return `${label(pack, "counters", p.counter)} is ${boundInWords(pack, p.is)}`;
27967
+ if ("resource" in p) return `${label(pack, "resources", p.resource)} is ${boundInWords(pack, p.is)}`;
27968
+ if ("flag" in p) return `${p.flag} is ${p.is === false ? "not " : ""}set`;
27969
+ if ("subjectHasState" in p) return `${targetInWords(pack, p.of)} is ${label(pack, "states", p.subjectHasState)}`;
27970
+ if ("priorSubjectTagged" in p) return `an earlier ${n.subject} was tagged \u201C${p.priorSubjectTagged}\u201D`;
27971
+ if ("modeIs" in p) return `playing ${p.modeIs.map((m) => label(pack, "modes", m)).join(" or ")}`;
27972
+ if ("not" in p) return `not (${predicateInWords(pack, p.not)})`;
27973
+ if ("allOf" in p) return p.allOf.map((q) => predicateInWords(pack, q)).join(" and ");
27974
+ if ("anyOf" in p) return p.anyOf.map((q) => predicateInWords(pack, q)).join(" or ");
27975
+ return "";
27976
+ }
27977
+ function conditionsInWords(pack, preds, joiner = "all") {
27978
+ if (!preds || preds.length === 0) return "";
27979
+ return preds.map((p) => predicateInWords(pack, p)).join(joiner === "all" ? " and " : ", or ");
27980
+ }
27981
+ var signed = (by, set2) => set2 !== void 0 ? `set to ${set2}` : by === void 0 ? "+1" : by >= 0 ? `+${by}` : `\u2212${Math.abs(by)}`;
27982
+ function actionInWords(pack, a) {
27983
+ const n = nouns(pack);
27984
+ switch (a.do) {
27985
+ case "roll":
27986
+ return a.label ? `${a.label} (${a.dice})` : `roll ${a.dice}`;
27987
+ case "rollOn": {
27988
+ const t = label(pack, "tables", a.table);
27989
+ if (a.times && a.times > 1) return `roll ${a.times} times on ${t}${a.choose === "one" ? " and keep one" : ""}`;
27990
+ return `roll on ${t}`;
27991
+ }
27992
+ case "branch": {
27993
+ const cases = a.cases.map((c) => {
27994
+ const cond = c.in ? `on ${c.in.join(", ")}` : c.is ? boundInWords(pack, c.is) : "otherwise";
27995
+ return `${cond} \u2192 ${actionsInWords(pack, c.then)}`;
27996
+ });
27997
+ if (a.else) cases.push(`otherwise \u2192 ${actionsInWords(pack, a.else)}`);
27998
+ return `then, by the result: ${cases.join("; ")}`;
27999
+ }
28000
+ case "prompt":
28001
+ return a.label;
28002
+ case "resolveTarget":
28003
+ return `pick the target${a.from === "choice" ? " yourself" : a.from === "event" ? " by the pack's targeting rule" : " from the roll"}`;
28004
+ case "applyState":
28005
+ return `mark ${targetInWords(pack, a.to)} ${label(pack, "states", a.state)}`;
28006
+ case "removeState":
28007
+ return `clear ${label(pack, "states", a.state)} from ${targetInWords(pack, a.to)}`;
28008
+ case "removeSubject":
28009
+ return `remove ${targetInWords(pack, a.to)} from play`;
28010
+ case "ban":
28011
+ return a.label ?? (a.subjectType ? `no more ${a.subjectType} ${n.subjects} this ${n.run}` : `no more ${n.subjects} of ${targetInWords(pack, a.from)}'s kind this ${n.run}`);
28012
+ case "forceUnit": {
28013
+ const c = a.count ?? 1;
28014
+ return `add ${c} more ${c === 1 ? n.unit : n.units} before the ${n.run} may end`;
28015
+ }
28016
+ case "modCounter":
28017
+ return `${label(pack, "counters", a.counter)} ${signed(a.by, a.set)}`;
28018
+ case "modResource":
28019
+ return `${label(pack, "resources", a.resource)} ${signed(a.by, a.set)}`;
28020
+ case "grantCard": {
28021
+ const c = a.count ?? 1;
28022
+ return `draw ${c} from ${label(pack, "decks", a.deck)}`;
28023
+ }
28024
+ case "discardCard": {
28025
+ const c = a.count ?? 1;
28026
+ return `discard ${c} from ${label(pack, "decks", a.deck)}`;
28027
+ }
28028
+ case "startTimer":
28029
+ return a.label ? `${a.label} (${a.minutes} min)` : `start a ${a.minutes}-minute timer`;
28030
+ case "note":
28031
+ return a.text;
28032
+ case "setFlag":
28033
+ return a.value === false ? `unset ${a.flag}` : `set ${a.flag}`;
28034
+ case "endRunAttempt":
28035
+ return `try to end the ${n.run}`;
28036
+ case "when": {
28037
+ const then = `if ${conditionsInWords(pack, a.all)}: ${actionsInWords(pack, a.then)}`;
28038
+ return a.else ? `${then}; otherwise ${actionsInWords(pack, a.else)}` : then;
28039
+ }
28040
+ }
28041
+ }
28042
+ function actionsInWords(pack, actions) {
28043
+ return actions.map((a) => unstop(actionInWords(pack, a))).join("; ");
28044
+ }
28045
+ function unstop(s) {
28046
+ return s.replace(/\.\s*$/, "");
28047
+ }
28048
+ function sentence(s) {
28049
+ const t = s.trim();
28050
+ return /[.!?…]$/.test(t) ? t : `${t}.`;
28051
+ }
28052
+ function triggerPointInWords(pack, on) {
28053
+ const n = nouns(pack);
28054
+ switch (on) {
28055
+ case "immediately":
28056
+ return "Then";
28057
+ case "onEnterUnit":
28058
+ return `When a ${n.unit} begins`;
28059
+ case "onDeclareSubject":
28060
+ return `When the ${n.subject} is declared`;
28061
+ case "afterWork":
28062
+ return "After the work";
28063
+ case "onFinalize":
28064
+ return `When you ${n.finalize}`;
28065
+ case "onDeclareRunOver":
28066
+ return `When you declare the ${n.run} over`;
28067
+ case "onRunEnd":
28068
+ return `When the ${n.run} ends`;
28069
+ }
28070
+ }
28071
+ function triggerInWords(pack, t) {
28072
+ const when = conditionsInWords(pack, t.when);
28073
+ const head = t.label ? `${t.label} \u2014 ${triggerPointInWords(pack, t.on).toLowerCase()}` : triggerPointInWords(pack, t.on);
28074
+ return `${head}${when ? ` (if ${when})` : ""}: ${actionsInWords(pack, t.do)}`;
28075
+ }
28076
+ function modeLength(pack, mode) {
28077
+ const n = nouns(pack);
28078
+ const u = mode.units;
28079
+ if (u?.fixed !== void 0) return `${u.fixed} ${u.fixed === 1 ? n.unit : n.units}`;
28080
+ if (u?.roll) return `roll ${u.roll} for the number of ${n.units}`;
28081
+ if (u?.min !== void 0 && u?.max !== void 0) return `${u.min}\u2013${u.max} ${n.units}`;
28082
+ if (u?.max !== void 0) return `up to ${u.max} ${n.units}`;
28083
+ if (u?.min !== void 0) return `at least ${u.min} ${n.units}`;
28084
+ const max = pack.unit.max;
28085
+ return `as many ${n.units} as you like${max ? `, up to ${max}` : ""}`;
28086
+ }
28087
+ function modePlayers(mode) {
28088
+ const p = mode.players;
28089
+ if (!p || p.min <= 1 && p.max <= 1) return "solo";
28090
+ return p.min === p.max ? `${p.min} players` : `${p.min}\u2013${p.max} players`;
28091
+ }
28092
+ var diceOf = (expr) => {
28093
+ const m = /^(\d*)d(\d+)/.exec(expr);
28094
+ return m ? `d${m[2]}` : expr;
28095
+ };
28096
+ function diceNeeded(pack) {
28097
+ const found = /* @__PURE__ */ new Set();
28098
+ const seeActions = (actions) => {
28099
+ for (const a of actions ?? []) {
28100
+ if (a.do === "roll") found.add(diceOf(a.dice));
28101
+ if (a.do === "branch") {
28102
+ for (const c of a.cases) seeActions(c.then);
28103
+ seeActions(a.else);
28104
+ }
28105
+ if (a.do === "when") {
28106
+ seeActions(a.then);
28107
+ seeActions(a.else);
28108
+ }
28109
+ }
28110
+ };
28111
+ const seeTriggers = (triggers) => {
28112
+ for (const t of triggers ?? []) seeActions(t.do);
28113
+ };
28114
+ for (const table of Object.values(pack.tables)) {
28115
+ if (table.resolution === "lookup" || table.resolution === "bands") found.add(diceOf(table.roll));
28116
+ if (table.resolution === "opposed") {
28117
+ found.add(diceOf(table.action));
28118
+ found.add(diceOf(table.challenge.dice));
28119
+ }
28120
+ for (const e of table.entries) seeTriggers(e.triggers);
28121
+ }
28122
+ for (const deck of Object.values(pack.decks ?? {})) if (deck.kind === "cards") for (const c of deck.cards) seeTriggers(c.triggers);
28123
+ for (const c of Object.values(pack.counters ?? {})) for (const t of c.triggers ?? []) seeActions(t.do);
28124
+ for (const m of Object.values(pack.moves ?? {})) seeActions(m.do);
28125
+ for (const m of Object.values(pack.modes)) {
28126
+ if (m.units?.roll) found.add(diceOf(m.units.roll));
28127
+ for (const p of m.perUnit ?? []) seeActions(p.extra);
28128
+ }
28129
+ for (const p of pack.phases) for (const s of p.steps) if (s.kind === "actions") seeActions(s.do);
28130
+ seeTriggers(pack.triggers);
28131
+ if (pack.targeting?.strategy === "anchoredOffset" && pack.targeting.eventFallback) found.add(diceOf(pack.targeting.eventFallback.roll));
28132
+ return [...found].sort((a, b) => Number(a.slice(1)) - Number(b.slice(1)));
28133
+ }
28134
+ function diceInWords(dice) {
28135
+ const names = dice.map((d) => d === "d100" ? "percentile dice (d100)" : `a ${d}`);
28136
+ if (names.length === 0) return "";
28137
+ if (names.length === 1) return names[0];
28138
+ return `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}`;
28139
+ }
28140
+
28141
+ // packages/rules-schema/src/docs.ts
28142
+ var DOC_KINDS = [
28143
+ { kind: "summary", label: "Summary", what: "What a catalog shows: the shape of the game without its rules.", full: false },
28144
+ { kind: "rulebook", label: "Rulebook", what: "Everything, in reading order: setup, flow, every table, every rule.", full: true },
28145
+ { kind: "quickstart", label: "Quick start", what: "Enough to play the first time; the rest is on the reference card.", full: true },
28146
+ { kind: "reference", label: "Reference card", what: "Every table and the things people forget, compact enough to keep beside you.", full: true },
28147
+ { kind: "runlog", label: "Run log sheet", what: "A printable sheet to write a run on by hand.", full: true }
28148
+ ];
28149
+ var Builder = class {
28150
+ blocks = [];
28151
+ h(level, text) {
28152
+ this.blocks.push({ kind: "heading", level, text });
28153
+ return this;
28154
+ }
28155
+ p(text, tone) {
28156
+ if (text && text.trim()) this.blocks.push(tone ? { kind: "paragraph", text: text.trim(), tone } : { kind: "paragraph", text: text.trim() });
28157
+ return this;
28158
+ }
28159
+ list(items, ordered = false) {
28160
+ if (items.length > 0) this.blocks.push(ordered ? { kind: "list", items, ordered } : { kind: "list", items });
28161
+ return this;
28162
+ }
28163
+ table(columns, rows, compact = false) {
28164
+ if (rows.length > 0) this.blocks.push(compact ? { kind: "table", columns, rows, compact } : { kind: "table", columns, rows });
28165
+ return this;
28166
+ }
28167
+ terms(items) {
28168
+ if (items.length > 0) this.blocks.push({ kind: "terms", items });
28169
+ return this;
28170
+ }
28171
+ form(fields) {
28172
+ this.blocks.push({ kind: "form", fields });
28173
+ return this;
28174
+ }
28175
+ rule() {
28176
+ this.blocks.push({ kind: "rule" });
28177
+ return this;
28178
+ }
28179
+ page() {
28180
+ this.blocks.push({ kind: "pagebreak" });
28181
+ return this;
28182
+ }
28183
+ };
28184
+ var plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
28185
+ var cap = (s) => s ? s[0].toUpperCase() + s.slice(1) : s;
28186
+ function stepInWords(pack, step) {
28187
+ const n = nouns(pack);
28188
+ switch (step.kind) {
28189
+ case "rollTable":
28190
+ return `${step.label ?? `Roll on ${pack.tables[step.table]?.title ?? step.table}`}${step.optional ? " (optional)" : ""}`;
28191
+ case "declareSubject":
28192
+ return step.label ?? `Declare the ${n.subject}${step.constrainedBy ? `, within what ${pack.tables[step.constrainedBy]?.title ?? step.constrainedBy} allowed` : ""}`;
28193
+ case "manual":
28194
+ return step.label;
28195
+ case "actions":
28196
+ return sentence(cap(actionsInWords(pack, step.do)));
28197
+ case "finalizeUnit":
28198
+ return step.label ?? `${n.finalize} the ${n.unit}`;
28199
+ }
28200
+ }
28201
+ var checklistText = (item) => typeof item === "string" ? item : item.text;
28202
+ function stepDetail(pack, step) {
28203
+ const out = [];
28204
+ if (step.kind === "manual") {
28205
+ if (step.description) out.push(step.description);
28206
+ for (const c of step.checklist ?? []) out.push(`\u2610 ${checklistText(c)}`);
28207
+ }
28208
+ if (step.kind === "finalizeUnit") for (const c of step.confirm ?? []) out.push(`\u2610 ${checklistText(c)}`);
28209
+ if ("skipWhen" in step && step.skipWhen?.length) out.push(`Skipped when ${conditionsInWords(pack, step.skipWhen, "any")}.`);
28210
+ return out;
28211
+ }
28212
+ function whatYouNeed(pack) {
28213
+ const n = nouns(pack);
28214
+ const items = [];
28215
+ const dice = diceInWords(diceNeeded(pack));
28216
+ if (dice) items.push(cap(dice) + ".");
28217
+ for (const deck of Object.values(pack.decks ?? {})) {
28218
+ if (deck.kind === "standard52") items.push(`A standard deck of playing cards${deck.includeJokers ? " with the jokers in" : ""}.`);
28219
+ else items.push(`${plural(deck.cards.length, "card", "cards")} for ${deck.title} \u2014 write them out, or use the app.`);
28220
+ }
28221
+ if (pack.capabilities.includes("timers")) items.push("A timer.");
28222
+ items.push(pack.journal?.enabled === false ? `Somewhere to keep the ${n.run} log.` : `Somewhere to keep the ${n.run} log \u2014 the run log sheet, or the app.`);
28223
+ return items;
28224
+ }
28225
+ function flowSteps(pack, opts) {
28226
+ const items = [];
28227
+ for (const phase of pack.phases) {
28228
+ const head = phase.steps.length === 1 ? `${phase.label}: ${stepInWords(pack, phase.steps[0])}` : phase.label;
28229
+ const notes = [];
28230
+ if (opts.detail && phase.description) notes.push(phase.description);
28231
+ if (opts.detail && phase.skipWhen?.length) notes.push(`Skipped when ${conditionsInWords(pack, phase.skipWhen, "any")}.`);
28232
+ if (phase.steps.length > 1) for (const s of phase.steps) notes.push(`${stepInWords(pack, s)}${opts.detail ? stepDetail(pack, s).map((d) => ` \u2014 ${d}`).join("") : ""}`);
28233
+ else if (opts.detail) notes.push(...stepDetail(pack, phase.steps[0]));
28234
+ items.push([head, ...notes.map((t) => ` ${t}`)].join("\n"));
28235
+ }
28236
+ return items;
28237
+ }
28238
+ function modeRows(pack, opts) {
28239
+ return Object.entries(pack.modes).map(([id, m]) => [
28240
+ `${m.label}${id === pack.defaultMode ? " (default)" : ""}`,
28241
+ modeLength(pack, m),
28242
+ modePlayers(m) + (m.seeded ? ", seeded" : ""),
28243
+ [m.description ?? "", ...opts.notes ? modeExtras(pack, m) : []].filter(Boolean).join(" ")
28244
+ ]);
28245
+ }
28246
+ function modeExtras(pack, m) {
28247
+ const n = nouns(pack);
28248
+ const out = [];
28249
+ for (const note of m.notes ?? []) out.push(note);
28250
+ const off = [
28251
+ ...(m.disable?.tables ?? []).map((t) => pack.tables[t]?.title ?? t),
28252
+ ...(m.disable?.decks ?? []).map((d) => pack.decks?.[d]?.title ?? d),
28253
+ ...(m.disable?.counters ?? []).map((c) => pack.counters?.[c]?.label ?? c),
28254
+ ...(m.disable?.phases ?? []).map((p) => pack.phases.find((x) => x.id === p)?.label ?? p)
28255
+ ];
28256
+ if (off.length) out.push(`Without ${off.join(", ")}.`);
28257
+ for (const p of m.perUnit ?? []) {
28258
+ const which = p.unit === "all" ? `Every ${n.unit}` : `${cap(n.unit)} ${p.unit}`;
28259
+ const skip = (p.skipPhases ?? []).map((id) => pack.phases.find((x) => x.id === id)?.label ?? id);
28260
+ const parts = [skip.length ? `skips ${skip.join(", ")}` : "", p.extra?.length ? actionsInWords(pack, p.extra) : ""].filter(Boolean);
28261
+ if (parts.length) out.push(sentence(`${which}: ${parts.join("; ")}`));
28262
+ }
28263
+ for (const r of m.players?.roles ?? []) out.push(`${r.label}: ${r.description ?? ""}`.trim());
28264
+ return out;
28265
+ }
28266
+ function tableRows(pack, table, opts) {
28267
+ const keys = entryKeys(table);
28268
+ return table.entries.map((e, i) => {
28269
+ const extras = [];
28270
+ if (opts.full) {
28271
+ for (const g of e.grants ?? []) extras.push(`\u2192 ${pack.states?.[g]?.label ?? g}`);
28272
+ for (const t of e.triggers ?? []) extras.push(sentence(triggerInWords(pack, t)));
28273
+ if (e.requires?.length) extras.push(`Only if ${conditionsInWords(pack, e.requires)}; otherwise roll again.`);
28274
+ }
28275
+ const text = [e.title ? `${e.title}. ` : "", sentence(e.text), ...extras.map((x) => ` ${x}`)].join("");
28276
+ return [keys[i] ?? "", text];
28277
+ });
28278
+ }
28279
+ function license(pack) {
28280
+ const l = pack.license;
28281
+ const who = l.holder ? ` \xA9 ${l.holder}` : "";
28282
+ return `${l.id}${who}${l.redistributable ? "" : " \u2014 private; not for redistribution"}${l.notice ? `. ${l.notice}` : ""}${l.url ? ` (${l.url})` : ""}`;
28283
+ }
28284
+ function byline(pack) {
28285
+ return [pack.author ? `by ${pack.author}` : "", `version ${pack.version}`].filter(Boolean).join(" \xB7 ");
28286
+ }
28287
+ function summaryDoc(pack) {
28288
+ const n = nouns(pack);
28289
+ const b = new Builder();
28290
+ b.p(pack.description);
28291
+ b.p(license(pack), "muted");
28292
+ b.h(2, "How it plays");
28293
+ const unitRange = `${pack.unit.min === pack.unit.max ? pack.unit.min : `${pack.unit.min}\u2013${pack.unit.max}`}`;
28294
+ b.p(
28295
+ `A ${n.run} is a series of ${n.units}${pack.unit.createsSubject ? `, each producing a ${n.subject}` : ""}; ${unitRange} of them, depending on the mode. Each ${n.unit} moves through ${plural(pack.phases.length, "phase", "phases")}: ${pack.phases.map((p) => p.label).join(", ")}.`
28296
+ );
28297
+ b.table(["Mode", "Length", "Players", "About"], modeRows(pack, { notes: false }));
28298
+ b.h(2, "What is inside");
28299
+ const tables = Object.values(pack.tables);
28300
+ const decks = Object.values(pack.decks ?? {});
28301
+ const parts = [];
28302
+ if (tables.length) parts.push(`${plural(tables.length, "table", "tables")}: ${tables.map((t) => `${t.title} (${howConsulted(t).replace(/\.$/, "").toLowerCase()}, ${plural(t.entries.length, "entry", "entries")})`).join("; ")}.`);
28303
+ if (decks.length) parts.push(`${plural(decks.length, "deck", "decks")}: ${decks.map((d) => d.kind === "cards" ? `${d.title} (${plural(d.cards.length, "card", "cards")})` : `${d.title} (a standard deck)`).join("; ")}.`);
28304
+ const states = Object.values(pack.states ?? {});
28305
+ if (states.length) parts.push(`${plural(states.length, "state", "states")} a ${n.subject} or ${n.run} can carry: ${states.map((s) => s.label).join(", ")}.`);
28306
+ const counters = Object.values(pack.counters ?? {}).filter((c) => !c.hidden);
28307
+ if (counters.length) parts.push(`${plural(counters.length, "tally", "tallies")}: ${counters.map((c) => c.label).join(", ")}.`);
28308
+ const resources = Object.values(pack.resources ?? {});
28309
+ if (resources.length) parts.push(`${plural(resources.length, "track", "tracks")}: ${resources.map((r) => r.label).join(", ")}.`);
28310
+ const moves = Object.values(pack.moves ?? {});
28311
+ if (moves.length) parts.push(`${plural(moves.length, "move", "moves")} you may make: ${moves.map((m) => m.label).join(", ")}.`);
28312
+ if (pack.endings?.length) parts.push(`${plural(pack.endings.length, "ending", "endings")}: ${pack.endings.map((e) => e.label).join(", ")}.`);
28313
+ if (pack.targeting && pack.targeting.strategy !== "none") parts.push(`Consequences can reach back to earlier ${n.subjects}.`);
28314
+ if (pack.journal?.enabled !== false) parts.push(`A journal line per ${n.unit}${pack.journal?.required ? ", required" : ""}.`);
28315
+ b.list(parts);
28316
+ b.h(2, "What you need");
28317
+ b.list(whatYouNeed(pack));
28318
+ if (pack.homepage) b.p(`More at ${pack.homepage}`, "muted");
28319
+ return { kind: "summary", layout: "book", title: pack.title, subtitle: byline(pack), blocks: b.blocks };
28320
+ }
28321
+ function rulebookDoc(pack) {
28322
+ const n = nouns(pack);
28323
+ const b = new Builder();
28324
+ b.p(pack.description);
28325
+ b.p(license(pack), "muted");
28326
+ b.h(2, "What you need");
28327
+ b.list(whatYouNeed(pack));
28328
+ b.h(2, "Terms");
28329
+ b.terms([
28330
+ { term: cap(n.run), text: `One session of play. A ${n.run} is a series of ${n.units}, and ends when you say so or when the game does.` },
28331
+ { term: cap(n.unit), text: `One round. Every ${n.unit} passes through the same phases${pack.unit.createsSubject ? ` and produces one ${n.subject}` : ""}.` },
28332
+ ...pack.unit.createsSubject ? [{ term: cap(n.subject), text: `The thing a ${n.unit} makes. States attach to it, and consequences can land on it later.` }] : [],
28333
+ { term: cap(n.finalize), text: `Closing a ${n.unit}. Once ${n.finalize.toLowerCase()}d, what it made is fixed unless a rule says otherwise.` },
28334
+ ...Object.entries(pack.vocabulary.terms ?? {}).map(([term, text]) => ({ term, text }))
28335
+ ]);
28336
+ if (pack.hierarchy?.length) b.p(`When rules contradict each other, the more specific wins, in this order: ${pack.hierarchy.join(" > ")}.`, "note");
28337
+ b.h(2, `A ${n.unit}, step by step`);
28338
+ b.list(flowSteps(pack, { detail: true }), true);
28339
+ if (pack.journal?.enabled !== false) b.p(`${pack.journal?.prompt ? `After each ${n.unit}: \u201C${pack.journal.prompt}\u201D` : `Keep a line of notes per ${n.unit}.`}${pack.journal?.required ? " A note is required before moving on." : ""}`);
28340
+ b.h(2, "Modes");
28341
+ b.table(["Mode", "Length", "Players", "About"], modeRows(pack, { notes: true }));
28342
+ b.h(2, "Tables");
28343
+ for (const [id, table] of Object.entries(pack.tables)) {
28344
+ b.h(3, table.title);
28345
+ b.p([howConsulted(table), table.description ?? ""].filter(Boolean).join(" "));
28346
+ b.table([table.resolution === "keyed" ? "Key" : "Roll", "Result"], tableRows(pack, table, { full: true }));
28347
+ void id;
28348
+ }
28349
+ const decks = Object.entries(pack.decks ?? {});
28350
+ if (decks.length) {
28351
+ b.h(2, "Cards");
28352
+ for (const [, deck] of decks) {
28353
+ b.h(3, deck.title);
28354
+ if (deck.kind === "standard52") {
28355
+ b.p([deck.description, `A standard deck${deck.includeJokers ? " with jokers" : ""}; ${deck.drawAtStart ? `draw ${deck.drawAtStart} at the start. ` : ""}Each draw is read by ${deck.resolveBy}${deck.resolveOn ? ` on ${pack.tables[deck.resolveOn]?.title ?? deck.resolveOn}` : ""}.`].filter(Boolean).join(" "));
28356
+ } else {
28357
+ b.p([deck.description, deck.drawAtStart ? `Draw ${deck.drawAtStart} at the start.` : "", deck.unique ? "" : "Cards may repeat.", deck.carriesOver ? `Unused cards carry over to the next ${n.run}.` : ""].filter(Boolean).join(" "));
28358
+ b.table(
28359
+ ["Card", "Effect"],
28360
+ deck.cards.map((c) => [c.title, [c.text, ...(c.triggers ?? []).map((t) => triggerInWords(pack, t)), c.requires?.length ? `Only if ${conditionsInWords(pack, c.requires)}.` : ""].filter(Boolean).join(" ")])
28361
+ );
28362
+ }
28363
+ }
28364
+ }
28365
+ const states = Object.entries(pack.states ?? {});
28366
+ if (states.length) {
28367
+ b.h(2, "States");
28368
+ b.table(
28369
+ ["State", "On", "Means"],
28370
+ states.map(([, s]) => [
28371
+ s.short ? `${s.label} (${s.short})` : s.label,
28372
+ s.scope === "run" ? `the ${n.run}` : `a ${n.subject}`,
28373
+ [s.description ?? "", ...(s.semantics ?? []).map(semanticInWords)].filter(Boolean).join(" ")
28374
+ ])
28375
+ );
28376
+ }
28377
+ const counters = Object.entries(pack.counters ?? {}).filter(([, c]) => !c.hidden);
28378
+ const resources = Object.entries(pack.resources ?? {});
28379
+ if (counters.length || resources.length) {
28380
+ b.h(2, "Tallies and tracks");
28381
+ b.table(
28382
+ ["Name", "Starts at", "Rule"],
28383
+ [
28384
+ ...counters.map(([, c]) => [
28385
+ c.label,
28386
+ String(c.initial ?? 0),
28387
+ [
28388
+ c.min !== void 0 && c.max !== void 0 ? `Stays within ${c.min}\u2013${c.max}.` : c.max !== void 0 ? `Never above ${c.max}.` : c.min !== void 0 && c.min !== 0 ? `Never below ${c.min}.` : "",
28389
+ ...(c.triggers ?? []).map((t) => sentence(`${t.label ? `${t.label} \u2014 ` : ""}at ${boundText(t.when)}${t.oncePerRun ? `, once per ${n.run}` : ""}: ${actionsInWords(pack, t.do)}`))
28390
+ ].filter(Boolean).join(" ")
28391
+ ]),
28392
+ ...resources.map(([, r]) => [r.label, String(r.initial ?? 0), [r.description ?? "", `From ${r.min ?? 0}${r.max !== void 0 ? ` to ${r.max}` : ""}, in steps of ${r.step ?? 1}.`].filter(Boolean).join(" ")])
28393
+ ]
28394
+ );
28395
+ }
28396
+ const moves = Object.entries(pack.moves ?? {});
28397
+ if (moves.length) {
28398
+ b.h(2, "Moves");
28399
+ b.table(
28400
+ ["Move", "When", "What happens"],
28401
+ moves.map(([, m]) => [
28402
+ m.label,
28403
+ [whenInWords(m.when, n), m.oncePerRun ? `once per ${n.run}` : "", m.available?.length ? `if ${conditionsInWords(pack, m.available)}` : ""].filter(Boolean).join("; "),
28404
+ [m.description ?? "", sentence(cap(actionsInWords(pack, m.do)))].filter(Boolean).join(" ")
28405
+ ])
28406
+ );
28407
+ }
28408
+ if (pack.targeting && pack.targeting.strategy !== "none") {
28409
+ b.h(2, "Reaching back");
28410
+ b.p(targetingInWords(pack));
28411
+ }
28412
+ if (pack.triggers?.length) {
28413
+ b.h(2, "Always in effect");
28414
+ b.list(pack.triggers.map((t) => sentence(cap(triggerInWords(pack, t)))));
28415
+ }
28416
+ if (pack.endings?.length) {
28417
+ b.h(2, `Ending a ${n.run}`);
28418
+ b.terms(pack.endings.map((e) => ({ term: e.label, text: [e.text ?? "", e.requires?.length ? `Available when ${conditionsInWords(pack, e.requires)}.` : ""].filter(Boolean).join(" ") })));
28419
+ }
28420
+ return { kind: "rulebook", layout: "book", title: pack.title, subtitle: byline(pack), blocks: b.blocks };
28421
+ }
28422
+ function quickstartDoc(pack) {
28423
+ const n = nouns(pack);
28424
+ const b = new Builder();
28425
+ b.p(pack.description);
28426
+ b.h(2, "What you need");
28427
+ b.list(whatYouNeed(pack));
28428
+ b.h(2, "The words");
28429
+ b.p(`A ${n.run} is one session. It is made of ${n.units}${pack.unit.createsSubject ? `, and each ${n.unit} makes one ${n.subject}` : ""}. Closing a ${n.unit} is called \u201C${n.finalize}\u201D.`);
28430
+ b.h(2, `Each ${n.unit}`);
28431
+ b.list(flowSteps(pack, { detail: false }), true);
28432
+ const d = pack.modes[pack.defaultMode];
28433
+ if (d) {
28434
+ b.h(2, `Start with ${d.label}`);
28435
+ b.p([d.description ?? "", `${cap(modeLength(pack, d))}, ${modePlayers(d)}.`, ...modeExtras(pack, d)].filter(Boolean).join(" "));
28436
+ const others = Object.entries(pack.modes).filter(([id]) => id !== pack.defaultMode);
28437
+ if (others.length) b.p(`Other ways to play: ${others.map(([, m]) => m.label).join(", ")}. They are in the rulebook.`, "muted");
28438
+ }
28439
+ if (pack.endings?.length) {
28440
+ b.h(2, "When you stop");
28441
+ b.terms(pack.endings.map((e) => ({ term: e.label, text: e.text ?? "" })));
28442
+ }
28443
+ b.p("Every table is on the reference card. Everything else is in the rulebook.", "muted");
28444
+ return { kind: "quickstart", layout: "book", title: pack.title, subtitle: "Quick start", blocks: b.blocks };
28445
+ }
28446
+ function referenceDoc(pack) {
28447
+ const n = nouns(pack);
28448
+ const b = new Builder();
28449
+ b.h(2, `Each ${n.unit}`);
28450
+ b.list(flowSteps(pack, { detail: false }), true);
28451
+ for (const [, table] of Object.entries(pack.tables)) {
28452
+ b.h(2, `${table.title} \u2014 ${howConsulted(table).replace(/\.$/, "")}`);
28453
+ b.table([table.resolution === "keyed" ? "Key" : "Roll", ""], tableRows(pack, table, { full: true }), true);
28454
+ }
28455
+ const states = Object.values(pack.states ?? {});
28456
+ if (states.length) {
28457
+ b.h(2, "States");
28458
+ b.table(["", "State"], states.map((s) => [s.short ?? "", `${s.label}${s.description ? ` \u2014 ${s.description}` : ""}`]), true);
28459
+ }
28460
+ const moves = Object.values(pack.moves ?? {});
28461
+ if (moves.length) {
28462
+ b.h(2, "Moves");
28463
+ b.list(moves.map((m) => sentence(`${m.label} (${whenInWords(m.when, n)}${m.oncePerRun ? `, once per ${n.run}` : ""}): ${actionsInWords(pack, m.do)}`)));
28464
+ }
28465
+ const remember = [];
28466
+ for (const t of pack.triggers ?? []) remember.push(sentence(cap(triggerInWords(pack, t))));
28467
+ for (const c of Object.values(pack.counters ?? {})) for (const t of c.triggers ?? []) remember.push(sentence(`${c.label} at ${boundText(t.when)}: ${actionsInWords(pack, t.do)}`));
28468
+ for (const m of Object.values(pack.modes)) for (const note of m.notes ?? []) remember.push(`${m.label}: ${note}`);
28469
+ if (pack.hierarchy?.length) remember.push(`Precedence: ${pack.hierarchy.join(" > ")}.`);
28470
+ if (remember.length) {
28471
+ b.h(2, "Frequently forgotten");
28472
+ b.list(remember);
28473
+ }
28474
+ if (pack.endings?.length) {
28475
+ b.h(2, "Endings");
28476
+ b.list(pack.endings.map((e) => `${e.label}${e.text ? `: ${e.text}` : ""}`));
28477
+ }
28478
+ return { kind: "reference", layout: "card", title: pack.title, subtitle: `Reference \xB7 v${pack.version}`, blocks: b.blocks };
28479
+ }
28480
+ function runlogDoc(pack) {
28481
+ const n = nouns(pack);
28482
+ const b = new Builder();
28483
+ const head = [{ label: "Date", width: "short" }, { label: "Mode", width: "short" }];
28484
+ if (Object.values(pack.modes).some((m) => m.seeded)) head.push({ label: "Seed", width: "short" });
28485
+ if (Object.values(pack.modes).some((m) => (m.players?.max ?? 1) > 1)) head.push({ label: "Players", width: "long" });
28486
+ for (const deck of Object.values(pack.decks ?? {})) if (deck.kind === "cards" && deck.drawAtStart) head.push({ label: deck.title, width: "short" });
28487
+ for (const c of Object.values(pack.counters ?? {}).filter((c2) => !c2.hidden)) head.push({ label: c.label, width: "short", box: true });
28488
+ for (const r of Object.values(pack.resources ?? {})) head.push({ label: r.label, width: "short", box: true });
28489
+ head.push({ label: "Page", width: "short" });
28490
+ b.form(head);
28491
+ b.rule();
28492
+ const unitFields = [];
28493
+ unitFields.push({ label: `${cap(n.unit)} \u2116`, width: "short" });
28494
+ if (pack.unit.createsSubject) unitFields.push({ label: `${cap(n.subject)} / states`, width: "long" });
28495
+ for (const phase of pack.phases) {
28496
+ for (const step of phase.steps) {
28497
+ if (step.kind === "rollTable") unitFields.push({ label: pack.tables[step.table]?.title ?? step.table, width: "full", lines: 2 });
28498
+ }
28499
+ }
28500
+ if (pack.journal?.enabled !== false) unitFields.push({ label: "Notes", width: "full", lines: 2 });
28501
+ for (let i = 0; i < 6; i++) b.form(unitFields);
28502
+ b.rule();
28503
+ b.form([{ label: `${cap(n.run)} states`, width: "full", lines: 2 }, { label: "Ending", width: "long" }]);
28504
+ return { kind: "runlog", layout: "sheet", title: pack.title, subtitle: `${cap(n.run)} log`, blocks: b.blocks };
28505
+ }
28506
+ function semanticInWords(s) {
28507
+ switch (s) {
28508
+ case "blocksEdit":
28509
+ return "Nothing about it may be changed.";
28510
+ case "makesUntargetable":
28511
+ return "Consequences cannot land on it.";
28512
+ case "excludesFromResult":
28513
+ return "It does not count toward the result.";
28514
+ case "locksValue":
28515
+ return "Its value is fixed.";
28516
+ case "removesFromPlay":
28517
+ return "It is out of play.";
28518
+ default:
28519
+ return s;
28520
+ }
28521
+ }
28522
+ function whenInWords(when, n) {
28523
+ switch (when) {
28524
+ case "betweenUnits":
28525
+ return `between ${n.units}`;
28526
+ case "beforeEnding":
28527
+ return `before ending the ${n.run}`;
28528
+ default:
28529
+ return "any time";
28530
+ }
28531
+ }
28532
+ function boundText(b) {
28533
+ if (b.eq !== void 0) return String(b.eq);
28534
+ if (b.gte !== void 0 && b.lte !== void 0) return `${b.gte}\u2013${b.lte}`;
28535
+ if (b.gte !== void 0) return `${b.gte}+`;
28536
+ if (b.lte !== void 0) return `\u2264${b.lte}`;
28537
+ return "";
28538
+ }
28539
+ function targetingInWords(pack) {
28540
+ const n = nouns(pack);
28541
+ const t = pack.targeting;
28542
+ if (!t) return "";
28543
+ switch (t.strategy) {
28544
+ case "none":
28545
+ return "";
28546
+ case "playerChoice":
28547
+ return `When a consequence reaches back, you choose which earlier ${n.subject} it lands on.`;
28548
+ case "random":
28549
+ return `When a consequence reaches back, pick an earlier ${n.subject} at random.`;
28550
+ case "oldest":
28551
+ return `When a consequence reaches back, it lands on the oldest ${n.subject} still in play.`;
28552
+ case "newest":
28553
+ return `When a consequence reaches back, it lands on the newest ${n.subject} still in play.`;
28554
+ case "anchoredOffset": {
28555
+ const bands = t.bands.map((b) => {
28556
+ const anchor2 = b.anchor === "playerChoice" ? "you choose" : `count ${b.direction ?? "before"} from the ${b.anchor}`;
28557
+ return `${b.range[0]}\u2013${b.range[1]}: ${anchor2}`;
28558
+ });
28559
+ return `When a roll reaches back, its ones digit says how far: ${bands.join("; ")}. Counting ${t.wraparound === false ? "stops at the end" : "wraps around"}${t.skipIneligible === false ? "" : ` and skips any ${n.subject} that cannot be targeted`}. A 0 lands on the anchor itself.${t.eventFallback ? ` When something other than a roll reaches back, roll ${t.eventFallback.roll} and read it the same way${t.eventFallback.missOn ? `; a ${t.eventFallback.missOn} misses` : ""}.` : ""}`;
28560
+ }
28561
+ }
28562
+ }
28563
+ function generateDoc(pack, kind) {
28564
+ switch (kind) {
28565
+ case "summary":
28566
+ return summaryDoc(pack);
28567
+ case "rulebook":
28568
+ return rulebookDoc(pack);
28569
+ case "quickstart":
28570
+ return quickstartDoc(pack);
28571
+ case "reference":
28572
+ return referenceDoc(pack);
28573
+ case "runlog":
28574
+ return runlogDoc(pack);
28575
+ }
28576
+ }
28577
+ var mdEscape = (s) => s.replace(/\|/g, "\\|").replace(/\n/g, " ");
28578
+ function toMarkdown(doc) {
28579
+ const out = [`# ${doc.title}`];
28580
+ if (doc.subtitle) out.push(`*${doc.subtitle}*`);
28581
+ for (const block of doc.blocks) {
28582
+ switch (block.kind) {
28583
+ case "heading":
28584
+ out.push(`${"#".repeat(block.level)} ${block.text}`);
28585
+ break;
28586
+ case "paragraph":
28587
+ out.push(block.tone === "note" ? `> ${block.text}` : block.text);
28588
+ break;
28589
+ case "list":
28590
+ out.push(block.items.map((item, i) => `${block.ordered ? `${i + 1}.` : "-"} ${item.replace(/\n\s*/g, " \n ")}`).join("\n"));
28591
+ break;
28592
+ case "table":
28593
+ out.push([`| ${block.columns.map(mdEscape).join(" | ")} |`, `| ${block.columns.map(() => "---").join(" | ")} |`, ...block.rows.map((r) => `| ${r.map(mdEscape).join(" | ")} |`)].join("\n"));
28594
+ break;
28595
+ case "terms":
28596
+ out.push(block.items.map((t) => `**${t.term}** \u2014 ${t.text}`).join("\n\n"));
28597
+ break;
28598
+ case "form":
28599
+ out.push(block.fields.map((f) => `${f.label}: ${f.box ? "[ ]" : "_".repeat(f.width === "full" ? 40 : f.width === "long" ? 24 : 12)}`).join(" \n"));
28600
+ break;
28601
+ case "rule":
28602
+ out.push("---");
28603
+ break;
28604
+ case "pagebreak":
28605
+ out.push("");
28606
+ break;
28607
+ }
28608
+ }
28609
+ return `${out.join("\n\n")}
28610
+ `;
28611
+ }
28612
+ var esc2 = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
28613
+ var multiline = (s) => esc2(s).replace(/\n/g, "<br>");
28614
+ var CSS = `
28615
+ :root { color-scheme: light; }
28616
+ body { margin: 0; background: #fff; color: #161513; font: 11pt/1.45 Georgia, "Times New Roman", serif; }
28617
+ main { max-width: 42em; margin: 0 auto; padding: 2.5rem 1.5rem; }
28618
+ h1 { font-size: 2rem; margin: 0 0 .25rem; line-height: 1.1; }
28619
+ .subtitle { margin: 0 0 2rem; color: #5b5751; font-style: italic; }
28620
+ h2 { font-size: 1.25rem; margin: 2rem 0 .5rem; border-bottom: 1px solid #d9d4cb; padding-bottom: .2rem; }
28621
+ h3 { font-size: 1.05rem; margin: 1.4rem 0 .35rem; }
28622
+ p { margin: 0 0 .75rem; }
28623
+ p.muted { color: #5b5751; font-size: .92em; }
28624
+ p.note { border-left: 3px solid #b9b2a5; padding-left: .75rem; color: #3f3b36; }
28625
+ ol, ul { margin: 0 0 .9rem; padding-left: 1.4rem; }
28626
+ li { margin: .2rem 0; white-space: pre-line; }
28627
+ table { border-collapse: collapse; width: 100%; margin: 0 0 1rem; font-size: .95em; }
28628
+ th, td { text-align: left; vertical-align: top; padding: .3rem .5rem; border-bottom: 1px solid #e4dfd6; }
28629
+ th { font-family: system-ui, sans-serif; font-size: .75em; letter-spacing: .04em; text-transform: uppercase; color: #5b5751; }
28630
+ td:first-child { white-space: nowrap; font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; font-size: .9em; }
28631
+ table.compact td { padding: .12rem .35rem; font-size: .85em; }
28632
+ dl { margin: 0 0 1rem; }
28633
+ dt { font-weight: 700; margin-top: .5rem; }
28634
+ dd { margin: 0; }
28635
+ hr { border: 0; border-top: 1px solid #d9d4cb; margin: 1.5rem 0; }
28636
+ .page { break-after: page; }
28637
+ .form { display: flex; flex-wrap: wrap; gap: .5rem 1.25rem; margin: .6rem 0 .9rem; font-family: system-ui, sans-serif; font-size: .85em; }
28638
+ .field { display: flex; align-items: flex-end; gap: .4rem; }
28639
+ .field .line { display: inline-block; border-bottom: 1px solid #161513; height: 1.3em; }
28640
+ .field.short .line { width: 7em; } .field.long .line { width: 16em; } .field.full { flex: 1 1 100%; }
28641
+ .field.full .line { flex: 1; }
28642
+ .field .lines { flex: 1; display: grid; gap: .55em; }
28643
+ .field .lines .line { width: 100%; }
28644
+ .field .box { display: inline-block; width: 2.6em; height: 1.6em; border: 1px solid #161513; }
28645
+ .layout-card main { max-width: none; column-width: 17em; column-gap: 2rem; padding: 1.25rem; font-size: 9pt; }
28646
+ .layout-card h1 { column-span: all; font-size: 1.4rem; }
28647
+ .layout-card .subtitle { column-span: all; margin-bottom: 1rem; }
28648
+ .layout-card h2 { margin-top: 1rem; font-size: .95rem; break-after: avoid; }
28649
+ .layout-card table { break-inside: auto; }
28650
+ .layout-card tr { break-inside: avoid; }
28651
+ .layout-sheet main { max-width: none; padding: 1.25rem; }
28652
+ .layout-sheet .form + .form { border-top: 1px dashed #d9d4cb; padding-top: .6rem; }
28653
+ @media print { main { padding: 0; } @page { margin: 1.6cm; } .layout-card @page { margin: 1cm; } }
28654
+ `;
28655
+ function toHtml(doc) {
28656
+ const body = [];
28657
+ for (const block of doc.blocks) {
28658
+ switch (block.kind) {
28659
+ case "heading":
28660
+ body.push(`<h${block.level}>${esc2(block.text)}</h${block.level}>`);
28661
+ break;
28662
+ case "paragraph":
28663
+ body.push(`<p${block.tone ? ` class="${block.tone}"` : ""}>${multiline(block.text)}</p>`);
28664
+ break;
28665
+ case "list":
28666
+ body.push(`<${block.ordered ? "ol" : "ul"}>${block.items.map((i) => `<li>${esc2(i)}</li>`).join("")}</${block.ordered ? "ol" : "ul"}>`);
28667
+ break;
28668
+ case "table":
28669
+ body.push(
28670
+ `<table${block.compact ? ' class="compact"' : ""}><thead><tr>${block.columns.map((c) => `<th>${esc2(c)}</th>`).join("")}</tr></thead><tbody>${block.rows.map((r) => `<tr>${r.map((c) => `<td>${multiline(c)}</td>`).join("")}</tr>`).join("")}</tbody></table>`
28671
+ );
28672
+ break;
28673
+ case "terms":
28674
+ body.push(`<dl>${block.items.map((t) => `<dt>${esc2(t.term)}</dt><dd>${multiline(t.text)}</dd>`).join("")}</dl>`);
28675
+ break;
28676
+ case "form":
28677
+ body.push(
28678
+ `<div class="form">${block.fields.map((f) => {
28679
+ const width = f.width ?? "short";
28680
+ const line = f.box ? `<span class="box"></span>` : f.lines && f.lines > 1 ? `<span class="lines">${'<span class="line"></span>'.repeat(f.lines)}</span>` : `<span class="line"></span>`;
28681
+ return `<span class="field ${width}"><span class="label">${esc2(f.label)}</span>${line}</span>`;
28682
+ }).join("")}</div>`
28683
+ );
28684
+ break;
28685
+ case "rule":
28686
+ body.push("<hr>");
28687
+ break;
28688
+ case "pagebreak":
28689
+ body.push('<div class="page"></div>');
28690
+ break;
28691
+ }
28692
+ }
28693
+ return `<!doctype html>
28694
+ <html lang="en" class="layout-${doc.layout}">
28695
+ <head>
28696
+ <meta charset="utf-8">
28697
+ <meta name="viewport" content="width=device-width, initial-scale=1">
28698
+ <title>${esc2(doc.title)}${doc.subtitle ? ` \u2014 ${esc2(doc.subtitle)}` : ""}</title>
28699
+ <style>${CSS}</style>
28700
+ </head>
28701
+ <body>
28702
+ <main>
28703
+ <h1>${esc2(doc.title)}</h1>
28704
+ ${doc.subtitle ? `<p class="subtitle">${esc2(doc.subtitle)}</p>` : ""}
28705
+ ${body.join("\n")}
28706
+ </main>
28707
+ </body>
28708
+ </html>
28709
+ `;
28710
+ }
28711
+
27888
28712
  // packages/engine/src/log.ts
27889
28713
  function voidedIds(events) {
27890
28714
  const out = /* @__PURE__ */ new Set();
@@ -28622,11 +29446,11 @@ async function cmdSign(args) {
28622
29446
  const format = detectFormat(path);
28623
29447
  const document = import_yaml3.default.parse(text);
28624
29448
  const signature = await signPack(document, key.privateKey, flag2(args, "--as"));
28625
- const signed = { ...document, signature };
29449
+ const signed2 = { ...document, signature };
28626
29450
  writeFileSync2(
28627
29451
  path,
28628
- format === "json" ? `${JSON.stringify(signed, null, 2)}
28629
- ` : import_yaml3.default.stringify(signed, { lineWidth: 90 }),
29452
+ format === "json" ? `${JSON.stringify(signed2, null, 2)}
29453
+ ` : import_yaml3.default.stringify(signed2, { lineWidth: 90 }),
28630
29454
  "utf8"
28631
29455
  );
28632
29456
  console.log(`${paint(GREEN, "signed")} ${input2}`);
@@ -28767,6 +29591,62 @@ async function reportSignature(document) {
28767
29591
  );
28768
29592
  }
28769
29593
 
29594
+ // packages/cli/src/docs.ts
29595
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "node:fs";
29596
+ import { basename, join as join2, resolve as resolve3 } from "node:path";
29597
+ import { readFileSync as readFileSync3 } from "node:fs";
29598
+ function cmdDocs(args) {
29599
+ const files = args.filter((a) => !a.startsWith("-"));
29600
+ const input2 = files[0];
29601
+ if (!input2) {
29602
+ console.error("usage: runlog docs <pack.yaml> [-o dir] [--only summary,rulebook,quickstart,reference,runlog] [--md | --html]");
29603
+ return 2;
29604
+ }
29605
+ const outIndex = args.findIndex((a) => a === "-o" || a === "--out");
29606
+ const onlyIndex = args.findIndex((a) => a === "--only");
29607
+ const wanted = new Set(
29608
+ onlyIndex >= 0 && args[onlyIndex + 1] ? args[onlyIndex + 1].split(",").map((k) => k.trim()) : DOC_KINDS.map((k) => k.kind)
29609
+ );
29610
+ for (const k of wanted) {
29611
+ if (!DOC_KINDS.some((d) => d.kind === k)) {
29612
+ console.error(`no such document: ${k}. Choose from ${DOC_KINDS.map((d) => d.kind).join(", ")}.`);
29613
+ return 2;
29614
+ }
29615
+ }
29616
+ const formats = args.includes("--md") && !args.includes("--html") ? ["md"] : args.includes("--html") && !args.includes("--md") ? ["html"] : ["html", "md"];
29617
+ const path = resolve3(input2);
29618
+ let text;
29619
+ try {
29620
+ text = readFileSync3(path, "utf8");
29621
+ } catch {
29622
+ console.error(`cannot read ${input2}`);
29623
+ return 1;
29624
+ }
29625
+ const loaded = loadPackText(text, detectFormat(path));
29626
+ if (!loaded.ok) {
29627
+ console.error(`${basename(input2)} does not load; run \`runlog validate\` to see why`);
29628
+ return 1;
29629
+ }
29630
+ const { pack } = loaded;
29631
+ const dir = outIndex >= 0 && args[outIndex + 1] ? resolve3(args[outIndex + 1]) : resolve3(`${pack.id}-docs`);
29632
+ mkdirSync2(dir, { recursive: true });
29633
+ for (const kind of DOC_KINDS) {
29634
+ if (!wanted.has(kind.kind)) continue;
29635
+ const doc = generateDoc(pack, kind.kind);
29636
+ for (const format of formats) {
29637
+ const file2 = join2(dir, `${kind.kind}.${format}`);
29638
+ writeFileSync3(file2, format === "md" ? toMarkdown(doc) : toHtml(doc), "utf8");
29639
+ console.log(`${kind.label.padEnd(14)} ${file2}`);
29640
+ }
29641
+ }
29642
+ if (!pack.license.redistributable) {
29643
+ console.log("note: this pack is marked non-redistributable. The summary is safe to show anyone; keep the rest as private as the pack.");
29644
+ } else {
29645
+ console.log("open an .html in a browser and print it for a PDF; the reference card lays out in columns and the run log as a sheet.");
29646
+ }
29647
+ return 0;
29648
+ }
29649
+
28770
29650
  // packages/cli/src/bin.ts
28771
29651
  var RESET2 = "\x1B[0m";
28772
29652
  var RED2 = "\x1B[31m";
@@ -28794,17 +29674,17 @@ function printDiagnostics(diagnostics, file2) {
28794
29674
  console.error(` ${parts.join(", ")} in ${file2}`);
28795
29675
  }
28796
29676
  function read(path) {
28797
- const abs = resolve3(path);
29677
+ const abs = resolve4(path);
28798
29678
  if (!existsSync3(abs)) {
28799
29679
  console.error(paint2(RED2, `no such file: ${path}`));
28800
29680
  process.exit(2);
28801
29681
  }
28802
- const text = readFileSync3(abs, "utf8");
29682
+ const text = readFileSync4(abs, "utf8");
28803
29683
  const result = loadPackText(text, detectFormat(abs));
28804
29684
  return { text, pack: result.pack, diagnostics: result.diagnostics };
28805
29685
  }
28806
29686
  function report(path, diagnostics, strict) {
28807
- printDiagnostics(diagnostics, basename(path));
29687
+ printDiagnostics(diagnostics, basename2(path));
28808
29688
  if (hasErrors(diagnostics)) return 1;
28809
29689
  if (strict && diagnostics.length > 0) {
28810
29690
  console.error(paint2(YELLOW2, " --strict: treating warnings as failures"));
@@ -28830,7 +29710,7 @@ async function cmdValidate(args) {
28830
29710
  console.log(
28831
29711
  `${paint2(GREEN2, "ok")} ${paint2(BOLD, pack.title)} ${paint2(DIM2, `v${pack.version}`)} \u2014 ${tables} table${tables === 1 ? "" : "s"}, ${entries} entries, ${Object.keys(pack.modes).length} mode(s)` + (diagnostics.length ? paint2(YELLOW2, `, ${diagnostics.length} warning(s)`) : "")
28832
29712
  );
28833
- await reportSignature(import_yaml4.default.parse(readFileSync3(resolve3(file2), "utf8")));
29713
+ await reportSignature(import_yaml4.default.parse(readFileSync4(resolve4(file2), "utf8")));
28834
29714
  }
28835
29715
  }
28836
29716
  return worst;
@@ -28850,14 +29730,14 @@ function cmdBundle(args) {
28850
29730
  }
28851
29731
  printDiagnostics(
28852
29732
  diagnostics.filter((d) => d.level === "warning"),
28853
- basename(input2)
29733
+ basename2(input2)
28854
29734
  );
28855
29735
  const body = JSON.stringify(pack, null, 2);
28856
29736
  const hash2 = createHash("sha256").update(body).digest("hex").slice(0, 16);
28857
29737
  const bundled = { ...pack, bundledAt: (/* @__PURE__ */ new Date()).toISOString(), contentHash: hash2 };
28858
- const out = outIndex >= 0 && args[outIndex + 1] ? resolve3(args[outIndex + 1]) : resolve3(`${pack.id}-${pack.version}.pack.json`);
28859
- mkdirSync2(dirname(out), { recursive: true });
28860
- writeFileSync3(out, `${JSON.stringify(bundled, null, 2)}
29738
+ const out = outIndex >= 0 && args[outIndex + 1] ? resolve4(args[outIndex + 1]) : resolve4(`${pack.id}-${pack.version}.pack.json`);
29739
+ mkdirSync3(dirname(out), { recursive: true });
29740
+ writeFileSync4(out, `${JSON.stringify(bundled, null, 2)}
28861
29741
  `, "utf8");
28862
29742
  console.log(`${paint2(GREEN2, "bundled")} ${out} ${paint2(DIM2, `sha256:${hash2}`)}`);
28863
29743
  if (!pack.license.redistributable) {
@@ -28910,12 +29790,12 @@ function cmdTest(args) {
28910
29790
  }
28911
29791
  function cmdInit(args) {
28912
29792
  const name = args.filter((a) => !a.startsWith("-"))[0] ?? "my-pack";
28913
- const out = resolve3(`${name}.yaml`);
29793
+ const out = resolve4(`${name}.yaml`);
28914
29794
  if (existsSync3(out)) {
28915
29795
  console.error(paint2(RED2, `refusing to overwrite ${out}`));
28916
29796
  return 1;
28917
29797
  }
28918
- writeFileSync3(out, SKELETON.replaceAll("__NAME__", name), "utf8");
29798
+ writeFileSync4(out, SKELETON.replaceAll("__NAME__", name), "utf8");
28919
29799
  console.log(`${paint2(GREEN2, "created")} ${out}`);
28920
29800
  console.log(paint2(DIM2, ` next: runlog validate ${name}.yaml`));
28921
29801
  return 0;
@@ -28977,6 +29857,8 @@ usage:
28977
29857
  runlog bundle <pack> [-o out.json] normalize to a distributable JSON
28978
29858
  runlog test <pack> replay the pack's own fixtures
28979
29859
  runlog init [name] scaffold a new pack
29860
+ runlog docs <pack> [-o dir] write its rulebook, quick start, reference card,
29861
+ [--only kinds] run log sheet and catalog summary (HTML and Markdown)
28980
29862
  runlog keygen [-o key.json] make a signing key for your packs
28981
29863
  runlog sign <pack> --key key.json sign a pack, proving you wrote it
28982
29864
  runlog issue <pack> --to "Name" stamp a copy with a buyer's name and sign it
@@ -29006,6 +29888,8 @@ async function main(argv) {
29006
29888
  return cmdValidate(args);
29007
29889
  case "bundle":
29008
29890
  return cmdBundle(args);
29891
+ case "docs":
29892
+ return cmdDocs(args);
29009
29893
  case "test":
29010
29894
  return cmdTest(args);
29011
29895
  case "keygen":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scrthq/runlog",
3
- "version": "0.0.39",
3
+ "version": "0.0.41",
4
4
  "description": "Validate, lint, bundle and test Runlog rule packs.",
5
5
  "type": "module",
6
6
  "license": "MIT",