@heroiclands/package-build 9.0.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.
Files changed (78) hide show
  1. package/CHANGELOG.md +694 -0
  2. package/CONTENT.md +273 -13
  3. package/bin/content-build.mjs +437 -7
  4. package/content-config.mjs +259 -28
  5. package/docs/content-format.md +1418 -0
  6. package/engine/address-charset.mjs +62 -0
  7. package/engine/alias-index.mjs +153 -0
  8. package/engine/base-compiler.mjs +194 -4
  9. package/engine/content-address.mjs +4 -4
  10. package/engine/content-format-check.mjs +570 -0
  11. package/engine/content-format.mjs +253 -0
  12. package/engine/content-links.mjs +132 -56
  13. package/engine/content-lint.mjs +8 -1
  14. package/engine/diagnostics.mjs +33 -0
  15. package/engine/document-subtypes.mjs +440 -0
  16. package/engine/field-spec.mjs +49 -43
  17. package/engine/frontmatter-lint.mjs +343 -27
  18. package/engine/generate.mjs +32 -4
  19. package/engine/helpers.mjs +41 -29
  20. package/engine/ids.mjs +19 -1
  21. package/engine/index.mjs +15 -0
  22. package/engine/item-registry.mjs +72 -5
  23. package/engine/kb-manifest.mjs +36 -7
  24. package/engine/map-notes.mjs +34 -18
  25. package/engine/note-claims.mjs +383 -0
  26. package/engine/note-vocabulary.mjs +678 -0
  27. package/engine/pack-config.mjs +39 -22
  28. package/engine/pack-router.mjs +17 -6
  29. package/engine/prose-lint.mjs +55 -3
  30. package/engine/retired-fields.mjs +117 -3
  31. package/engine/scenes.mjs +19 -1
  32. package/engine/schema-check.mjs +347 -3
  33. package/engine/site-build.mjs +1 -1
  34. package/engine/site-index.mjs +38 -21
  35. package/engine/system-block.mjs +513 -0
  36. package/engine/web-wikilinks.mjs +112 -80
  37. package/engine/wikilink-syntax.mjs +30 -0
  38. package/engine/wikilinks.mjs +67 -51
  39. package/package.json +6 -2
  40. package/sohl/actors.mjs +249 -36
  41. package/sohl/document-subtypes.mjs +82 -0
  42. package/sohl/index.mjs +3 -0
  43. package/sohl/items.mjs +110 -14
  44. package/sohl/note-schemas.mjs +11 -7
  45. package/types/content-config.d.mts +48 -4
  46. package/types/engine/address-charset.d.mts +45 -0
  47. package/types/engine/alias-index.d.mts +122 -0
  48. package/types/engine/base-compiler.d.mts +132 -4
  49. package/types/engine/content-address.d.mts +2 -2
  50. package/types/engine/content-format-check.d.mts +163 -0
  51. package/types/engine/content-format.d.mts +101 -0
  52. package/types/engine/content-links.d.mts +16 -1
  53. package/types/engine/content-lint.d.mts +6 -0
  54. package/types/engine/diagnostics.d.mts +29 -0
  55. package/types/engine/document-subtypes.d.mts +233 -0
  56. package/types/engine/field-spec.d.mts +76 -23
  57. package/types/engine/frontmatter-lint.d.mts +47 -2
  58. package/types/engine/generate.d.mts +14 -1
  59. package/types/engine/helpers.d.mts +21 -13
  60. package/types/engine/ids.d.mts +10 -0
  61. package/types/engine/index.d.mts +5 -0
  62. package/types/engine/item-registry.d.mts +21 -2
  63. package/types/engine/kb-manifest.d.mts +35 -8
  64. package/types/engine/map-notes.d.mts +21 -11
  65. package/types/engine/note-claims.d.mts +113 -0
  66. package/types/engine/note-vocabulary.d.mts +251 -0
  67. package/types/engine/pack-config.d.mts +4 -3
  68. package/types/engine/pack-router.d.mts +4 -4
  69. package/types/engine/prose-lint.d.mts +6 -2
  70. package/types/engine/retired-fields.d.mts +73 -2
  71. package/types/engine/schema-check.d.mts +182 -0
  72. package/types/engine/system-block.d.mts +281 -0
  73. package/types/engine/web-wikilinks.d.mts +23 -12
  74. package/types/engine/wikilink-syntax.d.mts +29 -0
  75. package/types/sohl/actors.d.mts +62 -6
  76. package/types/sohl/document-subtypes.d.mts +14 -0
  77. package/types/sohl/index.d.mts +1 -0
  78. 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, named for the Foundry actor it produces. It was two —
