@scrthq/runlog 0.0.45 → 0.0.46

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 (2) hide show
  1. package/dist/runlog.js +78 -12
  2. package/package.json +1 -1
package/dist/runlog.js CHANGED
@@ -26718,6 +26718,12 @@ var ResourceDef = external_exports.object({
26718
26718
  }).strict().describe(
26719
26719
  "A numeric track the player fills or spends: a progress clock, a word count, training volume, stock on hand."
26720
26720
  );
26721
+ var Clock = external_exports.object({
26722
+ kind: external_exports.enum(["stopwatch", "timer"]).describe("A stopwatch counts up from the start of the unit; a timer counts down from `minutes`."),
26723
+ minutes: external_exports.number().positive().max(24 * 60).optional().describe("How long a timer runs. Required for a timer; ignored by a stopwatch."),
26724
+ label: external_exports.string().optional().describe("What the clock is called on screen. Defaults to the unit's name and number."),
26725
+ auto: external_exports.boolean().default(true).describe("Start it when the unit is entered and stop it when the unit closes. Off, the player starts it by hand; it still stops with the unit.")
26726
+ }).strict().refine((c) => c.kind !== "timer" || c.minutes !== void 0, { message: "a timer needs minutes", path: ["minutes"] }).describe("A clock the unit runs: `stopwatch` to time it, `timer` with `minutes` to limit it. The time lands in the log when the unit closes.");
26721
26727
  var Requirement = external_exports.object({
26722
26728
  id: Id.describe("Unique among requirements. Table entries name it in `needs`."),
26723
26729
  label: external_exports.string().min(1).describe("What it is, in the words a person would use: Rocket League, a potter's wheel, an oven."),
@@ -26875,6 +26881,7 @@ var Mode = external_exports.object({
26875
26881
  ).optional().describe(
26876
26882
  "Per-unit overrides, for modes with a fixed shape \u2014 'unit three always suffers a consequence', and the like."
26877
26883
  ),
26884
+ clock: Clock.optional().describe("This mode's clock on every unit, instead of the pack's `unit.clock`."),
26878
26885
  moderated: external_exports.object({
26879
26886
  contestants: external_exports.object({
26880
26887
  min: external_exports.number().int().min(1).default(2).describe("Fewest contestants."),
@@ -26969,6 +26976,7 @@ var Pack = external_exports.object({
26969
26976
  ),
26970
26977
  vocabulary: Vocabulary,
26971
26978
  unit: external_exports.object({
26979
+ clock: Clock.optional().describe("A clock every unit runs. A mode can set its own with `clock`."),
26972
26980
  createsSubject: external_exports.boolean().default(true).describe("Whether entering a unit produces a new subject."),
26973
26981
  min: external_exports.number().int().min(1).default(1).describe("Fewest units in a run."),
26974
26982
  max: external_exports.number().int().min(1).default(20).describe("Most units in a run.")
@@ -27316,6 +27324,7 @@ function lintPack(pack) {
27316
27324
  }
27317
27325
  usesDecks = true;
27318
27326
  break;
27327
+ case "startStopwatch":
27319
27328
  case "startTimer":
27320
27329
  usesTimers = true;
27321
27330
  break;
@@ -27619,7 +27628,8 @@ function lintPack(pack) {
27619
27628
  requireCap("decks", usesDecks, "uses decks");
27620
27629
  requireCap("resources", usesResources, "uses resources");
27621
27630
  requireCap("counters", counterIds.size > 0, "declares counters");
27622
- requireCap("timers", usesTimers, "starts timers");
27631
+ const unitClock = Boolean(pack.unit.clock) || Object.values(pack.modes).some((m) => m.clock);
27632
+ requireCap("timers", usesTimers || unitClock, "runs a clock");
27623
27633
  requireCap(
27624
27634
  "backwardTargeting",
27625
27635
  usesResolveTarget || !!pack.targeting && pack.targeting.strategy !== "none",
@@ -27994,6 +28004,14 @@ function howConsulted(table) {
27994
28004
  return "Find the row whose key matches what was drawn.";
27995
28005
  }
27996
28006
  }
28007
+ function an(word, capital = false) {
28008
+ const w = word.trim().toLowerCase();
28009
+ const vowel = /^[aeiou]/.test(w);
28010
+ const soundsConsonant = /^(u[^aeiou]|un[aeiou]|uni|use|usu|ute|eu|one(?![a-z])|onc)/.test(w) || /^u[st][aeiou]/.test(w);
28011
+ const silentH = /^(hour|honest|honor|heir|herb)/.test(w);
28012
+ const article = vowel && !soundsConsonant || silentH ? "an" : "a";
28013
+ return `${capital ? article[0].toUpperCase() + article.slice(1) : article} ${word}`;
28014
+ }
27997
28015
  function nouns(pack) {
27998
28016
  const v = pack.vocabulary;
27999
28017
  return {
@@ -28110,6 +28128,8 @@ function actionInWords(pack, a) {
28110
28128
  }
28111
28129
  case "startTimer":
28112
28130
  return a.label ? `${a.label} (${a.minutes} min)` : `start a ${a.minutes}-minute timer`;
28131
+ case "startStopwatch":
28132
+ return a.label ? `start the ${a.label} stopwatch` : "start a stopwatch";
28113
28133
  case "note":
28114
28134
  return a.text;
28115
28135
  case "setFlag":
@@ -28138,7 +28158,7 @@ function triggerPointInWords(pack, on) {
28138
28158
  case "immediately":
28139
28159
  return "Then";
28140
28160
  case "onEnterUnit":
28141
- return `When a ${n.unit} begins`;
28161
+ return `When ${an(n.unit)} begins`;
28142
28162
  case "onDeclareSubject":
28143
28163
  return `When the ${n.subject} is declared`;
28144
28164
  case "afterWork":
@@ -28309,7 +28329,8 @@ function whatYouNeed(pack) {
28309
28329
  if (deck.kind === "standard52") items.push(`A standard deck of playing cards${deck.includeJokers ? " with the jokers in" : ""}.`);
28310
28330
  else items.push(`${plural(deck.cards.length, "card", "cards")} for ${deck.title} \u2014 write them out, or use the app.`);
28311
28331
  }
28312
- if (pack.capabilities.includes("timers")) items.push("A timer.");
28332
+ if (pack.capabilities.includes("timers") && !pack.unit.clock) items.push("A timer.");
28333
+ if (pack.unit.clock) items.push(pack.unit.clock.kind === "timer" ? `A clock: every ${n.unit} is timed, ${pack.unit.clock.minutes} minutes. The app keeps it.` : `A clock: every ${n.unit} is timed. The app keeps it.`);
28313
28334
  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.`);
28314
28335
  return items;
28315
28336
  }
@@ -28385,7 +28406,7 @@ function summaryDoc(pack) {
28385
28406
  b.h(2, "How it plays");
28386
28407
  const unitRange = `${pack.unit.min === pack.unit.max ? pack.unit.min : `${pack.unit.min}\u2013${pack.unit.max}`}`;
28387
28408
  b.p(
28388
- `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(", ")}.`
28409
+ `${an(n.run, true)} is a series of ${n.units}${pack.unit.createsSubject ? `, each producing ${an(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(", ")}.`
28389
28410
  );
28390
28411
  b.table(["Mode", "Length", "Players", "About"], modeRows(pack, { notes: false }));
28391
28412
  b.h(2, "What is inside");
@@ -28395,7 +28416,7 @@ function summaryDoc(pack) {
28395
28416
  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("; ")}.`);
28396
28417
  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("; ")}.`);
28397
28418
  const states = Object.values(pack.states ?? {});
28398
- if (states.length) parts.push(`${plural(states.length, "state", "states")} a ${n.subject} or ${n.run} can carry: ${states.map((s) => s.label).join(", ")}.`);
28419
+ if (states.length) parts.push(`${plural(states.length, "state", "states")} ${an(n.subject)} or ${n.run} can carry: ${states.map((s) => s.label).join(", ")}.`);
28399
28420
  const counters = Object.values(pack.counters ?? {}).filter((c) => !c.hidden);
28400
28421
  if (counters.length) parts.push(`${plural(counters.length, "tally", "tallies")}: ${counters.map((c) => c.label).join(", ")}.`);
28401
28422
  const resources = Object.values(pack.resources ?? {});
@@ -28420,14 +28441,14 @@ function rulebookDoc(pack) {
28420
28441
  b.list(whatYouNeed(pack));
28421
28442
  b.h(2, "Terms");
28422
28443
  b.terms([
28423
- { 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.` },
28444
+ { term: cap(n.run), text: `One session of play. ${an(n.run, true)} is a series of ${n.units}, and ends when you say so or when the game does.` },
28424
28445
  { term: cap(n.unit), text: `One round. Every ${n.unit} passes through the same phases${pack.unit.createsSubject ? ` and produces one ${n.subject}` : ""}.` },
28425
- ...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.` }] : [],
28426
- { term: cap(n.finalize), text: `Closing a ${n.unit}. Once ${n.finalize.toLowerCase()}d, what it made is fixed unless a rule says otherwise.` },
28446
+ ...pack.unit.createsSubject ? [{ term: cap(n.subject), text: `The thing ${an(n.unit)} makes. States attach to it, and consequences can land on it later.` }] : [],
28447
+ { term: cap(n.finalize), text: `Closing ${an(n.unit)}. Once ${n.finalize.toLowerCase()}d, what it made is fixed unless a rule says otherwise.` },
28427
28448
  ...Object.entries(pack.vocabulary.terms ?? {}).map(([term, text]) => ({ term, text }))
28428
28449
  ]);
28429
28450
  if (pack.hierarchy?.length) b.p(`When rules contradict each other, the more specific wins, in this order: ${pack.hierarchy.join(" > ")}.`, "note");
28430
- b.h(2, `A ${n.unit}, step by step`);
28451
+ b.h(2, `${an(n.unit, true)}, step by step`);
28431
28452
  b.list(flowSteps(pack, { detail: true }), true);
28432
28453
  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." : ""}`);
28433
28454
  b.h(2, "Modes");
@@ -28462,7 +28483,7 @@ function rulebookDoc(pack) {
28462
28483
  ["State", "On", "Means"],
28463
28484
  states.map(([, s]) => [
28464
28485
  s.short ? `${s.label} (${s.short})` : s.label,
28465
- s.scope === "run" ? `the ${n.run}` : s.scope === "contestant" ? "a contestant" : `a ${n.subject}`,
28486
+ s.scope === "run" ? `the ${n.run}` : s.scope === "contestant" ? "a contestant" : an(n.subject),
28466
28487
  [s.description ?? "", ...(s.semantics ?? []).map(semanticInWords), s.group ? `Replaces any other \u201C${s.group}\u201D state.` : "", s.until === "unitEnd" ? `Lifts when the ${n.unit} closes.` : ""].filter(Boolean).join(" ")
28467
28488
  ])
28468
28489
  );
@@ -28507,7 +28528,7 @@ function rulebookDoc(pack) {
28507
28528
  b.list(pack.triggers.map((t) => sentence(cap(triggerInWords(pack, t)))));
28508
28529
  }
28509
28530
  if (pack.endings?.length) {
28510
- b.h(2, `Ending a ${n.run}`);
28531
+ b.h(2, `Ending ${an(n.run)}`);
28511
28532
  b.terms(pack.endings.map((e) => ({ term: e.label, text: [e.text ?? "", e.requires?.length ? `Available when ${conditionsInWords(pack, e.requires)}.` : ""].filter(Boolean).join(" ") })));
28512
28533
  }
28513
28534
  return { kind: "rulebook", layout: "book", title: pack.title, subtitle: byline(pack), blocks: b.blocks };
@@ -28519,7 +28540,7 @@ function quickstartDoc(pack) {
28519
28540
  b.h(2, "What you need");
28520
28541
  b.list(whatYouNeed(pack));
28521
28542
  b.h(2, "The words");
28522
- 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.`);
28543
+ b.p(`${an(n.run, true)} 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.`);
28523
28544
  b.h(2, `Each ${n.unit}`);
28524
28545
  b.list(flowSteps(pack, { detail: false }), true);
28525
28546
  const d = pack.modes[pack.defaultMode];
@@ -28850,6 +28871,7 @@ function initialState(pack, e) {
28850
28871
  obligations: [],
28851
28872
  firedOnce: [],
28852
28873
  lacks: e.lacks ?? [],
28874
+ clocks: [],
28853
28875
  contestants: [],
28854
28876
  awards: [],
28855
28877
  ending: null
@@ -29063,6 +29085,50 @@ function reduce(pack, log) {
29063
29085
  state.status = "ended";
29064
29086
  state.ending = event.ending;
29065
29087
  break;
29088
+ case "ClockStarted":
29089
+ if (!state.clocks.some((c) => c.id === event.clock)) {
29090
+ state.clocks.push({
29091
+ id: event.clock,
29092
+ kind: event.kind,
29093
+ label: event.label,
29094
+ seconds: event.kind === "timer" ? event.seconds ?? null : null,
29095
+ unit: state.unit,
29096
+ status: "running",
29097
+ startedAt: event.at,
29098
+ runningSince: event.at,
29099
+ accumulatedMs: 0,
29100
+ elapsedMs: null,
29101
+ expired: false
29102
+ });
29103
+ }
29104
+ break;
29105
+ case "ClockPaused": {
29106
+ const c = state.clocks.find((x) => x.id === event.clock);
29107
+ if (c && c.status === "running" && c.runningSince) {
29108
+ c.accumulatedMs += Math.max(0, Date.parse(event.at) - Date.parse(c.runningSince));
29109
+ c.runningSince = null;
29110
+ c.status = "paused";
29111
+ }
29112
+ break;
29113
+ }
29114
+ case "ClockResumed": {
29115
+ const c = state.clocks.find((x) => x.id === event.clock);
29116
+ if (c && c.status === "paused") {
29117
+ c.runningSince = event.at;
29118
+ c.status = "running";
29119
+ }
29120
+ break;
29121
+ }
29122
+ case "ClockStopped": {
29123
+ const c = state.clocks.find((x) => x.id === event.clock);
29124
+ if (c && c.status !== "done") {
29125
+ c.elapsedMs = event.elapsedMs;
29126
+ c.runningSince = null;
29127
+ c.status = "done";
29128
+ c.expired = event.expired === true;
29129
+ }
29130
+ break;
29131
+ }
29066
29132
  case "ContestantAdded":
29067
29133
  if (!state.contestants.some((c) => c.id === event.contestant)) {
29068
29134
  state.contestants.push({ id: event.contestant, name: event.name, states: [] });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scrthq/runlog",
3
- "version": "0.0.45",
3
+ "version": "0.0.46",
4
4
  "description": "Validate, lint, bundle and test Runlog rule packs.",
5
5
  "type": "module",
6
6
  "license": "MIT",