@heroiclands/package-build 19.0.0 → 20.0.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 +624 -0
- package/CONTENT.md +79 -8
- package/bin/content-build.mjs +7 -1
- package/content-config.mjs +10 -1
- package/docs/content-format.md +394 -72
- 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-format.mjs +102 -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 +4 -1
- package/engine/foundry-entries.mjs +14 -0
- package/engine/frontmatter-lint.mjs +186 -27
- package/engine/frontmatter.mjs +11 -11
- package/engine/generate.mjs +67 -10
- package/engine/helpers.mjs +86 -9
- package/engine/index.mjs +3 -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-ids.mjs +25 -1
- package/engine/note-vocabulary.mjs +76 -9
- 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/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/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-format.d.mts +9 -0
- package/types/engine/field-spec.d.mts +271 -3
- package/types/engine/folder-notes.d.mts +20 -0
- package/types/engine/foundry-entries.d.mts +6 -0
- package/types/engine/frontmatter-lint.d.mts +18 -2
- package/types/engine/frontmatter.d.mts +11 -11
- package/types/engine/generate.d.mts +27 -0
- package/types/engine/helpers.d.mts +37 -9
- package/types/engine/index.d.mts +1 -0
- package/types/engine/map-notes.d.mts +2 -2
- package/types/engine/note-ids.d.mts +14 -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",
|
|
@@ -41,6 +41,54 @@ export function deepMerge(base: any, overlay: any): any;
|
|
|
41
41
|
* @returns {string} The address, `subType:shortcode`.
|
|
42
42
|
*/
|
|
43
43
|
export function itemAddress(subType: string, shortcode: string): string;
|
|
44
|
+
/**
|
|
45
|
+
* The key one predefined item is held under **for the package that publishes
|
|
46
|
+
* it** — the address a `model:` naming that package resolves through (#334).
|
|
47
|
+
*
|
|
48
|
+
* The unqualified {@link itemAddress} stays beside it, and the two answer
|
|
49
|
+
* different questions. A `model` that names no package means *this* one and
|
|
50
|
+
* takes the unqualified key, where a local definition still shadows a
|
|
51
|
+
* dependency's. A `model` that names a package takes this one, which nothing
|
|
52
|
+
* can shadow: that is the whole point of writing the package down.
|
|
53
|
+
*
|
|
54
|
+
* Not the canonical wikilink address, because this map is keyed in the
|
|
55
|
+
* **document's** vocabulary — a Foundry Item subtype — while a canonical address
|
|
56
|
+
* carries the *note* type. The two differ wherever a system maps a type to a
|
|
57
|
+
* differently-named subtype, and translating here would put the translation in
|
|
58
|
+
* two places.
|
|
59
|
+
*
|
|
60
|
+
* @param {string} pkg - The content package that publishes the item.
|
|
61
|
+
* @param {string} subType - The Foundry Item subtype.
|
|
62
|
+
* @param {string} shortcode - The item's `system.shortcode`.
|
|
63
|
+
* @returns {string} The address, `package:subType:shortcode`.
|
|
64
|
+
*/
|
|
65
|
+
export function packagedItemAddress(pkg: string, subType: string, shortcode: string): string;
|
|
66
|
+
/**
|
|
67
|
+
* The key one predefined item is held under **in the catalogue**, with the
|
|
68
|
+
* shortcode folded to lower case.
|
|
69
|
+
*
|
|
70
|
+
* A shortcode is case-sensitive and routinely mixed — `Clb`, `LtShoe`,
|
|
71
|
+
* `HsTunic` — while an **address** is not: `readQualifier` normalises what it
|
|
72
|
+
* reads, and every canonical address is lowercase. So the moment a `model:` is
|
|
73
|
+
* read as an address (#334), `weapongear-clb` has to find the document whose
|
|
74
|
+
* `system.shortcode` is `Clb`, and an exact match cannot (#346).
|
|
75
|
+
*
|
|
76
|
+
* Folding is safe because the fold is already the address: no two items in any
|
|
77
|
+
* published tree differ only by the case of their shortcode, and #340 will make
|
|
78
|
+
* that impossible rather than merely true.
|
|
79
|
+
*
|
|
80
|
+
* **This is not {@link itemAddress}, and must not become it.** That one seeds
|
|
81
|
+
* {@link embeddedItemId}, so folding there would change the `_id` of every
|
|
82
|
+
* embedded item whose identity carries a capital — a silent re-identification of
|
|
83
|
+
* documents nothing about which had changed. The catalogue is a lookup table;
|
|
84
|
+
* an id is a promise.
|
|
85
|
+
*
|
|
86
|
+
* @param {string} subType - The Foundry Item subtype.
|
|
87
|
+
* @param {string} shortcode - The item's `system.shortcode`, in any case.
|
|
88
|
+
* @param {string} [pkg] - The publishing package, for the qualified form.
|
|
89
|
+
* @returns {string} The catalogue key.
|
|
90
|
+
*/
|
|
91
|
+
export function catalogueKey(subType: string, shortcode: string, pkg?: string): string;
|
|
44
92
|
/**
|
|
45
93
|
* What identifies one embedded item on its actor.
|
|
46
94
|
*
|
|
@@ -136,6 +184,13 @@ export class SystemActorCompiler extends BasePackCompiler {
|
|
|
136
184
|
/** @type {readonly string[]} */
|
|
137
185
|
itemsSourceDirs: readonly string[];
|
|
138
186
|
foreignSourceDirs: any;
|
|
187
|
+
/**
|
|
188
|
+
* Every package a `model:` may name besides this one — the dependencies
|
|
189
|
+
* whose item catalogues were supplied (#334).
|
|
190
|
+
*
|
|
191
|
+
* @returns {Set<string>} The dependency package ids.
|
|
192
|
+
*/
|
|
193
|
+
get foreignPackages(): Set<string>;
|
|
139
194
|
/**
|
|
140
195
|
* This pass's system map, or a message naming the class that forgot it.
|
|
141
196
|
*
|
|
@@ -195,9 +250,34 @@ export class SystemActorCompiler extends BasePackCompiler {
|
|
|
195
250
|
* @returns {object|null} The embedded item, or null when it resolved to
|
|
196
251
|
* nothing — always with a finding emitted.
|
|
197
252
|
*/
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
253
|
+
/**
|
|
254
|
+
* Read an entry's `model:` — the address of the item it is a copy of.
|
|
255
|
+
*
|
|
256
|
+
* The address grammar is the wikilink one (#336), so a `model` is written at
|
|
257
|
+
* whatever length says what it means: `skill-wpnc` within this package,
|
|
258
|
+
* `sohl-sohl-skill-wpnc` to reach another. The system segment defaults from
|
|
259
|
+
* the block the entry sits in — `<system>.items` — which is what makes the
|
|
260
|
+
* short form name an *Item* here while the same string in body prose names
|
|
261
|
+
* a page.
|
|
262
|
+
*
|
|
263
|
+
* It replaced a top-level `shortcode:` that meant something different from
|
|
264
|
+
* the `system.shortcode` beside it and could not say which package a
|
|
265
|
+
* template came from (#334).
|
|
266
|
+
*
|
|
267
|
+
* @param {unknown} model - The authored value.
|
|
268
|
+
* @param {number} index - The entry's position, for the message.
|
|
269
|
+
* @param {string} ctx - Diagnostic context (the actor's label).
|
|
270
|
+
* @returns {{type: string, shortcode: string, package: string|null}|null}
|
|
271
|
+
* The parsed address, or `null` after reporting why it is not one.
|
|
272
|
+
*/
|
|
273
|
+
readModel(model: unknown, index: number, ctx: string): {
|
|
274
|
+
type: string;
|
|
275
|
+
shortcode: string;
|
|
276
|
+
package: string | null;
|
|
277
|
+
} | null;
|
|
278
|
+
resolveEmbedded(itemsMap: any, actorId: any, type: any, shortcode: any, overlay: any, indexKey: any, ctx: any, { fmKey, modelPackage }?: {
|
|
279
|
+
modelPackage?: null | undefined;
|
|
280
|
+
}): any;
|
|
201
281
|
#private;
|
|
202
282
|
}
|
|
203
283
|
import { BasePackCompiler } from "./base-compiler.mjs";
|
|
@@ -37,11 +37,29 @@ export function isAddressSegment(value: unknown): boolean;
|
|
|
37
37
|
* @module
|
|
38
38
|
*/
|
|
39
39
|
/**
|
|
40
|
-
* The shape every address segment must match: ASCII letters and
|
|
40
|
+
* The shape every address segment must match: **lowercase** ASCII letters and
|
|
41
|
+
* digits only.
|
|
41
42
|
*
|
|
42
|
-
* Case
|
|
43
|
-
*
|
|
44
|
-
*
|
|
43
|
+
* Case *was* deliberately unconstrained, on the reasoning that case has no
|
|
44
|
+
* bearing on the separator — which is true, and beside the point (#340).
|
|
45
|
+
*
|
|
46
|
+
* **Two names that differ only in case are two names nobody can tell apart.** A
|
|
47
|
+
* shortcode is how a person names a thing when writing a reference —
|
|
48
|
+
* `model: weapongear-dgr`, `[[skill-melee|…]]` — and `Dgr` beside `dgr` is a
|
|
49
|
+
* distinction you cannot say out loud and can only see by looking twice.
|
|
50
|
+
*
|
|
51
|
+
* The toolchain had already half-decided it: {@link canonicalKey} lowercases the
|
|
52
|
+
* address it builds, so a note declaring `Clb` published
|
|
53
|
+
* `sohl-sohl-weapongear-clb` and its `_id` derived from that. The authored name
|
|
54
|
+
* and its address disagreed, and everything downstream keys on the address —
|
|
55
|
+
* which left two notes differing only in case sharing one address, one `_id` and
|
|
56
|
+
* one URL, with nothing to report it. It also forced two exceptions elsewhere:
|
|
57
|
+
* #336 had to exempt the shortcode from the lowercase rule it pinned on every
|
|
58
|
+
* other segment, and #346 had to fold the shortcode's case in the item catalogue
|
|
59
|
+
* because an address is lowercased when read.
|
|
60
|
+
*
|
|
61
|
+
* One case, one spelling, no exceptions. Every tree already complies but two,
|
|
62
|
+
* and nothing in any of them collides when folded.
|
|
45
63
|
*
|
|
46
64
|
* @type {RegExp}
|
|
47
65
|
*/
|
|
@@ -99,14 +99,42 @@ export class BasePackCompiler {
|
|
|
99
99
|
* @type {boolean}
|
|
100
100
|
*/
|
|
101
101
|
static requiresSystemBlock: boolean;
|
|
102
|
+
/**
|
|
103
|
+
* The **art fields** this pass reads off a note and writes onto its
|
|
104
|
+
* document — `img`, `portrait`, whichever of them reaches the output.
|
|
105
|
+
*
|
|
106
|
+
* Empty by default, and every shipped pass states its own, for the reason
|
|
107
|
+
* {@link BasePackCompiler.readsPackOutputOf} does: the fact belongs to the
|
|
108
|
+
* class that does the writing, and a second list of it somewhere else is a
|
|
109
|
+
* list free to disagree with what is actually emitted.
|
|
110
|
+
*
|
|
111
|
+
* The reader is the frontmatter lint. `img` is a *shared top-level* field —
|
|
112
|
+
* legal on every note whatever its type, because
|
|
113
|
+
* `BLOCK_DOCUMENT_PROPERTIES` maps it onto `document.img` — so a note whose
|
|
114
|
+
* document has no such property authors it, validates, compiles, and loses
|
|
115
|
+
* the value with nothing said. That is #349: `Parrot` in `sohl-thalorna`
|
|
116
|
+
* had declared `img:` since long before the art rule existed and compiled
|
|
117
|
+
* `img: null` exactly as a note declaring nothing does. Naming the fields
|
|
118
|
+
* here is what lets the lint tell an inert key from a live one.
|
|
119
|
+
*
|
|
120
|
+
* A pass that emits art **anywhere** in its document declares it, not only
|
|
121
|
+
* one that writes a top-level `img`: the scenes pass puts the path on the
|
|
122
|
+
* scene's background rather than on a property called `img`, and the value
|
|
123
|
+
* is no less live for it. The question this answers is whether the authored
|
|
124
|
+
* path reaches the output at all.
|
|
125
|
+
*
|
|
126
|
+
* @type {readonly string[]}
|
|
127
|
+
*/
|
|
128
|
+
static emitsArt: readonly string[];
|
|
102
129
|
/**
|
|
103
130
|
* @param {object} options
|
|
104
131
|
* @param {string} options.contentBase - Root of the content tree.
|
|
105
132
|
* @param {string} options.dest - Where this pass writes its JSON.
|
|
106
133
|
* @param {readonly string[]} options.skipDirectories - Directories the walk
|
|
107
134
|
* never descends into. Required: see {@link assertStatedScope}.
|
|
108
|
-
* @param {(
|
|
109
|
-
* Resolves a `
|
|
135
|
+
* @param {(address: string|null) => string|null} [options.folderResolver] -
|
|
136
|
+
* Resolves a `packFolder` — a folder note's address — to the Foundry
|
|
137
|
+
* folder id it materialises as in this pack (#255, #257).
|
|
110
138
|
* @param {string} [options.packName] - The pack this pass writes.
|
|
111
139
|
* @param {string} [options.docType] - The Foundry document type it holds.
|
|
112
140
|
* @param {{resolve: Function}} [options.router] - The pack router. Omit it
|
|
@@ -118,7 +146,7 @@ export class BasePackCompiler {
|
|
|
118
146
|
contentBase: string;
|
|
119
147
|
dest: string;
|
|
120
148
|
skipDirectories: readonly string[];
|
|
121
|
-
folderResolver?: ((
|
|
149
|
+
folderResolver?: ((address: string | null) => string | null) | undefined;
|
|
122
150
|
packName?: string | undefined;
|
|
123
151
|
docType?: string | undefined;
|
|
124
152
|
router?: {
|
|
@@ -236,6 +264,29 @@ export class BasePackCompiler {
|
|
|
236
264
|
* the note's own file can be read.
|
|
237
265
|
*/
|
|
238
266
|
eligibleFor(fm: object): boolean;
|
|
267
|
+
/**
|
|
268
|
+
* A refusal only this pass can make, because its subject is the note's
|
|
269
|
+
* **type** (#330).
|
|
270
|
+
*
|
|
271
|
+
* The `assertNo*Field` family above it in the walk is type-agnostic by
|
|
272
|
+
* construction: it runs before `selects`, so that a note declaring a
|
|
273
|
+
* retired field is answered whichever pass would have claimed it. A rule
|
|
274
|
+
* about what a *`trauma`* may write cannot live there — it needs the type's
|
|
275
|
+
* field declaration, which only the pass that compiles the type can reach.
|
|
276
|
+
*
|
|
277
|
+
* So it is a hook, called once the note is known to be this pass's, and its
|
|
278
|
+
* throw is counted and located exactly as the family's is: the note is
|
|
279
|
+
* declined rather than skipped, and the build fails naming the line.
|
|
280
|
+
*
|
|
281
|
+
* The default refuses nothing, which is the honest position for a pass
|
|
282
|
+
* whose documents have no schema to have opinions about.
|
|
283
|
+
*
|
|
284
|
+
* @param {object} fm - The note's frontmatter.
|
|
285
|
+
* @returns {void}
|
|
286
|
+
* @throws {Error} When the note authors something its type forbids. The
|
|
287
|
+
* error may carry a `position` for the diagnostic.
|
|
288
|
+
*/
|
|
289
|
+
assertAuthorable(fm: object): void;
|
|
239
290
|
/**
|
|
240
291
|
* Whether this pass claims a note. **Required.**
|
|
241
292
|
*
|