@heroiclands/package-build 8.1.0 → 10.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 +748 -0
- package/CONTENT.md +273 -13
- package/bin/content-build.mjs +479 -123
- package/bin/package-build.mjs +27 -69
- package/bin/report.mjs +1 -2
- package/bundle.mjs +2 -10
- package/config.mjs +31 -106
- package/container.mjs +13 -57
- package/content-config.mjs +300 -188
- package/coverage.mjs +14 -55
- package/deploy.mjs +4 -13
- package/docs/content-format.md +1418 -0
- package/e2e.mjs +16 -55
- package/engine/address-charset.mjs +62 -0
- package/engine/address-diff.mjs +1 -4
- package/engine/alias-index.mjs +153 -0
- package/engine/base-compiler.mjs +203 -31
- package/engine/code-fences.mjs +4 -13
- package/engine/compendiums.mjs +13 -37
- package/engine/content-address.mjs +6 -10
- package/engine/content-format-check.mjs +570 -0
- package/engine/content-format.mjs +253 -0
- package/engine/content-links.mjs +144 -99
- package/engine/content-lint.mjs +12 -16
- package/engine/content-slug.mjs +2 -6
- package/engine/content-tables.mjs +26 -79
- package/engine/diagnostics.mjs +37 -15
- package/engine/document-subtypes.mjs +440 -0
- package/engine/field-reference.mjs +6 -20
- package/engine/field-spec.mjs +49 -45
- package/engine/foreign-catalog.mjs +7 -22
- package/engine/foreign-manifests.mjs +1 -4
- package/engine/frontmatter-lint.mjs +347 -43
- package/engine/frontmatter.mjs +3 -8
- package/engine/generate.mjs +36 -20
- package/engine/helpers.mjs +54 -81
- package/engine/homepage.mjs +4 -15
- package/engine/ids.mjs +21 -12
- package/engine/index.mjs +15 -0
- package/engine/item-registry.mjs +72 -9
- package/engine/journals.mjs +4 -14
- package/engine/kb-manifest.mjs +41 -24
- package/engine/macros.mjs +2 -10
- package/engine/manifest-emit.mjs +6 -17
- package/engine/map-notes.mjs +53 -87
- package/engine/note-claims.mjs +383 -0
- package/engine/note-package.mjs +1 -4
- package/engine/note-vocabulary.mjs +678 -0
- package/engine/pack-config.mjs +56 -60
- package/engine/pack-router.mjs +18 -8
- package/engine/prose-config.mjs +20 -4
- package/engine/prose-lint.mjs +61 -17
- package/engine/region-events.mjs +1 -3
- package/engine/retired-fields.mjs +117 -3
- package/engine/scene-levels.mjs +8 -22
- package/engine/scenes.mjs +31 -47
- package/engine/schema-check.mjs +348 -7
- package/engine/schema-extract.mjs +11 -39
- package/engine/site-build.mjs +13 -38
- package/engine/site-index.mjs +40 -35
- package/engine/system-block.mjs +513 -0
- package/engine/web-wikilinks.mjs +115 -92
- package/engine/wikilink-syntax.mjs +30 -0
- package/engine/wikilinks.mjs +147 -183
- package/index.mjs +1 -5
- package/lang.mjs +1 -3
- package/manifest.mjs +10 -37
- package/markdownlint-config.mjs +1 -5
- package/package.json +6 -2
- package/sohl/actors.mjs +251 -68
- package/sohl/being-info.mjs +3 -6
- package/sohl/document-subtypes.mjs +82 -0
- package/sohl/index.mjs +4 -6
- package/sohl/item-builders.mjs +1 -3
- package/sohl/item-fields.mjs +16 -34
- package/sohl/items.mjs +111 -17
- package/sohl/kb-passes.mjs +29 -39
- package/sohl/note-schemas.mjs +11 -7
- package/sohl/skill-base.mjs +7 -23
- package/stage.mjs +3 -13
- package/templates.mjs +4 -15
- package/types/bundle.d.mts +1 -1
- package/types/container.d.mts +2 -2
- package/types/content-config.d.mts +48 -4
- package/types/coverage.d.mts +1 -1
- package/types/e2e.d.mts +4 -4
- package/types/engine/address-charset.d.mts +45 -0
- package/types/engine/alias-index.d.mts +122 -0
- package/types/engine/base-compiler.d.mts +132 -4
- package/types/engine/content-address.d.mts +2 -2
- package/types/engine/content-format-check.d.mts +163 -0
- package/types/engine/content-format.d.mts +101 -0
- package/types/engine/content-links.d.mts +16 -1
- package/types/engine/content-lint.d.mts +6 -0
- package/types/engine/diagnostics.d.mts +29 -0
- package/types/engine/document-subtypes.d.mts +233 -0
- package/types/engine/field-spec.d.mts +76 -23
- package/types/engine/frontmatter-lint.d.mts +47 -2
- package/types/engine/generate.d.mts +15 -2
- package/types/engine/helpers.d.mts +22 -14
- package/types/engine/ids.d.mts +10 -0
- package/types/engine/index.d.mts +5 -0
- package/types/engine/item-registry.d.mts +21 -2
- package/types/engine/kb-manifest.d.mts +35 -8
- package/types/engine/map-notes.d.mts +21 -11
- package/types/engine/note-claims.d.mts +113 -0
- package/types/engine/note-vocabulary.d.mts +251 -0
- package/types/engine/pack-config.d.mts +4 -3
- package/types/engine/pack-router.d.mts +4 -4
- package/types/engine/prose-lint.d.mts +6 -2
- package/types/engine/retired-fields.d.mts +73 -2
- package/types/engine/schema-check.d.mts +182 -0
- package/types/engine/schema-extract.d.mts +1 -1
- package/types/engine/site-index.d.mts +1 -1
- package/types/engine/system-block.d.mts +281 -0
- package/types/engine/web-wikilinks.d.mts +23 -12
- package/types/engine/wikilink-syntax.d.mts +29 -0
- package/types/manifest.d.mts +1 -1
- package/types/sohl/actors.d.mts +62 -6
- package/types/sohl/document-subtypes.d.mts +14 -0
- package/types/sohl/index.d.mts +1 -0
- package/types/sohl/items.d.mts +21 -0
package/sohl/actors.mjs
CHANGED
|
@@ -15,12 +15,18 @@
|
|
|
15
15
|
* Actors pack compiler — produces JSON pack files for the "actors" Foundry
|
|
16
16
|
* compendium from markdown `being` notes in the `assets/content/` tree.
|
|
17
17
|
*
|
|
18
|
-
* One content type,
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
18
|
+
* One content type today, and the actor subtype it produces is **declared**
|
|
19
|
+
* rather than assumed to be the same word: `sohl/document-subtypes.mjs` maps
|
|
20
|
+
* `being` → `Actor` / `being`, and this pass looks it up (#79). It read
|
|
21
|
+
* `ACTOR_VAULT_TYPE = "being"` here and emitted `type: "being"` several hundred
|
|
22
|
+
* lines below, which made the two vocabularies agree by coincidence.
|
|
22
23
|
*
|
|
23
|
-
*
|
|
24
|
+
* It was two content types — `character` and `creature` — which compiled to the
|
|
25
|
+
* same `being` with no branch anywhere between them; they were retired in
|
|
26
|
+
* SoHL#1580 and are now reported by `assertTypeNotRetired` in
|
|
27
|
+
* `engine/ids.mjs`.
|
|
28
|
+
*
|
|
29
|
+
* Each actor's embedded items are resolved by looking `(type, shortcode)` up
|
|
24
30
|
* against the generated JSON tree of every Item pack (built in prior items
|
|
25
31
|
* passes and named by the caller as `itemsSourceDirs`). All of them, because a
|
|
26
32
|
* repository may group its items into several Item packs (#1566) and a being
|
|
@@ -30,6 +36,14 @@
|
|
|
30
36
|
* resolved by `(type, shortcode)` and deep-merged with the entry's other
|
|
31
37
|
* properties. `sohl.skills` is ignored.
|
|
32
38
|
*
|
|
39
|
+
* **Those references are in the note vocabulary and the addresses are in the
|
|
40
|
+
* document's**, and the difference is stated rather than assumed away (#140):
|
|
41
|
+
* {@link itemAddress} keys a predefined item by the subtype its compiled
|
|
42
|
+
* document carries, and {@link Actors#embeddedSubtype} translates each
|
|
43
|
+
* authored reference forward through the system's map before the lookup. A
|
|
44
|
+
* reference that then resolves to nothing is a finding naming the note and the
|
|
45
|
+
* reference — never an item quietly missing from the compiled actor.
|
|
46
|
+
*
|
|
33
47
|
* Not a standalone script — exports the `Actors` compiler class, imported and
|
|
34
48
|
* driven by `packages/content-build/engine/generate.mjs` (via `npm run build:compiledb`). Must run
|
|
35
49
|
* after the items pass, since it reads the items pack's generated JSON tree.
|
|
@@ -50,27 +64,81 @@ import {
|
|
|
50
64
|
resolveName,
|
|
51
65
|
resolveImg,
|
|
52
66
|
defaultStats,
|
|
53
|
-
|
|
67
|
+
systemArchetype,
|
|
54
68
|
md,
|
|
55
69
|
} from "../engine/helpers.mjs";
|
|
56
70
|
import { emitDiagnostic } from "../engine/diagnostics.mjs";
|
|
57
71
|
import { openingMasteryLevel } from "./skill-base.mjs";
|
|
58
72
|
import { BasePackCompiler } from "../engine/base-compiler.mjs";
|
|
59
73
|
import { contentPackage } from "../engine/content-package.mjs";
|
|
74
|
+
// Which Foundry Actor subtype a note's `type` compiles into, and which note
|
|
75
|
+
// types are actors at all. Looked up in the system's declared map, never
|
|
76
|
+
// inferred from the type itself (#79).
|
|
77
|
+
import {
|
|
78
|
+
documentSubtype,
|
|
79
|
+
mapsNoteType,
|
|
80
|
+
noteTypesFor,
|
|
81
|
+
referencedSubtype,
|
|
82
|
+
} from "../engine/document-subtypes.mjs";
|
|
83
|
+
import { SOHL_DOCUMENT_SUBTYPES } from "./document-subtypes.mjs";
|
|
84
|
+
import { locateFrontmatterKey } from "../engine/retired-fields.mjs";
|
|
85
|
+
// The note-level `sohl:` block: `sohl.system` onto the document's `system`
|
|
86
|
+
// verbatim, and `sohl.img` / `sohl.effects` / `sohl.flags` overriding their
|
|
87
|
+
// shared top-level forms for this system alone (#58).
|
|
88
|
+
import { blockProperty, mergeSystemData } from "../engine/system-block.mjs";
|
|
60
89
|
|
|
61
90
|
/**
|
|
62
|
-
* The
|
|
63
|
-
*
|
|
91
|
+
* The system this pass compiles for — the block its notes write.
|
|
92
|
+
*
|
|
93
|
+
* Read from the map rather than spelled here, so the block name and the subtype
|
|
94
|
+
* map are one statement (#58/#79).
|
|
95
|
+
*
|
|
96
|
+
* @type {string}
|
|
64
97
|
*/
|
|
65
|
-
const
|
|
98
|
+
const SYSTEM = SOHL_DOCUMENT_SUBTYPES.block;
|
|
66
99
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
100
|
+
/**
|
|
101
|
+
* The note types this pass claims — every one the system's map sends to an
|
|
102
|
+
* `Actor`.
|
|
103
|
+
*
|
|
104
|
+
* Read from the map rather than restated as a constant. It *was* a constant:
|
|
105
|
+
* `ACTOR_VAULT_TYPE = "being"` sat here and `type: "being"` was emitted several
|
|
106
|
+
* hundred lines below, with nothing relating them, so a change to either
|
|
107
|
+
* followed the other only by coincidence (#79).
|
|
108
|
+
*
|
|
109
|
+
* @type {readonly string[]}
|
|
110
|
+
*/
|
|
111
|
+
const ACTOR_NOTE_TYPES = Object.freeze(noteTypesFor(SOHL_DOCUMENT_SUBTYPES, "Actor"));
|
|
112
|
+
|
|
113
|
+
// Default art per actor **subtype**, applied when frontmatter supplies no `img`
|
|
114
|
+
// / `portrait`. Beings default to the generic person icon; another subtype adds
|
|
115
|
+
// its own entry here as the map gains a row for it.
|
|
70
116
|
const DEFAULT_IMG = {
|
|
71
117
|
being: "systems/sohl/assets/icons/game-icons/delapouite/person.svg",
|
|
72
118
|
};
|
|
73
119
|
|
|
120
|
+
/**
|
|
121
|
+
* The default art for an actor subtype.
|
|
122
|
+
*
|
|
123
|
+
* Fail-fast, for the reason {@link itemArt} is: a subtype with no art would
|
|
124
|
+
* otherwise ship a document with no image and nothing said about it.
|
|
125
|
+
*
|
|
126
|
+
* @param {string} subType - The Foundry actor subtype.
|
|
127
|
+
* @returns {string} The default image path.
|
|
128
|
+
* @throws {Error} When this map pairs no art with the subtype.
|
|
129
|
+
*/
|
|
130
|
+
function defaultActorImg(subType) {
|
|
131
|
+
const img = /** @type {Record<string, string|undefined>} */ (DEFAULT_IMG)[subType];
|
|
132
|
+
if (!img) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
`No default art for actor subtype "${subType}" — add an entry to ` +
|
|
135
|
+
`\`DEFAULT_IMG\` in sohl/actors.mjs, beside the map row that ` +
|
|
136
|
+
`introduced the subtype.`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
return img;
|
|
140
|
+
}
|
|
141
|
+
|
|
74
142
|
/**
|
|
75
143
|
* Strip compendium-only fields from a predefined item before embedding it
|
|
76
144
|
* inside an actor's `items[]`. These fields belong on a top-level
|
|
@@ -151,17 +219,35 @@ function extractBodyAndMovement(fm) {
|
|
|
151
219
|
return {
|
|
152
220
|
body: normalizeBody(sohlField(fm, "body", {})),
|
|
153
221
|
currentMoveMedium: String(sohlField(fm, "currentMoveMedium", "none")),
|
|
154
|
-
movementProfiles: normalizeMovementProfiles(
|
|
155
|
-
sohlField(fm, "movementProfiles", []),
|
|
156
|
-
),
|
|
222
|
+
movementProfiles: normalizeMovementProfiles(sohlField(fm, "movementProfiles", [])),
|
|
157
223
|
};
|
|
158
224
|
}
|
|
159
225
|
|
|
226
|
+
/**
|
|
227
|
+
* The key one predefined item is held under, and every place that spells it.
|
|
228
|
+
*
|
|
229
|
+
* **The vocabulary is the document's, not the note's** — `subType` is the
|
|
230
|
+
* Foundry Item subtype the compiled document carries, which is the only thing a
|
|
231
|
+
* compiled pack (or an extracted dependency catalogue) records about what an
|
|
232
|
+
* item *is*. A being's frontmatter addresses the same item in the *note*
|
|
233
|
+
* vocabulary, so a reference is translated forward through the system's map
|
|
234
|
+
* before it reaches this function; see {@link Actors#embeddedSubtype} for why
|
|
235
|
+
* the translation goes that way and not the other (#140).
|
|
236
|
+
*
|
|
237
|
+
* @param {string} subType - The Foundry Item subtype.
|
|
238
|
+
* @param {string} shortcode - The item's `system.shortcode`.
|
|
239
|
+
* @returns {string} The address, `subType:shortcode`.
|
|
240
|
+
*/
|
|
241
|
+
function itemAddress(subType, shortcode) {
|
|
242
|
+
return `${subType}:${shortcode}`;
|
|
243
|
+
}
|
|
244
|
+
|
|
160
245
|
/**
|
|
161
246
|
* Load every JSON file under each of `itemsSourceDirs`, returning one Map keyed
|
|
162
|
-
* by
|
|
163
|
-
* shortcode
|
|
164
|
-
* it is not part of the item
|
|
247
|
+
* by {@link itemAddress} — the compiled document's **subtype** and its
|
|
248
|
+
* `system.shortcode`. Folder docs and entries without a shortcode are skipped.
|
|
249
|
+
* The `_key` field is stripped from each entry — it is not part of the item
|
|
250
|
+
* data model.
|
|
165
251
|
*
|
|
166
252
|
* The directories are read as one address space, because a being names an item
|
|
167
253
|
* by `(type, shortcode)` and never by the pack it happens to ship in. Two local
|
|
@@ -215,7 +301,7 @@ function loadItemsMap(itemsSourceDirs, foreignSourceDirs = []) {
|
|
|
215
301
|
}
|
|
216
302
|
const shortcode = doc?.system?.shortcode;
|
|
217
303
|
if (!doc?.type || !shortcode) continue;
|
|
218
|
-
const address =
|
|
304
|
+
const address = itemAddress(doc.type, shortcode);
|
|
219
305
|
const owner = source.get(address);
|
|
220
306
|
if (owner && owner !== itemsSourceDir) {
|
|
221
307
|
throw new Error(
|
|
@@ -249,7 +335,7 @@ function loadItemsMap(itemsSourceDirs, foreignSourceDirs = []) {
|
|
|
249
335
|
}
|
|
250
336
|
const shortcode = doc?.system?.shortcode;
|
|
251
337
|
if (!doc?.type || !shortcode) continue;
|
|
252
|
-
const address =
|
|
338
|
+
const address = itemAddress(doc.type, shortcode);
|
|
253
339
|
if (map.has(address)) {
|
|
254
340
|
// Deliberate: this repository defines it, so its version wins.
|
|
255
341
|
if (source.has(address)) shadowed.push(address);
|
|
@@ -290,8 +376,7 @@ function extractAnchorSection(body, anchorId) {
|
|
|
290
376
|
if (capturing) captured.push(line);
|
|
291
377
|
continue;
|
|
292
378
|
}
|
|
293
|
-
const h1Match =
|
|
294
|
-
!inCodeBlock ? line.match(/^\s*#\s+(.+?)\s*#*\s*$/) : null;
|
|
379
|
+
const h1Match = !inCodeBlock ? line.match(/^\s*#\s+(.+?)\s*#*\s*$/) : null;
|
|
295
380
|
if (h1Match) {
|
|
296
381
|
const anchor = h1Match[1].match(/\{#([^}]+)\}\s*$/);
|
|
297
382
|
const id = anchor?.[1]?.trim().toLowerCase() || null;
|
|
@@ -367,14 +452,34 @@ export class Actors extends BasePackCompiler {
|
|
|
367
452
|
});
|
|
368
453
|
}
|
|
369
454
|
|
|
455
|
+
/**
|
|
456
|
+
* The note-type → document-subtype map this pass compiles against.
|
|
457
|
+
*
|
|
458
|
+
* Stated by the class rather than reached for through the module import, so
|
|
459
|
+
* every subtype decision the pass makes — the actor's own, and each
|
|
460
|
+
* embedded item reference's — reads one declaration that a subclass
|
|
461
|
+
* compiling for another system can replace. That is also what lets the
|
|
462
|
+
* non-identity behaviour be exercised without introducing a non-identity
|
|
463
|
+
* row into SoHL's own map, which is #78's job and moves compiled bytes.
|
|
464
|
+
*
|
|
465
|
+
* @type {import("../engine/document-subtypes.mjs").DocumentSubtypeMap}
|
|
466
|
+
*/
|
|
467
|
+
static documentSubtypes = SOHL_DOCUMENT_SUBTYPES;
|
|
468
|
+
|
|
370
469
|
/**
|
|
371
470
|
* @param {object} fm - The note's frontmatter.
|
|
372
|
-
* @returns {boolean} True for a `
|
|
471
|
+
* @returns {boolean} True for a note type the system maps onto an `Actor`.
|
|
373
472
|
*/
|
|
374
473
|
selects(fm) {
|
|
375
|
-
return fm.type
|
|
474
|
+
return mapsNoteType(this.constructor.documentSubtypes, fm.type, "Actor");
|
|
376
475
|
}
|
|
377
476
|
|
|
477
|
+
/**
|
|
478
|
+
* An Actor **is** a system's data, so this pack takes only notes carrying
|
|
479
|
+
* this system's block (#58).
|
|
480
|
+
*/
|
|
481
|
+
static requiresSystemBlock = true;
|
|
482
|
+
|
|
378
483
|
/**
|
|
379
484
|
* The predefined items each being's embedded items resolve against, loaded
|
|
380
485
|
* before the walk from the items passes' output.
|
|
@@ -383,13 +488,8 @@ export class Actors extends BasePackCompiler {
|
|
|
383
488
|
*/
|
|
384
489
|
async prepare() {
|
|
385
490
|
await super.prepare();
|
|
386
|
-
this.itemsMap = loadItemsMap(
|
|
387
|
-
|
|
388
|
-
this.foreignSourceDirs,
|
|
389
|
-
);
|
|
390
|
-
log.info(
|
|
391
|
-
`Loaded ${this.itemsMap.size} predefined items for actor resolution`,
|
|
392
|
-
);
|
|
491
|
+
this.itemsMap = loadItemsMap(this.itemsSourceDirs, this.foreignSourceDirs);
|
|
492
|
+
log.info(`Loaded ${this.itemsMap.size} predefined items for actor resolution`);
|
|
393
493
|
}
|
|
394
494
|
|
|
395
495
|
/**
|
|
@@ -408,53 +508,112 @@ export class Actors extends BasePackCompiler {
|
|
|
408
508
|
reportDetail(stats) {
|
|
409
509
|
log.debug(
|
|
410
510
|
`Skipped ${stats.skippedOther} non-actor file(s) ` +
|
|
411
|
-
`(not ${
|
|
511
|
+
`(not ${ACTOR_NOTE_TYPES.join("/")}, package:${contentPackage()})`,
|
|
412
512
|
);
|
|
413
513
|
}
|
|
414
514
|
|
|
515
|
+
/**
|
|
516
|
+
* The Foundry Item subtype an embedded reference's `type` addresses.
|
|
517
|
+
*
|
|
518
|
+
* **The reference is in the note vocabulary; the address is in the
|
|
519
|
+
* document's** (#140). A being writes `(type, shortcode)` with the type an
|
|
520
|
+
* author authors, while {@link itemAddress} keys the predefined items by
|
|
521
|
+
* the subtype each compiled document carries — so exactly one of the two
|
|
522
|
+
* sides has to translate, and it is this one. The system's map is a
|
|
523
|
+
* function from note type to subtype by construction; the reverse is not,
|
|
524
|
+
* and a compiled document records nothing about the note that produced it,
|
|
525
|
+
* so there is no honest way to key the addresses the other way round.
|
|
526
|
+
*
|
|
527
|
+
* The two vocabularies are the same string in every SoHL row today, which
|
|
528
|
+
* is why looking a reference up verbatim worked. The first non-identity row
|
|
529
|
+
* (#78: `armor` → `armorgear`) ends that, and a reference resolving to
|
|
530
|
+
* nothing must be a finding rather than an item quietly missing from the
|
|
531
|
+
* compiled actor.
|
|
532
|
+
*
|
|
533
|
+
* @param {string} type - The type the reference names.
|
|
534
|
+
* @returns {import("../engine/document-subtypes.mjs").ReferencedSubtype}
|
|
535
|
+
* The subtype, or why the reference names none.
|
|
536
|
+
*/
|
|
537
|
+
embeddedSubtype(type) {
|
|
538
|
+
return referencedSubtype(this.constructor.documentSubtypes, type, "Item");
|
|
539
|
+
}
|
|
540
|
+
|
|
415
541
|
/**
|
|
416
542
|
* Resolve one embedded item from a `(type, shortcode?, overlay)`
|
|
417
543
|
* descriptor. If `shortcode` is given, the predefined item is fetched
|
|
418
544
|
* from `itemsMap` and the overlay deep-merged on top. If absent, the
|
|
419
545
|
* descriptor must carry enough fields to stand alone. The embedded
|
|
420
546
|
* item's `_id` is regenerated deterministically from
|
|
421
|
-
* `(actorId,
|
|
547
|
+
* `(actorId, subType, shortcode, indexKey)` so re-exports are stable —
|
|
548
|
+
* from the **document subtype**, so that renaming a note type (#78) leaves
|
|
549
|
+
* every embedded id exactly where it was.
|
|
422
550
|
* Returns null if the descriptor cannot be resolved.
|
|
551
|
+
*
|
|
552
|
+
* @param {Map<string, object>} itemsMap - The predefined items, by address.
|
|
553
|
+
* @param {string} actorId - The owning actor's id, seeding embedded ids.
|
|
554
|
+
* @param {string} type - The **note** type the reference names.
|
|
555
|
+
* @param {string|null} shortcode - The referenced item's shortcode, or
|
|
556
|
+
* `null` for a stand-alone entry.
|
|
557
|
+
* @param {object} [overlay] - The entry's remaining properties.
|
|
558
|
+
* @param {string} indexKey - Distinguishes two references to one item.
|
|
559
|
+
* @param {string} ctx - Diagnostic context (the actor's label).
|
|
560
|
+
* @param {object} [at] - Where to locate a finding.
|
|
561
|
+
* @param {string} [at.fmKey] - The frontmatter key the reference sits
|
|
562
|
+
* under, so an unresolved one is reported at the reference rather than
|
|
563
|
+
* at the note.
|
|
564
|
+
* @returns {object|null} The embedded item, or null when it resolved to
|
|
565
|
+
* nothing — always with a finding emitted.
|
|
423
566
|
*/
|
|
424
|
-
resolveEmbedded(
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
567
|
+
resolveEmbedded(itemsMap, actorId, type, shortcode, overlay, indexKey, ctx, { fmKey } = {}) {
|
|
568
|
+
// Where a finding about this reference points. The value locates the
|
|
569
|
+
// exact entry in a list; the key is the fallback when it cannot be
|
|
570
|
+
// found, which still beats naming the note alone.
|
|
571
|
+
const where = () =>
|
|
572
|
+
locateFrontmatterKey(this.currentNote?.absPath, fmKey ?? "items", shortcode || type);
|
|
573
|
+
|
|
574
|
+
const { subType, problem } = this.embeddedSubtype(type);
|
|
575
|
+
if (problem) {
|
|
576
|
+
this.noteError(`${ctx}: ${indexKey}: ${problem}`, where());
|
|
577
|
+
this.errorCount++;
|
|
578
|
+
return null;
|
|
579
|
+
}
|
|
580
|
+
const address = itemAddress(/** @type {string} */ (subType), shortcode ?? "");
|
|
581
|
+
|
|
433
582
|
let base = null;
|
|
434
583
|
if (shortcode) {
|
|
435
|
-
base = itemsMap.get(
|
|
584
|
+
base = itemsMap.get(address);
|
|
436
585
|
if (!base) {
|
|
586
|
+
// Both vocabularies where they differ, so an author sees why an
|
|
587
|
+
// address they wrote did not land where they expected.
|
|
588
|
+
const translated =
|
|
589
|
+
subType === type ? "" : (
|
|
590
|
+
` (looked up as "${address}", the ` +
|
|
591
|
+
`${this.constructor.documentSubtypes.system} Item subtype a ` +
|
|
592
|
+
`"${type}" note compiles into)`
|
|
593
|
+
);
|
|
437
594
|
this.noteError(
|
|
438
|
-
`${ctx}: no predefined item for "${type}:${shortcode}"`,
|
|
595
|
+
`${ctx}: no predefined item for "${type}:${shortcode}"${translated}`,
|
|
596
|
+
where(),
|
|
439
597
|
);
|
|
440
598
|
this.errorCount++;
|
|
441
599
|
return null;
|
|
442
600
|
}
|
|
443
601
|
base = stripCompendiumFields(base);
|
|
444
602
|
} else if (overlay && overlay.name && overlay.system) {
|
|
445
|
-
base = { type, name: overlay.name, system: {} };
|
|
603
|
+
base = { type: subType, name: overlay.name, system: {} };
|
|
446
604
|
} else {
|
|
447
605
|
this.noteError(
|
|
448
606
|
`${ctx}: embedded item missing shortcode and not enough fields to stand alone`,
|
|
607
|
+
where(),
|
|
449
608
|
);
|
|
450
609
|
this.errorCount++;
|
|
451
610
|
return null;
|
|
452
611
|
}
|
|
453
612
|
const merged = overlay ? deepMerge(base, overlay) : base;
|
|
454
|
-
merged.type =
|
|
613
|
+
merged.type = subType;
|
|
455
614
|
merged._id = makeId(
|
|
456
615
|
actorId,
|
|
457
|
-
`${type}
|
|
616
|
+
`${itemAddress(/** @type {string} */ (subType), shortcode || merged.name)}:${indexKey}`,
|
|
458
617
|
);
|
|
459
618
|
// Foundry's pack compiler flattens the document hierarchy into LevelDB,
|
|
460
619
|
// storing each embedded document under its own `_key`. Embedded items
|
|
@@ -490,6 +649,7 @@ export class Actors extends BasePackCompiler {
|
|
|
490
649
|
overlay,
|
|
491
650
|
`attr:${shortcode}`,
|
|
492
651
|
ctx,
|
|
652
|
+
{ fmKey: "attributes" },
|
|
493
653
|
);
|
|
494
654
|
if (embedded) items.push(embedded);
|
|
495
655
|
}
|
|
@@ -499,9 +659,7 @@ export class Actors extends BasePackCompiler {
|
|
|
499
659
|
if (Array.isArray(sohlItems)) {
|
|
500
660
|
sohlItems.forEach((entry, index) => {
|
|
501
661
|
if (!entry || typeof entry !== "object") {
|
|
502
|
-
this.noteError(
|
|
503
|
-
`${ctx}: sohl.items[${index}] is not an object`,
|
|
504
|
-
);
|
|
662
|
+
this.noteError(`${ctx}: sohl.items[${index}] is not an object`);
|
|
505
663
|
this.errorCount++;
|
|
506
664
|
return;
|
|
507
665
|
}
|
|
@@ -519,6 +677,7 @@ export class Actors extends BasePackCompiler {
|
|
|
519
677
|
rest,
|
|
520
678
|
`items:${index}`,
|
|
521
679
|
ctx,
|
|
680
|
+
{ fmKey: "items" },
|
|
522
681
|
);
|
|
523
682
|
if (embedded) items.push(embedded);
|
|
524
683
|
});
|
|
@@ -556,10 +715,7 @@ export class Actors extends BasePackCompiler {
|
|
|
556
715
|
*/
|
|
557
716
|
openUnopenedSkills(items, ctx) {
|
|
558
717
|
const skills = items.filter(
|
|
559
|
-
(item) =>
|
|
560
|
-
item.type === "skill" &&
|
|
561
|
-
item.system &&
|
|
562
|
-
item.system.masteryLevelBase == null,
|
|
718
|
+
(item) => item.type === "skill" && item.system && item.system.masteryLevelBase == null,
|
|
563
719
|
);
|
|
564
720
|
if (!skills.length) return;
|
|
565
721
|
|
|
@@ -585,6 +741,14 @@ export class Actors extends BasePackCompiler {
|
|
|
585
741
|
const name = resolveName(fm);
|
|
586
742
|
const id = fm.id;
|
|
587
743
|
const ctx = `actor "${name}"`;
|
|
744
|
+
// The document's own subtype, and the art that goes with it. Both are
|
|
745
|
+
// looked up from the note's `type` rather than spelled here (#79).
|
|
746
|
+
const subType = /** @type {string} */ (
|
|
747
|
+
documentSubtype(this.constructor.documentSubtypes, fm.type, fm, {
|
|
748
|
+
absPath: this.currentNote?.absPath,
|
|
749
|
+
})
|
|
750
|
+
);
|
|
751
|
+
const defaultImg = defaultActorImg(subType);
|
|
588
752
|
|
|
589
753
|
const items = this.buildEmbeddedItems(itemsMap, id, fm, ctx);
|
|
590
754
|
|
|
@@ -593,10 +757,13 @@ export class Actors extends BasePackCompiler {
|
|
|
593
757
|
|
|
594
758
|
const system = {
|
|
595
759
|
// The frontmatter shortcode is the actor's stable `(type, shortcode)`
|
|
596
|
-
// key — and, for a
|
|
760
|
+
// key — and, for a being that is an archetype, its archetype
|
|
597
761
|
// identity (the dedup/override key of the Create-dialog picker, #604).
|
|
598
762
|
shortcode: fm.shortcode || "",
|
|
599
|
-
|
|
763
|
+
// Required nullable number: a priority, or `null` for a being that
|
|
764
|
+
// is not an archetype (#126 / archetype contract #604).
|
|
765
|
+
archetype: systemArchetype(fm, ctx),
|
|
766
|
+
portrait: resolveImg(blockProperty(fm, SYSTEM, "portrait")) || defaultImg,
|
|
600
767
|
appearance: renderSection(body || "", "appearance"),
|
|
601
768
|
dossier: renderSection(body || "", "dossier"),
|
|
602
769
|
};
|
|
@@ -621,19 +788,34 @@ export class Actors extends BasePackCompiler {
|
|
|
621
788
|
}
|
|
622
789
|
|
|
623
790
|
// Being-only combat grouping (mirrors `system.defaultCombatGroup`).
|
|
624
|
-
const defaultCombatGroup = sohlField(
|
|
625
|
-
fm,
|
|
626
|
-
"defaultCombatGroup",
|
|
627
|
-
undefined,
|
|
628
|
-
);
|
|
791
|
+
const defaultCombatGroup = sohlField(fm, "defaultCombatGroup", undefined);
|
|
629
792
|
if (defaultCombatGroup !== undefined) {
|
|
630
793
|
system.defaultCombatGroup = defaultCombatGroup;
|
|
631
794
|
}
|
|
632
795
|
|
|
796
|
+
// Whatever the note authors under `sohl.system`, at the DataModel's own
|
|
797
|
+
// paths (#58). This pass has no field declaration, so it claims
|
|
798
|
+
// nothing: every authored path is the author's, and the fields above
|
|
799
|
+
// are what a note that authors none still gets.
|
|
800
|
+
mergeSystemData(system, fm, { block: SYSTEM });
|
|
801
|
+
this.reportUndeclaredSystemData(fm, SYSTEM, "Actor", subType);
|
|
802
|
+
// And what this pass wrote itself. There is no field declaration for a
|
|
803
|
+
// being at all, so *every* key here is a compiler emission — including
|
|
804
|
+
// `archetype` (#126), which nothing compared until #155.
|
|
805
|
+
this.reportEmittedSystemData(system, {
|
|
806
|
+
fm,
|
|
807
|
+
block: SYSTEM,
|
|
808
|
+
documentType: "Actor",
|
|
809
|
+
subType,
|
|
810
|
+
type: fm.type,
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
const effects = blockProperty(fm, SYSTEM, "effects");
|
|
814
|
+
|
|
633
815
|
return {
|
|
634
816
|
name,
|
|
635
|
-
type:
|
|
636
|
-
img: resolveImg(fm
|
|
817
|
+
type: subType,
|
|
818
|
+
img: resolveImg(blockProperty(fm, SYSTEM, "img")) || defaultImg,
|
|
637
819
|
_id: id,
|
|
638
820
|
system,
|
|
639
821
|
items,
|
|
@@ -641,19 +823,20 @@ export class Actors extends BasePackCompiler {
|
|
|
641
823
|
name,
|
|
642
824
|
displayName: 0,
|
|
643
825
|
actorLink: false,
|
|
644
|
-
texture: { src: resolveImg(fm
|
|
826
|
+
texture: { src: resolveImg(blockProperty(fm, SYSTEM, "img")) || defaultImg },
|
|
645
827
|
width: 1,
|
|
646
828
|
height: 1,
|
|
647
829
|
sight: { enabled: false },
|
|
648
830
|
detectionModes: [],
|
|
649
831
|
},
|
|
650
|
-
effects: [],
|
|
832
|
+
effects: Array.isArray(effects) ? [...effects] : [],
|
|
651
833
|
folder,
|
|
652
834
|
sort: 0,
|
|
653
835
|
ownership: { default: 0 },
|
|
654
|
-
// `
|
|
655
|
-
// `flags.sohl.docArchetype
|
|
656
|
-
|
|
836
|
+
// Whatever the note authors, and nothing else. `archetype` used to
|
|
837
|
+
// be spliced in here as `flags.sohl.docArchetype`; it is a schema
|
|
838
|
+
// field now and sits in `system` (#126).
|
|
839
|
+
flags: blockProperty(fm, SYSTEM, "flags", {}),
|
|
657
840
|
_stats: this.stats,
|
|
658
841
|
_key: `!actors!${id}`,
|
|
659
842
|
};
|
package/sohl/being-info.mjs
CHANGED
|
@@ -108,8 +108,7 @@ export function deriveBeingInfo(sohl, index) {
|
|
|
108
108
|
const items = Array.isArray(out.items) ? out.items : [];
|
|
109
109
|
if (items.length === 0) return out;
|
|
110
110
|
|
|
111
|
-
const lookup = (type, shortcode) =>
|
|
112
|
-
shortcode ? index.get(`${type}:${shortcode}`) : undefined;
|
|
111
|
+
const lookup = (type, shortcode) => (shortcode ? index.get(`${type}:${shortcode}`) : undefined);
|
|
113
112
|
|
|
114
113
|
/** An item's display name: its own, then the index's, then its shortcode. */
|
|
115
114
|
const displayName = (it, ref, shortcode) =>
|
|
@@ -135,8 +134,7 @@ export function deriveBeingInfo(sohl, index) {
|
|
|
135
134
|
if (!isMap(it)) continue;
|
|
136
135
|
const key = GEAR_TYPE_TO_KEY[it.type];
|
|
137
136
|
if (!key) continue;
|
|
138
|
-
const shortcode =
|
|
139
|
-
typeof it.shortcode === "string" ? it.shortcode : undefined;
|
|
137
|
+
const shortcode = typeof it.shortcode === "string" ? it.shortcode : undefined;
|
|
140
138
|
const ref = lookup(it.type, shortcode);
|
|
141
139
|
const name = displayName(it, ref, shortcode);
|
|
142
140
|
if (!name) continue;
|
|
@@ -153,8 +151,7 @@ export function deriveBeingInfo(sohl, index) {
|
|
|
153
151
|
const talents = [];
|
|
154
152
|
for (const it of items) {
|
|
155
153
|
if (!isMap(it) || it.type !== "mysticalability") continue;
|
|
156
|
-
const shortcode =
|
|
157
|
-
typeof it.shortcode === "string" ? it.shortcode : undefined;
|
|
154
|
+
const shortcode = typeof it.shortcode === "string" ? it.shortcode : undefined;
|
|
158
155
|
const ref = lookup("mysticalability", shortcode);
|
|
159
156
|
// No shortcode fallback here: an ability with neither an inline name
|
|
160
157
|
// nor an index entry has nothing to show, and a row reading like a
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
|
|
3
|
+
* Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
|
|
4
|
+
*
|
|
5
|
+
* This work is licensed under the GNU General Public License v3.0 (GPLv3).
|
|
6
|
+
* You may copy, modify, and distribute it under the terms of that license.
|
|
7
|
+
*
|
|
8
|
+
* For full terms, see the LICENSE.md file in the project root or visit:
|
|
9
|
+
* https://www.gnu.org/licenses/gpl-3.0.html
|
|
10
|
+
*
|
|
11
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* **SoHL's note-type → document-subtype map** — which Foundry document, and
|
|
16
|
+
* which subtype of it, a note of each content type compiles into (#79).
|
|
17
|
+
*
|
|
18
|
+
* The mechanism is `engine/document-subtypes.mjs`; what a *game system* does
|
|
19
|
+
* with it is here, which is the `engine/` ÷ `sohl/` line this package draws
|
|
20
|
+
* everywhere else (#36). A second system declares its own map in its own half
|
|
21
|
+
* and shares nothing but the mechanism.
|
|
22
|
+
*
|
|
23
|
+
* **Every row is written out, identity rows included.** `skill → skill` looks
|
|
24
|
+
* like a row that could be derived from the item registry's keys, and deriving
|
|
25
|
+
* it is precisely the defect this map exists to remove: the note vocabulary
|
|
26
|
+
* and the document vocabulary would once again be the same identifier because
|
|
27
|
+
* one list generated the other, and a rename on either side would follow
|
|
28
|
+
* silently. Sixteen lines of data are the price of the two vocabularies being
|
|
29
|
+
* separately stated.
|
|
30
|
+
*
|
|
31
|
+
* **Today every row is the identity, and that is a fact about SoHL rather than
|
|
32
|
+
* a rule.** The renames the format calls for (`armorgear` → `armor`, and its
|
|
33
|
+
* three siblings) are #78, deliberately deferred: they cost 30,741 embedded
|
|
34
|
+
* `(type, shortcode)` references across four content repositories, and until
|
|
35
|
+
* they land the compiled packs must not move by a byte. When one does land it
|
|
36
|
+
* changes one row here and the notes that address it — a data change, not a
|
|
37
|
+
* mechanism change, which is the whole point of having the map first.
|
|
38
|
+
*
|
|
39
|
+
* **What this map is not.** It says which document a note becomes, never what
|
|
40
|
+
* that document contains: the `system` block comes from the item registry's
|
|
41
|
+
* builders (`item-builders.mjs`), and an item's own `system.subType` field —
|
|
42
|
+
* a skill's `physical` / `combattechnique`, an affliction's kind — is a
|
|
43
|
+
* *data-model field*, unrelated to the document subtype named here.
|
|
44
|
+
*
|
|
45
|
+
* @module
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
import { defineDocumentSubtypes } from "../engine/document-subtypes.mjs";
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Every content type SoHL compiles into a Foundry document, and what it
|
|
52
|
+
* becomes.
|
|
53
|
+
*
|
|
54
|
+
* The Item rows are the thirteen types the item registry declares; the one
|
|
55
|
+
* Actor row is `being`, which was two types (`character` and `creature`)
|
|
56
|
+
* compiling to the same actor until they were retired in SoHL#1580. Types this
|
|
57
|
+
* map does not name — `doc`, `macro`, the three map types — compile into
|
|
58
|
+
* documents that carry no system subtype at all, so they have no row and never
|
|
59
|
+
* needed one.
|
|
60
|
+
*
|
|
61
|
+
* @type {import("../engine/document-subtypes.mjs").DocumentSubtypeMap}
|
|
62
|
+
*/
|
|
63
|
+
export const SOHL_DOCUMENT_SUBTYPES = defineDocumentSubtypes({
|
|
64
|
+
system: "sohl",
|
|
65
|
+
types: {
|
|
66
|
+
affiliation: { document: "Item", subType: "affiliation" },
|
|
67
|
+
affliction: { document: "Item", subType: "affliction" },
|
|
68
|
+
armorgear: { document: "Item", subType: "armorgear" },
|
|
69
|
+
attribute: { document: "Item", subType: "attribute" },
|
|
70
|
+
concoctiongear: { document: "Item", subType: "concoctiongear" },
|
|
71
|
+
containergear: { document: "Item", subType: "containergear" },
|
|
72
|
+
miscgear: { document: "Item", subType: "miscgear" },
|
|
73
|
+
mystery: { document: "Item", subType: "mystery" },
|
|
74
|
+
mysticalability: { document: "Item", subType: "mysticalability" },
|
|
75
|
+
projectilegear: { document: "Item", subType: "projectilegear" },
|
|
76
|
+
skill: { document: "Item", subType: "skill" },
|
|
77
|
+
trauma: { document: "Item", subType: "trauma" },
|
|
78
|
+
weapongear: { document: "Item", subType: "weapongear" },
|
|
79
|
+
|
|
80
|
+
being: { document: "Actor", subType: "being" },
|
|
81
|
+
},
|
|
82
|
+
});
|
package/sohl/index.mjs
CHANGED
|
@@ -33,6 +33,9 @@
|
|
|
33
33
|
/** The item-type registry: every type that compiles into an Item, and its builder. */
|
|
34
34
|
export * as itemBuilders from "./item-builders.mjs";
|
|
35
35
|
|
|
36
|
+
/** This system's note-type → document-subtype map, identity rows included (#79). */
|
|
37
|
+
export * as documentSubtypes from "./document-subtypes.mjs";
|
|
38
|
+
|
|
36
39
|
/** The Item compiler. */
|
|
37
40
|
export * as items from "./items.mjs";
|
|
38
41
|
|
|
@@ -47,9 +50,4 @@ export * as kbPasses from "./kb-passes.mjs";
|
|
|
47
50
|
// compilers arrived (#1510).
|
|
48
51
|
export { DEFAULT_ITEM_ART, defaultItemArt } from "./default-item-art.mjs";
|
|
49
52
|
export { AFFILIATION_STANDINGS } from "./affiliation-standings.mjs";
|
|
50
|
-
export {
|
|
51
|
-
BEING_TYPE,
|
|
52
|
-
GEAR_TYPE_TO_KEY,
|
|
53
|
-
deriveBeingInfo,
|
|
54
|
-
isBeing,
|
|
55
|
-
} from "./being-info.mjs";
|
|
53
|
+
export { BEING_TYPE, GEAR_TYPE_TO_KEY, deriveBeingInfo, isBeing } from "./being-info.mjs";
|
package/sohl/item-builders.mjs
CHANGED
|
@@ -108,7 +108,5 @@ function entryFor(type) {
|
|
|
108
108
|
* @type {Readonly<Record<string, Readonly<{system: (fm: object) => object, img: string, fields: readonly object[]}>>>}
|
|
109
109
|
*/
|
|
110
110
|
export const ITEM_BUILDERS = Object.freeze(
|
|
111
|
-
Object.fromEntries(
|
|
112
|
-
Object.keys(ITEM_FIELDS).map((type) => [type, entryFor(type)]),
|
|
113
|
-
),
|
|
111
|
+
Object.fromEntries(Object.keys(ITEM_FIELDS).map((type) => [type, entryFor(type)])),
|
|
114
112
|
);
|