19
- * `character` and `creature` which compiled to the same `being` with no
20
- * branch anywhere between them; they were retired in SoHL#1580 and are now
21
- * reported by `assertTypeNotRetired` in `engine/ids.mjs`.
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
- * Each actor's embedded items are resolved by looking up `<type>:<shortcode>`
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
- withArchetypeFlag,
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 content type this pass claims. A note's `type` names the Foundry document
63
- * it compiles into, exactly as every other content type does.
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 ACTOR_VAULT_TYPE = "being";
98
+ const SYSTEM = SOHL_DOCUMENT_SUBTYPES.block;
66
99
 
67
- // Default art per actor type, applied when frontmatter supplies no `img` /
68
- // `portrait`. Beings default to the generic person icon; other actor types add
69
- // their own default here as they gain a builder.
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
@@ -155,11 +223,31 @@ function extractBodyAndMovement(fm) {
155
223
  };
156
224
  }
157
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
+
158
245
  /**
159
246
  * Load every JSON file under each of `itemsSourceDirs`, returning one Map keyed
160
- * by `${type}:${system.shortcode}`. Folder docs and entries without a
161
- * shortcode are skipped. The `_key` field is stripped from each entry —
162
- * it is not part of the item data model.
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.
163
251
  *
164
252
  * The directories are read as one address space, because a being names an item
165
253
  * by `(type, shortcode)` and never by the pack it happens to ship in. Two local
@@ -213,7 +301,7 @@ function loadItemsMap(itemsSourceDirs, foreignSourceDirs = []) {
213
301
  }
214
302
  const shortcode = doc?.system?.shortcode;
215
303
  if (!doc?.type || !shortcode) continue;
216
- const address = `${doc.type}:${shortcode}`;
304
+ const address = itemAddress(doc.type, shortcode);
217
305
  const owner = source.get(address);
218
306
  if (owner && owner !== itemsSourceDir) {
219
307
  throw new Error(
@@ -247,7 +335,7 @@ function loadItemsMap(itemsSourceDirs, foreignSourceDirs = []) {
247
335
  }
248
336
  const shortcode = doc?.system?.shortcode;
249
337
  if (!doc?.type || !shortcode) continue;
250
- const address = `${doc.type}:${shortcode}`;
338
+ const address = itemAddress(doc.type, shortcode);
251
339
  if (map.has(address)) {
252
340
  // Deliberate: this repository defines it, so its version wins.
253
341
  if (source.has(address)) shadowed.push(address);
@@ -364,14 +452,34 @@ export class Actors extends BasePackCompiler {
364
452
  });
365
453
  }
366
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
+
367
469
  /**
368
470
  * @param {object} fm - The note's frontmatter.
369
- * @returns {boolean} True for a `being` note.
471
+ * @returns {boolean} True for a note type the system maps onto an `Actor`.
370
472
  */
371
473
  selects(fm) {
372
- return fm.type === ACTOR_VAULT_TYPE;
474
+ return mapsNoteType(this.constructor.documentSubtypes, fm.type, "Actor");
373
475
  }
374
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
+
375
483
  /**
376
484
  * The predefined items each being's embedded items resolve against, loaded
377
485
  * before the walk from the items passes' output.
@@ -400,41 +508,113 @@ export class Actors extends BasePackCompiler {
400
508
  reportDetail(stats) {
401
509
  log.debug(
402
510
  `Skipped ${stats.skippedOther} non-actor file(s) ` +
403
- `(not ${ACTOR_VAULT_TYPE}, package:${contentPackage()})`,
511
+ `(not ${ACTOR_NOTE_TYPES.join("/")}, package:${contentPackage()})`,
404
512
  );
405
513
  }
406
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
+
407
541
  /**
408
542
  * Resolve one embedded item from a `(type, shortcode?, overlay)`
409
543
  * descriptor. If `shortcode` is given, the predefined item is fetched
410
544
  * from `itemsMap` and the overlay deep-merged on top. If absent, the
411
545
  * descriptor must carry enough fields to stand alone. The embedded
412
546
  * item's `_id` is regenerated deterministically from
413
- * `(actorId, type, shortcode, indexKey)` so re-exports are stable.
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.
414
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.
415
566
  */
