@heroiclands/package-build 19.0.0 → 20.2.0
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.
- package/CHANGELOG.md +1100 -0
- package/CONTENT.md +264 -33
- package/README.md +43 -4
- package/bin/content-build.mjs +94 -3
- package/config.mjs +9 -1
- package/content-config.mjs +99 -19
- package/docs/content-format.md +394 -72
- package/e2e.mjs +297 -3
- package/engine/actor-compiler.mjs +197 -7
- package/engine/address-charset.mjs +23 -5
- package/engine/base-compiler.mjs +63 -2
- package/engine/bundles.mjs +9 -0
- package/engine/content-address.mjs +92 -1
- package/engine/content-charset.mjs +434 -0
- package/engine/content-format.mjs +102 -0
- package/engine/content-icons.mjs +388 -0
- package/engine/content-index.mjs +11 -8
- package/engine/content-links.mjs +37 -21
- package/engine/field-reference.mjs +57 -5
- package/engine/field-spec.mjs +214 -7
- package/engine/folder-notes.mjs +24 -1
- package/engine/foreign-catalog.mjs +112 -7
- package/engine/foundry-entries.mjs +14 -0
- package/engine/frontmatter-lint.mjs +377 -56
- package/engine/frontmatter.mjs +11 -11
- package/engine/generate.mjs +72 -12
- package/engine/helpers.mjs +96 -10
- package/engine/index.mjs +9 -0
- package/engine/item-compiler.mjs +37 -0
- package/engine/journals.mjs +21 -4
- package/engine/macros.mjs +8 -0
- package/engine/map-notes.mjs +7 -7
- package/engine/note-claims.mjs +208 -5
- package/engine/note-ids.mjs +25 -1
- package/engine/note-vocabulary.mjs +76 -9
- package/engine/pack-config.mjs +102 -12
- package/engine/pack-router.mjs +0 -0
- package/engine/prose-config.mjs +42 -0
- package/engine/prose-lint.mjs +126 -0
- package/engine/retired-fields.mjs +57 -16
- package/engine/runtime-only-fields.mjs +204 -0
- package/engine/scenes.mjs +12 -19
- package/engine/schema-check.mjs +23 -1
- package/engine/schema-extract.mjs +13 -0
- package/engine/site-index.mjs +17 -0
- package/engine/subtype-registry.mjs +30 -0
- package/engine/system-block.mjs +81 -3
- package/engine/web-wikilinks.mjs +33 -27
- package/engine/wikilink-syntax.mjs +7 -0
- package/engine/wikilinks.mjs +74 -16
- package/hm3/actors.mjs +63 -13
- package/package.json +2 -2
- package/sohl/actors.mjs +106 -7
- package/sohl/item-fields.mjs +203 -0
- package/sohl/note-schemas.mjs +6 -3
- package/types/config.d.mts +7 -0
- package/types/e2e.d.mts +130 -3
- package/types/engine/actor-compiler.d.mts +83 -3
- package/types/engine/address-charset.d.mts +22 -4
- package/types/engine/base-compiler.d.mts +54 -3
- package/types/engine/content-address.d.mts +64 -0
- package/types/engine/content-charset.d.mts +127 -0
- package/types/engine/content-format.d.mts +9 -0
- package/types/engine/content-icons.d.mts +151 -0
- package/types/engine/field-spec.d.mts +271 -3
- package/types/engine/folder-notes.d.mts +20 -0
- package/types/engine/foreign-catalog.d.mts +38 -2
- package/types/engine/foundry-entries.d.mts +6 -0
- package/types/engine/frontmatter-lint.d.mts +164 -30
- package/types/engine/frontmatter.d.mts +11 -11
- package/types/engine/generate.d.mts +27 -0
- package/types/engine/helpers.d.mts +45 -9
- package/types/engine/index.d.mts +3 -0
- package/types/engine/map-notes.d.mts +2 -2
- package/types/engine/note-claims.d.mts +67 -0
- package/types/engine/note-ids.d.mts +14 -0
- package/types/engine/pack-config.d.mts +35 -0
- package/types/engine/prose-config.d.mts +41 -0
- package/types/engine/prose-lint.d.mts +36 -0
- package/types/engine/retired-fields.d.mts +29 -13
- package/types/engine/runtime-only-fields.d.mts +102 -0
- package/types/engine/schema-check.d.mts +10 -1
- package/types/engine/subtype-registry.d.mts +21 -0
- package/types/engine/system-block.d.mts +28 -2
- package/types/sohl/actors.d.mts +3 -3
package/sohl/actors.mjs
CHANGED
|
@@ -63,6 +63,14 @@ import { SOHL_DOCUMENT_SUBTYPES } from "./document-subtypes.mjs";
|
|
|
63
63
|
// verbatim, and `sohl.img` / `sohl.effects` / `sohl.flags` overriding their
|
|
64
64
|
// shared top-level forms for this system alone (#58).
|
|
65
65
|
import { blockProperty, mergeSystemData } from "../engine/system-block.mjs";
|
|
66
|
+
import { readField, retiredTopLevelKey } from "../engine/field-spec.mjs";
|
|
67
|
+
// The retirement window's reports, shared with the frontmatter lint so the two
|
|
68
|
+
// cannot say different things about the same key (#305, #332).
|
|
69
|
+
import {
|
|
70
|
+
legacyKeyMessage,
|
|
71
|
+
locateFrontmatterKey,
|
|
72
|
+
retiredTopLevelMessage,
|
|
73
|
+
} from "../engine/retired-fields.mjs";
|
|
66
74
|
|
|
67
75
|
/**
|
|
68
76
|
* The system this pass compiles for — the block its notes write.
|
|
@@ -81,6 +89,38 @@ const DEFAULT_IMG = {
|
|
|
81
89
|
being: "systems/sohl/assets/icons/game-icons/delapouite/person.svg",
|
|
82
90
|
};
|
|
83
91
|
|
|
92
|
+
/**
|
|
93
|
+
* The being's sheet portrait — the one row of the content format's actor
|
|
94
|
+
* mapping table that is authored rather than derived.
|
|
95
|
+
*
|
|
96
|
+
* **Declared, because the position is not a spelling anyone can guess.** This
|
|
97
|
+
* was read with `blockProperty(fm, SYSTEM, "portrait")`, which knows the block
|
|
98
|
+
* and the note's top level and nothing else — so `data.portrait`, the position
|
|
99
|
+
* the specification names and `sohl-thalorna` writes on 646 beings, was
|
|
100
|
+
* invisible, and `?? defaultImg` on the next line turned every miss into the
|
|
101
|
+
* generic person icon rather than into a complaint (#332). Going through
|
|
102
|
+
* {@link module:engine/field-spec.readField} is what makes the mapping table
|
|
103
|
+
* executable here as it already is for HM3's `data.species`.
|
|
104
|
+
*
|
|
105
|
+
* `img` is deliberately **not** declared beside it: the mapping table keeps a
|
|
106
|
+
* note's token art at the top level, so `blockProperty` is the whole of its
|
|
107
|
+
* resolution and there is no `data.img` to reach for.
|
|
108
|
+
*
|
|
109
|
+
* @type {import("../engine/field-spec.mjs").FieldSpec}
|
|
110
|
+
*/
|
|
111
|
+
const PORTRAIT_FIELD = Object.freeze({
|
|
112
|
+
name: "data.portrait",
|
|
113
|
+
legacyKey: "portrait",
|
|
114
|
+
to: "portrait",
|
|
115
|
+
shape: "path",
|
|
116
|
+
// The two empties survive, because the caller's `?? defaultImg` is what
|
|
117
|
+
// tells them apart: `null` and an absent key mean "no art named, default
|
|
118
|
+
// me", `""` means "ship blank on purpose" (#218).
|
|
119
|
+
read: (raw) => resolveImg(raw),
|
|
120
|
+
default: null,
|
|
121
|
+
describe: "Path to the portrait image.",
|
|
122
|
+
});
|
|
123
|
+
|
|
84
124
|
/**
|
|
85
125
|
* The default art for an actor subtype.
|
|
86
126
|
*
|
|
@@ -206,21 +246,48 @@ export class Actors extends SystemActorCompiler {
|
|
|
206
246
|
this.errorCount++;
|
|
207
247
|
return;
|
|
208
248
|
}
|
|
209
|
-
const { shortcode, type, ...rest } = entry;
|
|
210
|
-
if (
|
|
211
|
-
this.noteError(
|
|
249
|
+
const { model, shortcode, type, ...rest } = entry;
|
|
250
|
+
if (shortcode !== undefined) {
|
|
251
|
+
this.noteError(
|
|
252
|
+
`${ctx}: sohl.items[${index}] carries a top-level ` +
|
|
253
|
+
`\`shortcode\` — that key is retired. Name the item ` +
|
|
254
|
+
`this entry copies with \`model:\` (an address, e.g. ` +
|
|
255
|
+
`\`${type ?? "skill"}-${shortcode}\`), and use ` +
|
|
256
|
+
`\`system.shortcode\` for this item's own identity.`,
|
|
257
|
+
);
|
|
258
|
+
this.errorCount++;
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const read = model === undefined ? null : this.readModel(model, index, ctx);
|
|
262
|
+
if (model !== undefined && !read) return;
|
|
263
|
+
if (read && type !== undefined) {
|
|
264
|
+
this.noteError(
|
|
265
|
+
`${ctx}: sohl.items[${index}] states both \`model\` and ` +
|
|
266
|
+
`\`type\` — the model's address already names the ` +
|
|
267
|
+
`type, so the second is a place to be wrong. Drop \`type\`.`,
|
|
268
|
+
);
|
|
269
|
+
this.errorCount++;
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const effectiveType = read ? read.type : type;
|
|
273
|
+
if (!effectiveType) {
|
|
274
|
+
this.noteError(
|
|
275
|
+
`${ctx}: sohl.items[${index}] names no \`model\` and no ` +
|
|
276
|
+
`\`type\` — an entry that copies nothing must state ` +
|
|
277
|
+
`\`name\`, \`type\` and \`system.shortcode\`.`,
|
|
278
|
+
);
|
|
212
279
|
this.errorCount++;
|
|
213
280
|
return;
|
|
214
281
|
}
|
|
215
282
|
const embedded = this.resolveEmbedded(
|
|
216
283
|
itemsMap,
|
|
217
284
|
actorId,
|
|
218
|
-
|
|
219
|
-
shortcode
|
|
285
|
+
effectiveType,
|
|
286
|
+
read ? read.shortcode : null,
|
|
220
287
|
rest,
|
|
221
288
|
`items:${index}`,
|
|
222
289
|
ctx,
|
|
223
|
-
{ fmKey: "items" },
|
|
290
|
+
{ fmKey: "items", modelPackage: read?.package ?? null },
|
|
224
291
|
);
|
|
225
292
|
if (embedded) items.push(embedded);
|
|
226
293
|
});
|
|
@@ -298,6 +365,36 @@ export class Actors extends SystemActorCompiler {
|
|
|
298
365
|
const { value: authoredFolder, isAddress } = folderField(fm);
|
|
299
366
|
const folder = this.folderResolver(authoredFolder, { isAddress });
|
|
300
367
|
|
|
368
|
+
// The two retiring positions a declared field may be read from (#305,
|
|
369
|
+
// #332). **Warnings**, on the pattern every retirement in this package
|
|
370
|
+
// follows: the note compiles to the correct document either way, so
|
|
371
|
+
// reddening a tree over one would refuse before the sweep rather than
|
|
372
|
+
// after it. What they buy is a count — the whole reason #332 was
|
|
373
|
+
// invisible for so long is that nothing said which position a value
|
|
374
|
+
// had come from, and a default is indistinguishable from a miss.
|
|
375
|
+
const portraitReports = {
|
|
376
|
+
block: SYSTEM,
|
|
377
|
+
onLegacyKey: (field) =>
|
|
378
|
+
this.noteWarn(
|
|
379
|
+
legacyKeyMessage(SYSTEM, field),
|
|
380
|
+
locateFrontmatterKey(this.currentNote?.absPath, field.legacyKey),
|
|
381
|
+
),
|
|
382
|
+
// Anchored at column 1: the two positions share a spelling here —
|
|
383
|
+
// `sohl.portrait` and `portrait` — so a locator that took the first
|
|
384
|
+
// match would point at the block key while the message named the
|
|
385
|
+
// top-level one.
|
|
386
|
+
onRetiredTopLevel: (field) =>
|
|
387
|
+
this.noteWarn(
|
|
388
|
+
retiredTopLevelMessage(field),
|
|
389
|
+
locateFrontmatterKey(
|
|
390
|
+
this.currentNote?.absPath,
|
|
391
|
+
retiredTopLevelKey(field),
|
|
392
|
+
undefined,
|
|
393
|
+
{ topLevel: true },
|
|
394
|
+
),
|
|
395
|
+
),
|
|
396
|
+
};
|
|
397
|
+
|
|
301
398
|
const system = {
|
|
302
399
|
// The frontmatter shortcode is the actor's stable `(type, shortcode)`
|
|
303
400
|
// key — and, for a being that is an archetype, its archetype
|
|
@@ -311,7 +408,9 @@ export class Actors extends SystemActorCompiler {
|
|
|
311
408
|
templatePriority: systemTemplatePriority(fm, ctx),
|
|
312
409
|
// Nullish, not `||` (#218): a note that names no portrait gets the
|
|
313
410
|
// subtype's default, one that writes `""` ships blank on purpose.
|
|
314
|
-
|
|
411
|
+
// Resolved through the declaration so `data.portrait` is reached at
|
|
412
|
+
// all — see {@link PORTRAIT_FIELD} (#332).
|
|
413
|
+
portrait: readField(PORTRAIT_FIELD, fm, portraitReports) ?? defaultImg,
|
|
315
414
|
appearance: renderSection(body || "", "appearance"),
|
|
316
415
|
dossier: renderSection(body || "", "dossier"),
|
|
317
416
|
};
|
package/sohl/item-fields.mjs
CHANGED
|
@@ -103,6 +103,40 @@ function noteContext(fm, type) {
|
|
|
103
103
|
return `${type} "${fm?.name?.full ?? fm?.shortcode ?? "?"}"`;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
/**
|
|
107
|
+
* A timed phase's duration **formula** — what is rolled to seed the interval.
|
|
108
|
+
*
|
|
109
|
+
* SoHL's own field is a nullable, non-blank `StringField`, and the interval it
|
|
110
|
+
* yields is in **seconds** (`"432000"` is the five days the world setting
|
|
111
|
+
* defaults to). A bare number is as valid as a dice expression, and both are
|
|
112
|
+
* stringified here so a note may write either — which is also why no `kind` is
|
|
113
|
+
* declared: a lint claiming this must be a string would report `86400` as an
|
|
114
|
+
* authoring mistake when it is the commonest thing to write.
|
|
115
|
+
*
|
|
116
|
+
* Blank reads as unset rather than as `""`, matching the field's `blank: false`
|
|
117
|
+
* — but a field declaring {@link FieldSpec.omitWhenAbsent} skips before this
|
|
118
|
+
* runs when the note carries nothing, so the `null` is only ever reached by a
|
|
119
|
+
* note that cleared the key on purpose.
|
|
120
|
+
*/
|
|
121
|
+
const DURATION_FORMULA = Object.freeze({
|
|
122
|
+
shape: "roll formula, or a whole number of seconds",
|
|
123
|
+
read: (raw) => (raw == null || raw === "" ? null : String(raw)),
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* A timed phase's rolled duration, in **seconds** — the fact, where the formula
|
|
128
|
+
* above is the definition.
|
|
129
|
+
*
|
|
130
|
+
* Authoring it states the interval outright instead of leaving it to a roll,
|
|
131
|
+
* which is what a fixed-cadence phase wants. Non-numeric reads `0` rather than
|
|
132
|
+
* `NaN`, as every other count in this vocabulary does.
|
|
133
|
+
*/
|
|
134
|
+
const DURATION_BASE = Object.freeze({
|
|
135
|
+
shape: "whole number of seconds",
|
|
136
|
+
kind: "number",
|
|
137
|
+
read: (raw) => (raw == null || raw === "" ? null : Number(raw) || 0),
|
|
138
|
+
});
|
|
139
|
+
|
|
106
140
|
/** Aptitude weights per skill selector, validated as whole numbers. */
|
|
107
141
|
const SKILL_APTITUDES = Object.freeze({
|
|
108
142
|
shape: "map of skill selector → whole number",
|
|
@@ -414,6 +448,104 @@ export const ITEM_FIELDS = Object.freeze({
|
|
|
414
448
|
describe:
|
|
415
449
|
"What running the course to the end does to the host: `death`, or the benign default `cured`.",
|
|
416
450
|
},
|
|
451
|
+
// The two **authored** thirds of each timed-phase triplet (#329). Every
|
|
452
|
+
// one omits its key when the note does not carry it, because the value
|
|
453
|
+
// that stands otherwise is the DataModel's `initial: null` — writing a
|
|
454
|
+
// compile-time `null` over it would say "this phase takes no time",
|
|
455
|
+
// which is a different claim from "this note does not set the phase",
|
|
456
|
+
// and `AfflictionLogic.rollDuration()` opens `if (!formula) return 0`.
|
|
457
|
+
//
|
|
458
|
+
// Until they were declared here the only way to author one was the raw
|
|
459
|
+
// `system:` passthrough — undocumented, uncoerced, and absent from the
|
|
460
|
+
// field list every author-facing surface is built from — so no note in
|
|
461
|
+
// any tree wrote one and every shipped affliction had zero-length
|
|
462
|
+
// phases.
|
|
463
|
+
// Spelled out, one entry per field, for the reason the dates below are:
|
|
464
|
+
// a name assembled from a phase argument is not in the source, so it
|
|
465
|
+
// cannot be grepped, and each phase's prose differs anyway — which is
|
|
466
|
+
// the whole content of the declaration.
|
|
467
|
+
{
|
|
468
|
+
name: "onsetDurationFormula",
|
|
469
|
+
to: "onsetDurationFormula",
|
|
470
|
+
...DURATION_FORMULA,
|
|
471
|
+
omitWhenAbsent: true,
|
|
472
|
+
describe:
|
|
473
|
+
"Interval from contracting the affliction to the start of onset. Omitted when unset, leaving no incubation.",
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
name: "onsetDurationBase",
|
|
477
|
+
to: "onsetDurationBase",
|
|
478
|
+
...DURATION_BASE,
|
|
479
|
+
omitWhenAbsent: true,
|
|
480
|
+
describe:
|
|
481
|
+
"The onset interval in seconds, standing in for a roll of the formula. Omitted when unset.",
|
|
482
|
+
},
|
|
483
|
+
{
|
|
484
|
+
name: "healingCheckDurationFormula",
|
|
485
|
+
to: "healingCheckDurationFormula",
|
|
486
|
+
...DURATION_FORMULA,
|
|
487
|
+
omitWhenAbsent: true,
|
|
488
|
+
describe:
|
|
489
|
+
"Interval between healing checks, once the affliction is symptomatic. Omitted when unset.",
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
name: "healingCheckDurationBase",
|
|
493
|
+
to: "healingCheckDurationBase",
|
|
494
|
+
...DURATION_BASE,
|
|
495
|
+
omitWhenAbsent: true,
|
|
496
|
+
describe:
|
|
497
|
+
"The healing-check interval in seconds, standing in for a roll of the formula. Omitted when unset.",
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
name: "resolutionDurationFormula",
|
|
501
|
+
to: "resolutionDurationFormula",
|
|
502
|
+
...DURATION_FORMULA,
|
|
503
|
+
omitWhenAbsent: true,
|
|
504
|
+
describe:
|
|
505
|
+
"Interval from onset to the affliction running its course. Omitted when unset.",
|
|
506
|
+
},
|
|
507
|
+
{
|
|
508
|
+
name: "resolutionDurationBase",
|
|
509
|
+
to: "resolutionDurationBase",
|
|
510
|
+
...DURATION_BASE,
|
|
511
|
+
omitWhenAbsent: true,
|
|
512
|
+
describe:
|
|
513
|
+
"The resolution interval in seconds, standing in for a roll of the formula. Omitted when unset.",
|
|
514
|
+
},
|
|
515
|
+
// The `…Date` third of every timed-phase triplet, declared as **runtime
|
|
516
|
+
// state** (#330). A phase is authored as its `…DurationFormula`; the
|
|
517
|
+
// `…DurationBase` records what that rolled to; the date records *when
|
|
518
|
+
// it happened*, and only play can know that. All four are
|
|
519
|
+
// `worldTimeDateField()`s in SoHL's schema — nullable rather than
|
|
520
|
+
// sentinelled precisely because `0` is itself a valid world time — so
|
|
521
|
+
// there is no blank an author could honestly write either.
|
|
522
|
+
//
|
|
523
|
+
// Declaring them does two things at once: the key is left out of the
|
|
524
|
+
// compiled document, so SoHL's own `initial: null` stands, and a note
|
|
525
|
+
// authoring one is refused with a message naming the field and its
|
|
526
|
+
// reason. Written out rather than generated from a phase name, as the
|
|
527
|
+
// schema they mirror now is: a field name assembled from an argument is
|
|
528
|
+
// not in the source, and cannot be grepped, read or reported on.
|
|
529
|
+
{
|
|
530
|
+
to: "contractDate",
|
|
531
|
+
runtimeOnly: "the world time the host contracted the affliction",
|
|
532
|
+
describe: "When the host contracted it. Play state, never authored.",
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
to: "onsetDate",
|
|
536
|
+
runtimeOnly: "the world time onset fired at",
|
|
537
|
+
describe: "When onset fired. Play state, never authored.",
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
to: "treatmentDate",
|
|
541
|
+
runtimeOnly: "the world time the affliction was last treated",
|
|
542
|
+
describe: "When it was last treated. Play state, never authored.",
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
to: "resolutionDate",
|
|
546
|
+
runtimeOnly: "the world time the affliction ran its course",
|
|
547
|
+
describe: "When it resolved. Play state, never authored.",
|
|
548
|
+
},
|
|
417
549
|
]),
|
|
418
550
|
|
|
419
551
|
armorgear: Object.freeze([
|
|
@@ -817,6 +949,77 @@ export const ITEM_FIELDS = Object.freeze({
|
|
|
817
949
|
default: null,
|
|
818
950
|
describe: "Shortcode of the body location injured. Unset on a descriptive condition.",
|
|
819
951
|
},
|
|
952
|
+
// A trauma's three timed phases (#329). Its healing check and course
|
|
953
|
+
// both fall back to a **world setting** when the trauma sets neither
|
|
954
|
+
// half, which is the sharper reason these omit rather than default: a
|
|
955
|
+
// compile-time `null` written here is still a value, and the seeding in
|
|
956
|
+
// `TraumaDataModel._preCreate` reads `data.courseDurationFormula == null`
|
|
957
|
+
// to decide whether the GM's configured interval applies. A default
|
|
958
|
+
// would answer that question for every trauma in every world.
|
|
959
|
+
//
|
|
960
|
+
// `bloodLossAdvance*` is the one an author reaches for most: a trauma
|
|
961
|
+
// that sets it bleeds, and one that leaves it unset does not.
|
|
962
|
+
{
|
|
963
|
+
name: "healingCheckDurationFormula",
|
|
964
|
+
to: "healingCheckDurationFormula",
|
|
965
|
+
...DURATION_FORMULA,
|
|
966
|
+
omitWhenAbsent: true,
|
|
967
|
+
describe:
|
|
968
|
+
"Interval between healing checks. Omitted when unset, leaving the world's configured interval to apply.",
|
|
969
|
+
},
|
|
970
|
+
{
|
|
971
|
+
name: "healingCheckDurationBase",
|
|
972
|
+
to: "healingCheckDurationBase",
|
|
973
|
+
...DURATION_BASE,
|
|
974
|
+
omitWhenAbsent: true,
|
|
975
|
+
describe:
|
|
976
|
+
"The healing-check interval in seconds, standing in for a roll of the formula. Omitted when unset.",
|
|
977
|
+
},
|
|
978
|
+
{
|
|
979
|
+
name: "bloodLossAdvanceDurationFormula",
|
|
980
|
+
to: "bloodLossAdvanceDurationFormula",
|
|
981
|
+
...DURATION_FORMULA,
|
|
982
|
+
omitWhenAbsent: true,
|
|
983
|
+
describe:
|
|
984
|
+
"Interval between blood-loss advances. Omitted when unset, leaving the world's configured interval to apply.",
|
|
985
|
+
},
|
|
986
|
+
{
|
|
987
|
+
name: "bloodLossAdvanceDurationBase",
|
|
988
|
+
to: "bloodLossAdvanceDurationBase",
|
|
989
|
+
...DURATION_BASE,
|
|
990
|
+
omitWhenAbsent: true,
|
|
991
|
+
describe:
|
|
992
|
+
"The blood-loss interval in seconds. Setting it is what makes the wound bleed; omitted when unset, and the wound does not.",
|
|
993
|
+
},
|
|
994
|
+
{
|
|
995
|
+
name: "courseDurationFormula",
|
|
996
|
+
to: "courseDurationFormula",
|
|
997
|
+
...DURATION_FORMULA,
|
|
998
|
+
omitWhenAbsent: true,
|
|
999
|
+
describe:
|
|
1000
|
+
"Interval between course tests, for a condition that runs one — shock, coma, infection. Omitted when unset.",
|
|
1001
|
+
},
|
|
1002
|
+
{
|
|
1003
|
+
name: "courseDurationBase",
|
|
1004
|
+
to: "courseDurationBase",
|
|
1005
|
+
...DURATION_BASE,
|
|
1006
|
+
omitWhenAbsent: true,
|
|
1007
|
+
describe:
|
|
1008
|
+
"The course-test interval in seconds, standing in for a roll of the formula. Omitted when unset.",
|
|
1009
|
+
},
|
|
1010
|
+
// Runtime state, for the reason `affliction`'s four are (#330): a
|
|
1011
|
+
// trauma's dates are crystallized when the phase fires, and world time
|
|
1012
|
+
// does not exist while content is compiled.
|
|
1013
|
+
{
|
|
1014
|
+
to: "contractDate",
|
|
1015
|
+
runtimeOnly: "the world time the injury was taken",
|
|
1016
|
+
describe: "When the injury was taken. Play state, never authored.",
|
|
1017
|
+
},
|
|
1018
|
+
{
|
|
1019
|
+
to: "treatmentDate",
|
|
1020
|
+
runtimeOnly: "the world time the injury was last treated",
|
|
1021
|
+
describe: "When it was last treated. Play state, never authored.",
|
|
1022
|
+
},
|
|
820
1023
|
]),
|
|
821
1024
|
|
|
822
1025
|
weapongear: Object.freeze([
|
package/sohl/note-schemas.mjs
CHANGED
|
@@ -148,8 +148,8 @@ const BEING_FIELDS = Object.freeze([
|
|
|
148
148
|
*
|
|
149
149
|
* `img` is the one required field — the compiler refuses a map note without it,
|
|
150
150
|
* since a scene with no background is not a map. It was spelled `image` and
|
|
151
|
-
* read from the `sohl:` block until #142
|
|
152
|
-
*
|
|
151
|
+
* read from the `sohl:` block until #142, which read both while the trees were
|
|
152
|
+
* swept; that retirement has since completed and `image` is gone (#149).
|
|
153
153
|
*
|
|
154
154
|
* @type {readonly import("../engine/field-spec.mjs").FieldSpec[]}
|
|
155
155
|
*/
|
|
@@ -161,7 +161,10 @@ const MAP_FIELDS = Object.freeze([
|
|
|
161
161
|
// Art is not system-specific — a Scene is a core Foundry document, and
|
|
162
162
|
// every other note type carries its `img` at the note's top level.
|
|
163
163
|
shared: true,
|
|
164
|
-
describe:
|
|
164
|
+
describe:
|
|
165
|
+
"The scene's background image. Owned by whichever package its first segment " +
|
|
166
|
+
"names — `systems/…` and `modules/…` are emitted unchanged, anything else is " +
|
|
167
|
+
"this package's own and is rooted under its assets.",
|
|
165
168
|
},
|
|
166
169
|
{
|
|
167
170
|
name: "dimensions",
|
package/types/config.d.mts
CHANGED
|
@@ -55,6 +55,8 @@
|
|
|
55
55
|
* @property {string} e2eStage Which stage the suite runs against.
|
|
56
56
|
* @property {Readonly<{run: readonly string[], open: readonly string[]|null}>|null} e2eSuite
|
|
57
57
|
* What to run against the served world; `null` when the repository has none.
|
|
58
|
+
* @property {readonly string[]} e2eResults Where the suite writes its results,
|
|
59
|
+
* so a run that wrote none is not mistaken for one that passed.
|
|
58
60
|
* @property {Readonly<Record<string, Readonly<{script: string, recreate: boolean}>>>} e2eBuild
|
|
59
61
|
* Build targets the fast loop can produce, in declaration order.
|
|
60
62
|
* @property {Readonly<Record<string, string>>} e2eWorld Declared world identity.
|
|
@@ -260,6 +262,11 @@ export type PackageBuildConfig = {
|
|
|
260
262
|
run: readonly string[];
|
|
261
263
|
open: readonly string[] | null;
|
|
262
264
|
}> | null;
|
|
265
|
+
/**
|
|
266
|
+
* Where the suite writes its results,
|
|
267
|
+
* so a run that wrote none is not mistaken for one that passed.
|
|
268
|
+
*/
|
|
269
|
+
e2eResults: readonly string[];
|
|
263
270
|
/**
|
|
264
271
|
* Build targets the fast loop can produce, in declaration order.
|
|
265
272
|
*/
|
package/types/e2e.d.mts
CHANGED
|
@@ -188,6 +188,106 @@ export function waitForWorld({ url, container, stage, timeoutMs, log }: {
|
|
|
188
188
|
timeoutMs?: number | undefined;
|
|
189
189
|
log?: ((message: string) => void) | undefined;
|
|
190
190
|
}): Promise<void>;
|
|
191
|
+
/**
|
|
192
|
+
* Every executable that must exist for a suite command to run at all.
|
|
193
|
+
*
|
|
194
|
+
* One name for a plain command, two when a package runner is standing in for a
|
|
195
|
+
* tool. This is deliberately a *reading* of the command rather than a guess:
|
|
196
|
+
* anything it cannot read reduces to the program alone, because naming the
|
|
197
|
+
* wrong missing thing would send someone after a dependency they already have.
|
|
198
|
+
*
|
|
199
|
+
* @param {readonly string[]} command - The program and its arguments.
|
|
200
|
+
* @returns {string[]} The executables to resolve, in the order to report them.
|
|
201
|
+
*/
|
|
202
|
+
export function suiteExecutables(command: readonly string[]): string[];
|
|
203
|
+
/**
|
|
204
|
+
* Find an executable the way the child process will: a path is a path, and a
|
|
205
|
+
* bare name is looked for in the repository's `node_modules/.bin` first, then
|
|
206
|
+
* along `PATH`.
|
|
207
|
+
*
|
|
208
|
+
* @param {string} name - The program, as the command line spells it.
|
|
209
|
+
* @param {object} [opts]
|
|
210
|
+
* @param {string} [opts.cwd] - The repository root, for `node_modules/.bin`.
|
|
211
|
+
* @param {NodeJS.ProcessEnv} [opts.env] - Environment to read `PATH` from.
|
|
212
|
+
* @returns {string|null} Where it is, or `null` if it is nowhere.
|
|
213
|
+
*/
|
|
214
|
+
export function findExecutable(name: string, { cwd, env }?: {
|
|
215
|
+
cwd?: string | undefined;
|
|
216
|
+
env?: NodeJS.ProcessEnv | undefined;
|
|
217
|
+
}): string | null;
|
|
218
|
+
/**
|
|
219
|
+
* Which of a suite command's executables are not there.
|
|
220
|
+
*
|
|
221
|
+
* Asked twice per run, and the second asking is the point: an install running
|
|
222
|
+
* alongside the suite can take the runner out from under it mid-flight, which
|
|
223
|
+
* is precisely the failure that reported itself as green (#153).
|
|
224
|
+
*
|
|
225
|
+
* @param {object} opts
|
|
226
|
+
* @param {readonly string[]} opts.command - The program and its arguments.
|
|
227
|
+
* @param {string} [opts.cwd] - The repository root.
|
|
228
|
+
* @param {NodeJS.ProcessEnv} [opts.env] - Environment to read.
|
|
229
|
+
* @returns {string[]} The names that resolve to nothing.
|
|
230
|
+
*/
|
|
231
|
+
export function missingExecutables({ command, cwd, env }: {
|
|
232
|
+
command: readonly string[];
|
|
233
|
+
cwd?: string | undefined;
|
|
234
|
+
env?: NodeJS.ProcessEnv | undefined;
|
|
235
|
+
}): string[];
|
|
236
|
+
/**
|
|
237
|
+
* Which declared result paths the suite actually wrote to during this run.
|
|
238
|
+
*
|
|
239
|
+
* Existence is not the test. A results directory left behind by the previous
|
|
240
|
+
* run exists, and reading that as evidence would make the check agree with
|
|
241
|
+
* exactly the thing it was built to catch. What counts is a file modified since
|
|
242
|
+
* the spawn.
|
|
243
|
+
*
|
|
244
|
+
* @param {object} opts
|
|
245
|
+
* @param {readonly string[]} opts.paths - Declared result paths, repo-relative.
|
|
246
|
+
* @param {number} opts.since - Milliseconds since the epoch, at spawn time.
|
|
247
|
+
* @param {string} [opts.cwd] - The repository root.
|
|
248
|
+
* @returns {string[]} The declared paths carrying something new.
|
|
249
|
+
*/
|
|
250
|
+
export function freshResults({ paths, since, cwd }: {
|
|
251
|
+
paths: readonly string[];
|
|
252
|
+
since: number;
|
|
253
|
+
cwd?: string | undefined;
|
|
254
|
+
}): string[];
|
|
255
|
+
/**
|
|
256
|
+
* What the harness reports for a finished suite.
|
|
257
|
+
*
|
|
258
|
+
* @typedef {object} SuiteVerdict
|
|
259
|
+
* @property {number} status The exit status to hand back.
|
|
260
|
+
* @property {string|null} message What to say about it, if anything.
|
|
261
|
+
*/
|
|
262
|
+
/**
|
|
263
|
+
* Decide what a finished suite is worth, given what it exited with and what it
|
|
264
|
+
* left behind.
|
|
265
|
+
*
|
|
266
|
+
* The point of the e2e suite is to be *evidence*: `compatibility.verified`
|
|
267
|
+
* moves on a green run, and a sweep exists to produce a citable result. So an
|
|
268
|
+
* exit status on its own cannot call a run green, because every way of stopping
|
|
269
|
+
* a runner before it starts — a corrupt install, a missing browser, a killed
|
|
270
|
+
* process, the concurrent `npm ci` that surfaced this — produces a run that
|
|
271
|
+
* executed nothing, and nothing is not a pass (#153).
|
|
272
|
+
*
|
|
273
|
+
* This can only ever make a verdict worse. A suite that failed keeps its own
|
|
274
|
+
* status; a suite that passed on no evidence loses the claim. Never the other
|
|
275
|
+
* way round — a harness that could *upgrade* a result would be a second way to
|
|
276
|
+
* report a green that did not happen.
|
|
277
|
+
*
|
|
278
|
+
* @param {object} opts
|
|
279
|
+
* @param {number} opts.status - What the suite process exited with.
|
|
280
|
+
* @param {readonly string[]} [opts.vanished] - Executables gone since it started.
|
|
281
|
+
* @param {readonly string[]} [opts.declared] - Result paths the repository declares.
|
|
282
|
+
* @param {readonly string[]} [opts.fresh] - Those of them it wrote to.
|
|
283
|
+
* @returns {SuiteVerdict} The status to report, and why.
|
|
284
|
+
*/
|
|
285
|
+
export function suiteVerdict({ status, vanished, declared, fresh }: {
|
|
286
|
+
status: number;
|
|
287
|
+
vanished?: readonly string[] | undefined;
|
|
288
|
+
declared?: readonly string[] | undefined;
|
|
289
|
+
fresh?: readonly string[] | undefined;
|
|
290
|
+
}): SuiteVerdict;
|
|
191
291
|
/**
|
|
192
292
|
* Run the repository's suite.
|
|
193
293
|
*
|
|
@@ -196,18 +296,32 @@ export function waitForWorld({ url, container, stage, timeoutMs, log }: {
|
|
|
196
296
|
* runner launches as plain Node, rejects its own flags, and dies with a
|
|
197
297
|
* `MODULE_NOT_FOUND` naming nothing relevant.
|
|
198
298
|
*
|
|
299
|
+
* The suite is bracketed by checks rather than trusted on its exit status,
|
|
300
|
+
* because a run that never started used to report as green (#153):
|
|
301
|
+
*
|
|
302
|
+
* - **Before.** Every executable the command needs is resolved, and a missing
|
|
303
|
+
* one is an error naming it — rather than a container stood up, a world
|
|
304
|
+
* seeded, and a failure three minutes later that names nothing.
|
|
305
|
+
* - **After.** The same question again, because the reported failure was an
|
|
306
|
+
* install pulling the runner out from under a run already in progress; and,
|
|
307
|
+
* where the repository declares where its results land, whether anything was
|
|
308
|
+
* written there while the suite ran.
|
|
309
|
+
*
|
|
199
310
|
* @param {object} opts
|
|
200
|
-
* @param {string[]} opts.command - The program and its arguments.
|
|
311
|
+
* @param {readonly string[]} opts.command - The program and its arguments.
|
|
201
312
|
* @param {string[]} [opts.args] - Extra arguments, appended verbatim.
|
|
202
313
|
* @param {string} opts.cwd - The repository root.
|
|
314
|
+
* @param {readonly string[]} [opts.results] - Declared result paths to check.
|
|
203
315
|
* @param {NodeJS.ProcessEnv} [opts.env] - Environment for the child.
|
|
204
316
|
* @param {(message: string) => void} [opts.log] - Progress reporting.
|
|
205
317
|
* @returns {number} The suite's exit status.
|
|
318
|
+
* @throws {Error} When the command names an executable that is not installed.
|
|
206
319
|
*/
|
|
207
|
-
export function runSuite({ command, args, cwd, env, log }: {
|
|
208
|
-
command: string[];
|
|
320
|
+
export function runSuite({ command, args, cwd, results, env, log, }: {
|
|
321
|
+
command: readonly string[];
|
|
209
322
|
args?: string[] | undefined;
|
|
210
323
|
cwd: string;
|
|
324
|
+
results?: readonly string[] | undefined;
|
|
211
325
|
env?: NodeJS.ProcessEnv | undefined;
|
|
212
326
|
log?: ((message: string) => void) | undefined;
|
|
213
327
|
}): number;
|
|
@@ -349,3 +463,16 @@ export type FastArgs = {
|
|
|
349
463
|
*/
|
|
350
464
|
suiteArgs: string[];
|
|
351
465
|
};
|
|
466
|
+
/**
|
|
467
|
+
* What the harness reports for a finished suite.
|
|
468
|
+
*/
|
|
469
|
+
export type SuiteVerdict = {
|
|
470
|
+
/**
|
|
471
|
+
* The exit status to hand back.
|
|
472
|
+
*/
|
|
473
|
+
status: number;
|
|
474
|
+
/**
|
|
475
|
+
* What to say about it, if anything.
|
|
476
|
+
*/
|
|
477
|
+
message: string | null;
|
|
478
|
+
};
|