@scrthq/runlog 0.0.41 → 0.0.43

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 +108 -11
  2. package/package.json +1 -1
package/dist/runlog.js CHANGED
@@ -26318,6 +26318,9 @@ var Predicate = external_exports.lazy(
26318
26318
  "Tests whether any earlier subject was created from an entry carrying this tag \u2014 for results that only make sense once something of a given kind exists."
26319
26319
  ),
26320
26320
  external_exports.object({ modeIs: external_exports.array(Id).min(1).describe("Mode ids in which this holds true.") }).strict().describe("Tests which mode the run is being played in."),
26321
+ external_exports.object({ phaseDone: Id.describe("Id of a phase in the flow.") }).strict().describe(
26322
+ "True once the named phase has been completed in the current unit. This is how a move that records an outcome is kept off the screen until there is something to have an outcome about."
26323
+ ),
26321
26324
  external_exports.object({ not: Predicate }).strict().describe("True when the inner predicate is false."),
26322
26325
  external_exports.object({ allOf: external_exports.array(Predicate).min(1) }).strict().describe("True only when every listed predicate holds."),
26323
26326
  external_exports.object({ anyOf: external_exports.array(Predicate).min(1) }).strict().describe("True when at least one listed predicate holds.")
@@ -26380,6 +26383,7 @@ var Action = external_exports.lazy(
26380
26383
  do: external_exports.literal("rollOn").describe("Roll on another table and resolve whatever comes up."),
26381
26384
  table: Id.describe("Id of the table to roll on."),
26382
26385
  times: external_exports.number().int().min(1).max(10).optional().describe("How many times to roll. Defaults to 1."),
26386
+ timesFrom: external_exports.string().min(1).optional().describe("Name a total bound earlier with `roll \u2026 into`; that many times, instead of `times`. How a pack rolls for how many to roll."),
26383
26387
  choose: external_exports.enum(["one", "all"]).optional().describe(
26384
26388
  "With times > 1: `one` lets the player pick a single result to apply, `all` applies every result. Defaults to `all`."
26385
26389
  ),
@@ -26516,7 +26520,10 @@ var EntryBase = {
26516
26520
  grants: external_exports.array(Id).optional().describe(
26517
26521
  "States applied to the relevant subject on resolution. Shorthand for an applyState trigger."
26518
26522
  ),
26519
- tags: external_exports.array(external_exports.string()).optional().describe("Free tags, for cross-referencing and for filtering results out in some modes.")
26523
+ tags: external_exports.array(external_exports.string()).optional().describe("Free tags, for cross-referencing and for filtering results out in some modes."),
26524
+ points: external_exports.number().int().min(0).optional().describe(
26525
+ "What a contestant earns for completing this result in a moderated mode. A result with points is a challenge the moderator can award; one without is an effect that lands on everyone."
26526
+ )
26520
26527
  };
26521
26528
  var LookupEntry = external_exports.object({
26522
26529
  ...EntryBase,
@@ -26636,7 +26643,8 @@ var Capability = external_exports.enum([
26636
26643
  "journal",
26637
26644
  "bandsResolution",
26638
26645
  "opposedResolution",
26639
- "keyedResolution"
26646
+ "keyedResolution",
26647
+ "moderated"
26640
26648
  ]).describe("An engine feature this pack needs in order to play correctly.");
26641
26649
  var Vocabulary = external_exports.object({
26642
26650
  run: external_exports.object({
@@ -26667,6 +26675,9 @@ var StateDef = external_exports.object({
26667
26675
  ).optional().describe(
26668
26676
  "What this state means mechanically. Without it the engine would have to know what your state names mean, which is exactly what keeps an engine game-specific. `makesUntargetable` removes the subject from targeting; `excludesFromResult` leaves it in the run but out of the finished piece; `removesFromPlay` takes it out entirely."
26669
26677
  ),
26678
+ group: Id.optional().describe(
26679
+ "States in the same group are mutually exclusive on one holder: applying one removes the others. Use it for outcomes that cannot both be true, like landed and missed."
26680
+ ),
26670
26681
  description: external_exports.string().optional().describe("Explanation shown to the player on hover.")
26671
26682
  }).strict().describe("A persistent condition that sticks to a subject, or to the run, once applied.");
26672
26683
  var CounterDef = external_exports.object({
@@ -26703,12 +26714,16 @@ var ChecklistItem = external_exports.union([
26703
26714
  external_exports.string().min(1),
26704
26715
  external_exports.object({
26705
26716
  text: external_exports.string().min(1).describe("The point to tick off."),
26717
+ optional: external_exports.boolean().optional().describe("May be left unticked: closing the unit does not wait for it. For boxes that record an outcome rather than promise one."),
26718
+ tally: Id.optional().describe(
26719
+ "Each box ticked under this point adds one to this counter, and unticking takes it back. With `shows`, one box per result; without, the point's own box."
26720
+ ),
26706
26721
  shows: external_exports.object({
26707
26722
  table: external_exports.string().describe("Results from this table are listed under the item."),
26708
26723
  scope: external_exports.enum(["unit", "subject", "run"]).default("unit").describe(
26709
26724
  "Which results: those rolled this unit, those that reached this unit's subject, or every one in the run."
26710
26725
  )
26711
- }).strict().describe("What to look at while ticking this: a table's results, listed under the point with a box each.")
26726
+ }).strict().optional().describe("What to look at while ticking this: a table's results, listed under the point with a box each.")
26712
26727
  }).strict().describe("A point with the results it is about shown beneath it.")
26713
26728
  ]).describe("A point to tick off: plain text, or text with the table results it is about.");
26714
26729
  var Step = external_exports.discriminatedUnion("kind", [
@@ -26844,6 +26859,16 @@ var Mode = external_exports.object({
26844
26859
  ).optional().describe(
26845
26860
  "Per-unit overrides, for modes with a fixed shape \u2014 'unit three always suffers a consequence', and the like."
26846
26861
  ),
26862
+ moderated: external_exports.object({
26863
+ contestants: external_exports.object({
26864
+ min: external_exports.number().int().min(1).default(2).describe("Fewest contestants."),
26865
+ max: external_exports.number().int().min(1).default(10).describe("Most contestants.")
26866
+ }).strict().default({ min: 2, max: 10 }).describe("How many people race. The moderator is not one of them."),
26867
+ award: external_exports.enum(["first", "everyone"]).default("first").describe("Whether only the first to finish a challenge scores it, or everyone who finishes does."),
26868
+ firstBonus: external_exports.number().int().min(0).default(0).describe("Extra points for finishing first, on top of the result's own. Meaningful with `everyone`.")
26869
+ }).strict().optional().describe(
26870
+ "Moderated play: one person runs the game on the device, a roster of named contestants races every drawn result, and the moderator awards points. Results with `points` are the challenges; everything else lands on everyone."
26871
+ ),
26847
26872
  notes: external_exports.array(external_exports.string()).optional().describe("Reminders shown to the player in this mode.")
26848
26873
  }).strict().describe(
26849
26874
  "A set of deltas over the base ruleset. Keeping modes as deltas rather than separate rulesets is what stops them drifting apart as the pack evolves."
@@ -26856,6 +26881,9 @@ var MoveDef = external_exports.object({
26856
26881
  ),
26857
26882
  oncePerRun: external_exports.boolean().default(false).describe("Whether the move is spent after a single use."),
26858
26883
  do: external_exports.array(Action).min(1).describe("What happens when the player takes it."),
26884
+ finalizes: external_exports.boolean().optional().describe(
26885
+ "Taking this move also closes the current unit: the rest of its flow is recorded as done and the unit is finalized. For moves that are outcomes, like landed or missed, so the player is not asked to close the unit a second time."
26886
+ ),
26859
26887
  /** Where the move belongs in the interface. */
26860
26888
  when: external_exports.enum(["anytime", "betweenUnits", "beforeEnding"]).default("anytime").describe(
26861
26889
  "When the move is offered: at any point, only between units, or only when the player is deciding whether to stop."
@@ -27332,6 +27360,9 @@ function lintPack(pack) {
27332
27360
  }
27333
27361
  }
27334
27362
  }
27363
+ if ("phaseDone" in p && !pack.phases.some((ph) => ph.id === p.phaseDone)) {
27364
+ d.push(err("ref/unknown-phase", site.path, `predicate references unknown phase ${p.phaseDone}`));
27365
+ }
27335
27366
  });
27336
27367
  }
27337
27368
  const seenPhaseIds = /* @__PURE__ */ new Set();
@@ -27350,8 +27381,11 @@ function lintPack(pack) {
27350
27381
  }
27351
27382
  const points = step.kind === "manual" ? step.checklist : step.kind === "finalizeUnit" ? step.confirm : void 0;
27352
27383
  points?.forEach((item, ii) => {
27353
- if (typeof item !== "string" && !tableIds.has(item.shows.table)) {
27354
- const field = step.kind === "manual" ? "checklist" : "confirm";
27384
+ const field = step.kind === "manual" ? "checklist" : "confirm";
27385
+ if (typeof item !== "string" && item.tally && !counterIds.has(item.tally)) {
27386
+ d.push(err("ref/unknown-counter", `${path}.${field}[${ii}]`, `tallies unknown counter ${item.tally}`));
27387
+ }
27388
+ if (typeof item !== "string" && item.shows && !tableIds.has(item.shows.table)) {
27355
27389
  d.push(err("ref/unknown-table", `${path}.${field}[${ii}]`, `shows unknown table ${item.shows.table}`));
27356
27390
  }
27357
27391
  });
@@ -27535,6 +27569,17 @@ function lintPack(pack) {
27535
27569
  );
27536
27570
  }
27537
27571
  };
27572
+ const moderatedModes = Object.values(pack.modes).filter((m) => m.moderated);
27573
+ const scoringEntries = Object.values(pack.tables).some((t) => t.entries.some((e) => (e.points ?? 0) > 0));
27574
+ requireCap("moderated", moderatedModes.length > 0, "has a moderated mode");
27575
+ if (moderatedModes.length > 0 && !scoringEntries) {
27576
+ d.push(warn("mode/nothing-to-award", "modes", "a mode is moderated, but no table entry carries points, so there is nothing to award"));
27577
+ }
27578
+ for (const [modeId, m] of Object.entries(pack.modes)) {
27579
+ if (m.moderated && m.moderated.contestants.max < m.moderated.contestants.min) {
27580
+ d.push(err("mode/contestant-range", `modes.${modeId}.moderated.contestants`, `max (${m.moderated.contestants.max}) is below min (${m.moderated.contestants.min})`));
27581
+ }
27582
+ }
27538
27583
  requireCap("decks", usesDecks, "uses decks");
27539
27584
  requireCap("resources", usesResources, "uses resources");
27540
27585
  requireCap("counters", counterIds.size > 0, "declares counters");
@@ -27593,7 +27638,8 @@ var IMPLEMENTED_CAPABILITIES = [
27593
27638
  "journal",
27594
27639
  "bandsResolution",
27595
27640
  "opposedResolution",
27596
- "keyedResolution"
27641
+ "keyedResolution",
27642
+ "moderated"
27597
27643
  ];
27598
27644
  function parsePack(input2) {
27599
27645
  if (typeof input2 !== "object" || input2 === null) {
@@ -27969,6 +28015,7 @@ function predicateInWords(pack, p) {
27969
28015
  if ("subjectHasState" in p) return `${targetInWords(pack, p.of)} is ${label(pack, "states", p.subjectHasState)}`;
27970
28016
  if ("priorSubjectTagged" in p) return `an earlier ${n.subject} was tagged \u201C${p.priorSubjectTagged}\u201D`;
27971
28017
  if ("modeIs" in p) return `playing ${p.modeIs.map((m) => label(pack, "modes", m)).join(" or ")}`;
28018
+ if ("phaseDone" in p) return `${pack.phases.find((ph) => ph.id === p.phaseDone)?.label ?? p.phaseDone} is done`;
27972
28019
  if ("not" in p) return `not (${predicateInWords(pack, p.not)})`;
27973
28020
  if ("allOf" in p) return p.allOf.map((q) => predicateInWords(pack, q)).join(" and ");
27974
28021
  if ("anyOf" in p) return p.anyOf.map((q) => predicateInWords(pack, q)).join(" or ");
@@ -28085,6 +28132,11 @@ function modeLength(pack, mode) {
28085
28132
  return `as many ${n.units} as you like${max ? `, up to ${max}` : ""}`;
28086
28133
  }
28087
28134
  function modePlayers(mode) {
28135
+ const m = mode.moderated;
28136
+ if (m) {
28137
+ const n = m.contestants.min === m.contestants.max ? `${m.contestants.min}` : `${m.contestants.min}\u2013${m.contestants.max}`;
28138
+ return `moderated, ${n} contestants, ${m.award === "everyone" ? "everyone who finishes scores" : "first to finish scores"}${m.firstBonus ? ` (+${m.firstBonus} for first)` : ""}`;
28139
+ }
28088
28140
  const p = mode.players;
28089
28141
  if (!p || p.min <= 1 && p.max <= 1) return "solo";
28090
28142
  return p.min === p.max ? `${p.min} players` : `${p.min}\u2013${p.max} players`;
@@ -28273,7 +28325,8 @@ function tableRows(pack, table, opts) {
28273
28325
  if (e.requires?.length) extras.push(`Only if ${conditionsInWords(pack, e.requires)}; otherwise roll again.`);
28274
28326
  }
28275
28327
  const text = [e.title ? `${e.title}. ` : "", sentence(e.text), ...extras.map((x) => ` ${x}`)].join("");
28276
- return [keys[i] ?? "", text];
28328
+ const pts = e.points !== void 0 && e.points > 0 ? `${keys[i] ?? ""} \xB7 ${e.points} pt${e.points === 1 ? "" : "s"}` : keys[i] ?? "";
28329
+ return [pts, text];
28277
28330
  });
28278
28331
  }
28279
28332
  function license(pack) {
@@ -28370,7 +28423,7 @@ function rulebookDoc(pack) {
28370
28423
  states.map(([, s]) => [
28371
28424
  s.short ? `${s.label} (${s.short})` : s.label,
28372
28425
  s.scope === "run" ? `the ${n.run}` : `a ${n.subject}`,
28373
- [s.description ?? "", ...(s.semantics ?? []).map(semanticInWords)].filter(Boolean).join(" ")
28426
+ [s.description ?? "", ...(s.semantics ?? []).map(semanticInWords), s.group ? `Replaces any other \u201C${s.group}\u201D state.` : ""].filter(Boolean).join(" ")
28374
28427
  ])
28375
28428
  );
28376
28429
  }
@@ -28401,7 +28454,7 @@ function rulebookDoc(pack) {
28401
28454
  moves.map(([, m]) => [
28402
28455
  m.label,
28403
28456
  [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(" ")
28457
+ [m.description ?? "", sentence(cap(actionsInWords(pack, m.do))), m.finalizes ? `Closes the ${n.unit}.` : ""].filter(Boolean).join(" ")
28405
28458
  ])
28406
28459
  );
28407
28460
  }
@@ -28743,6 +28796,7 @@ function initialState(pack, e) {
28743
28796
  unit: 0,
28744
28797
  phasesDone: [],
28745
28798
  stepsDone: [],
28799
+ checks: [],
28746
28800
  subjects: [],
28747
28801
  runStates: [],
28748
28802
  counters,
@@ -28755,6 +28809,8 @@ function initialState(pack, e) {
28755
28809
  outcomes: [],
28756
28810
  obligations: [],
28757
28811
  firedOnce: [],
28812
+ contestants: [],
28813
+ awards: [],
28758
28814
  ending: null
28759
28815
  };
28760
28816
  }
@@ -28824,6 +28880,7 @@ function reduce(pack, log) {
28824
28880
  state.unit += 1;
28825
28881
  state.phasesDone = [];
28826
28882
  state.stepsDone = [];
28883
+ state.checks = [];
28827
28884
  resetThisUnit = /* @__PURE__ */ new Set();
28828
28885
  if (state.forcedUnits > 0) state.forcedUnits -= 1;
28829
28886
  if (pack.unit.createsSubject) {
@@ -28861,12 +28918,18 @@ function reduce(pack, log) {
28861
28918
  });
28862
28919
  break;
28863
28920
  case "StateApplied": {
28864
- const scope = pack.states?.[event.state]?.scope ?? "subject";
28921
+ const def = pack.states?.[event.state];
28922
+ const scope = def?.scope ?? "subject";
28923
+ const rivals = (held) => def?.group ? held.filter((s) => s === event.state || pack.states?.[s]?.group !== def.group) : held;
28865
28924
  if (scope === "run" || event.subject === void 0) {
28925
+ state.runStates = rivals(state.runStates);
28866
28926
  if (!state.runStates.includes(event.state)) state.runStates.push(event.state);
28867
28927
  } else {
28868
28928
  const subject = subjectById(state, event.subject);
28869
- if (subject && !subject.states.includes(event.state)) subject.states.push(event.state);
28929
+ if (subject) {
28930
+ subject.states = rivals(subject.states);
28931
+ if (!subject.states.includes(event.state)) subject.states.push(event.state);
28932
+ }
28870
28933
  }
28871
28934
  break;
28872
28935
  }
@@ -28955,6 +29018,40 @@ function reduce(pack, log) {
28955
29018
  state.status = "ended";
28956
29019
  state.ending = event.ending;
28957
29020
  break;
29021
+ case "ContestantAdded":
29022
+ if (!state.contestants.some((c) => c.id === event.contestant)) {
29023
+ state.contestants.push({ id: event.contestant, name: event.name });
29024
+ }
29025
+ break;
29026
+ case "ContestantRemoved":
29027
+ state.contestants = state.contestants.filter((c) => c.id !== event.contestant);
29028
+ break;
29029
+ case "Awarded":
29030
+ if (!state.awards.some((a) => a.contestant === event.contestant && a.outcome === event.outcome)) {
29031
+ state.awards.push({
29032
+ contestant: event.contestant,
29033
+ outcome: event.outcome,
29034
+ table: event.table,
29035
+ entryId: event.entryId,
29036
+ points: event.points,
29037
+ at: event.at
29038
+ });
29039
+ }
29040
+ break;
29041
+ case "AwardRevoked":
29042
+ state.awards = state.awards.filter((a) => !(a.contestant === event.contestant && a.outcome === event.outcome));
29043
+ break;
29044
+ case "Checked": {
29045
+ const key = `${event.step}|${event.item}`;
29046
+ if (event.on) {
29047
+ if (!state.checks.includes(key)) state.checks.push(key);
29048
+ } else {
29049
+ state.checks = state.checks.filter((k) => k !== key);
29050
+ }
29051
+ break;
29052
+ }
29053
+ case "Corrected":
29054
+ break;
28958
29055
  case "Undone":
28959
29056
  break;
28960
29057
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scrthq/runlog",
3
- "version": "0.0.41",
3
+ "version": "0.0.43",
4
4
  "description": "Validate, lint, bundle and test Runlog rule packs.",
5
5
  "type": "module",
6
6
  "license": "MIT",