416
- resolveEmbedded(itemsMap, actorId, type, shortcode, overlay, indexKey, ctx) {
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
+
417
582
  let base = null;
418
583
  if (shortcode) {
419
- base = itemsMap.get(`${type}:${shortcode}`);
584
+ base = itemsMap.get(address);
420
585
  if (!base) {
421
- this.noteError(`${ctx}: no predefined item for "${type}:${shortcode}"`);
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
+ );
594
+ this.noteError(
595
+ `${ctx}: no predefined item for "${type}:${shortcode}"${translated}`,
596
+ where(),
597
+ );
422
598
  this.errorCount++;
423
599
  return null;
424
600
  }
425
601
  base = stripCompendiumFields(base);
426
602
  } else if (overlay && overlay.name && overlay.system) {
427
- base = { type, name: overlay.name, system: {} };
603
+ base = { type: subType, name: overlay.name, system: {} };
428
604
  } else {
429
605
  this.noteError(
430
606
  `${ctx}: embedded item missing shortcode and not enough fields to stand alone`,
607
+ where(),
431
608
  );
432
609
  this.errorCount++;
433
610
  return null;
434
611
  }
435
612
  const merged = overlay ? deepMerge(base, overlay) : base;
436
- merged.type = type;
437
- merged._id = makeId(actorId, `${type}:${shortcode || merged.name}:${indexKey}`);
613
+ merged.type = subType;
614
+ merged._id = makeId(
615
+ actorId,
616
+ `${itemAddress(/** @type {string} */ (subType), shortcode || merged.name)}:${indexKey}`,
617
+ );
438
618
  // Foundry's pack compiler flattens the document hierarchy into LevelDB,
439
619
  // storing each embedded document under its own `_key`. Embedded items
440
620
  // therefore need a hierarchical key, as do any effects they carry
@@ -469,6 +649,7 @@ export class Actors extends BasePackCompiler {
469
649
  overlay,
470
650
  `attr:${shortcode}`,
471
651
  ctx,
652
+ { fmKey: "attributes" },
472
653
  );
473
654
  if (embedded) items.push(embedded);
474
655
  }
@@ -496,6 +677,7 @@ export class Actors extends BasePackCompiler {
496
677
  rest,
497
678
  `items:${index}`,
498
679
  ctx,
680
+ { fmKey: "items" },
499
681
  );
500
682
  if (embedded) items.push(embedded);
501
683
  });
@@ -559,6 +741,14 @@ export class Actors extends BasePackCompiler {
559
741
  const name = resolveName(fm);
560
742
  const id = fm.id;
561
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);
562
752
 
563
753
  const items = this.buildEmbeddedItems(itemsMap, id, fm, ctx);
564
754
 
@@ -567,10 +757,13 @@ export class Actors extends BasePackCompiler {
567
757
 
568
758
  const system = {
569
759
  // The frontmatter shortcode is the actor's stable `(type, shortcode)`
570
- // key — and, for a `docArchetype`-flagged being, its archetype
760
+ // key — and, for a being that is an archetype, its archetype
571
761
  // identity (the dedup/override key of the Create-dialog picker, #604).
572
762
  shortcode: fm.shortcode || "",
573
- portrait: resolveImg(fm.portrait) || DEFAULT_IMG.being,
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,
574
767
  appearance: renderSection(body || "", "appearance"),
575
768
  dossier: renderSection(body || "", "dossier"),
576
769
  };
@@ -600,10 +793,29 @@ export class Actors extends BasePackCompiler {
600
793
  system.defaultCombatGroup = defaultCombatGroup;
601
794
  }
602
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
+
603
815
  return {
604
816
  name,
605
- type: "being",
606
- img: resolveImg(fm.img) || DEFAULT_IMG.being,
817
+ type: subType,
818
+ img: resolveImg(blockProperty(fm, SYSTEM, "img")) || defaultImg,
607
819
  _id: id,
608
820
  system,
609
821
  items,
@@ -611,19 +823,20 @@ export class Actors extends BasePackCompiler {
611
823
  name,
612
824
  displayName: 0,
613
825
  actorLink: false,
614
- texture: { src: resolveImg(fm.img) || DEFAULT_IMG.being },
826
+ texture: { src: resolveImg(blockProperty(fm, SYSTEM, "img")) || defaultImg },
615
827
  width: 1,
616
828
  height: 1,
617
829
  sight: { enabled: false },
618
830
  detectionModes: [],
619
831
  },
620
- effects: [],
832
+ effects: Array.isArray(effects) ? [...effects] : [],
621
833
  folder,
622
834
  sort: 0,
623
835
  ownership: { default: 0 },
624
- // `sohl.archetype` (required nullable number) drives
625
- // `flags.sohl.docArchetype` (#640 / archetype contract #604).
626
- flags: withArchetypeFlag(fm, fm.flags, ctx),
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", {}),
627
840
  _stats: this.stats,
628
841
  _key: `!actors!${id}`,
629
842
  };
@@ -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