@heroiclands/package-build 17.2.0 → 18.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 (152) hide show
  1. package/CHANGELOG.md +1711 -0
  2. package/CONTENT.md +288 -72
  3. package/README.md +1 -1
  4. package/bin/content-build.mjs +218 -137
  5. package/bin/package-build.mjs +43 -0
  6. package/content-config.mjs +62 -67
  7. package/docs/content-format.md +768 -58
  8. package/engine/actor-compiler.mjs +586 -0
  9. package/engine/address-charset.mjs +11 -8
  10. package/engine/address-diff.mjs +266 -22
  11. package/engine/anchored-sections.mjs +83 -0
  12. package/engine/anchors.mjs +83 -0
  13. package/engine/base-compiler.mjs +148 -18
  14. package/engine/bundle-notes.mjs +276 -0
  15. package/engine/bundles.mjs +307 -0
  16. package/engine/code-fences.mjs +103 -0
  17. package/engine/compile-corpus.mjs +89 -0
  18. package/engine/content-address.mjs +267 -5
  19. package/engine/content-format-check.mjs +54 -11
  20. package/engine/content-format.mjs +37 -5
  21. package/engine/content-index.mjs +225 -127
  22. package/engine/content-links.mjs +185 -77
  23. package/engine/content-lint.mjs +232 -14
  24. package/engine/content-package.mjs +2 -1
  25. package/engine/content-tables.mjs +99 -3
  26. package/engine/document-subtypes.mjs +49 -6
  27. package/engine/field-reference.mjs +1 -1
  28. package/engine/field-spec.mjs +56 -6
  29. package/engine/folder-notes.mjs +470 -0
  30. package/engine/foreign-catalog.mjs +195 -4
  31. package/engine/{manifest-emit.mjs → foundry-entries.mjs} +55 -83
  32. package/engine/frontmatter-lint.mjs +245 -19
  33. package/engine/frontmatter.mjs +88 -8
  34. package/engine/generate.mjs +304 -13
  35. package/engine/helpers.mjs +293 -64
  36. package/engine/ids.mjs +137 -2
  37. package/engine/index-records.mjs +126 -0
  38. package/engine/index.mjs +23 -5
  39. package/engine/item-compiler.mjs +349 -0
  40. package/engine/item-docs.mjs +7 -1
  41. package/engine/item-registry.mjs +6 -0
  42. package/engine/journals.mjs +93 -27
  43. package/engine/macros.mjs +4 -2
  44. package/engine/metadata-index.mjs +495 -0
  45. package/engine/note-claims.mjs +146 -25
  46. package/engine/note-ids.mjs +112 -0
  47. package/engine/note-renames.mjs +134 -0
  48. package/engine/note-schemas.mjs +25 -0
  49. package/engine/note-vocabulary.mjs +110 -27
  50. package/engine/pack-config.mjs +4 -0
  51. package/engine/pack-router.mjs +23 -0
  52. package/engine/retired-fields.mjs +137 -3
  53. package/engine/scenes.mjs +66 -34
  54. package/engine/schema-check.mjs +58 -17
  55. package/engine/site-build.mjs +71 -12
  56. package/engine/site-index.mjs +20 -7
  57. package/engine/sql-tables.mjs +485 -0
  58. package/engine/subtype-registry.mjs +102 -0
  59. package/engine/system-block.mjs +96 -12
  60. package/engine/systems.mjs +201 -0
  61. package/engine/web-wikilinks.mjs +50 -20
  62. package/engine/wikilink-syntax.mjs +17 -8
  63. package/engine/wikilinks.mjs +124 -51
  64. package/engine/yaml-lint.mjs +282 -0
  65. package/hm3/actors.mjs +352 -0
  66. package/hm3/default-item-art.mjs +75 -0
  67. package/hm3/document-subtypes.mjs +134 -0
  68. package/hm3/index.mjs +56 -0
  69. package/hm3/item-builders.mjs +84 -0
  70. package/hm3/item-fields.mjs +180 -0
  71. package/hm3/items.mjs +96 -0
  72. package/hm3/template-priority.mjs +85 -0
  73. package/manifest.mjs +43 -2
  74. package/package.json +18 -4
  75. package/release.mjs +62 -7
  76. package/sohl/actors.mjs +33 -487
  77. package/sohl/being-info.mjs +16 -7
  78. package/sohl/default-item-art.mjs +14 -3
  79. package/sohl/document-subtypes.mjs +16 -10
  80. package/sohl/item-builders.mjs +14 -5
  81. package/sohl/item-fields.mjs +68 -7
  82. package/sohl/items.mjs +44 -258
  83. package/sohl/note-schemas.mjs +2 -2
  84. package/types/content-config.d.mts +21 -42
  85. package/types/engine/actor-compiler.d.mts +204 -0
  86. package/types/engine/address-charset.d.mts +11 -8
  87. package/types/engine/address-diff.d.mts +53 -5
  88. package/types/engine/anchored-sections.d.mts +21 -0
  89. package/types/engine/anchors.d.mts +20 -0
  90. package/types/engine/base-compiler.d.mts +17 -17
  91. package/types/engine/bundle-notes.d.mts +173 -0
  92. package/types/engine/bundles.d.mts +60 -0
  93. package/types/engine/code-fences.d.mts +43 -0
  94. package/types/engine/compile-corpus.d.mts +32 -0
  95. package/types/engine/content-address.d.mts +205 -5
  96. package/types/engine/content-format-check.d.mts +6 -2
  97. package/types/engine/content-format.d.mts +57 -1
  98. package/types/engine/content-index.d.mts +54 -49
  99. package/types/engine/content-links.d.mts +52 -7
  100. package/types/engine/content-lint.d.mts +10 -1
  101. package/types/engine/content-package.d.mts +2 -1
  102. package/types/engine/content-tables.d.mts +21 -39
  103. package/types/engine/document-subtypes.d.mts +37 -3
  104. package/types/engine/field-spec.d.mts +76 -5
  105. package/types/engine/folder-notes.d.mts +159 -0
  106. package/types/engine/foreign-catalog.d.mts +53 -0
  107. package/types/engine/{manifest-emit.d.mts → foundry-entries.d.mts} +2 -43
  108. package/types/engine/frontmatter-lint.d.mts +10 -2
  109. package/types/engine/frontmatter.d.mts +64 -0
  110. package/types/engine/generate.d.mts +38 -0
  111. package/types/engine/helpers.d.mts +94 -30
  112. package/types/engine/ids.d.mts +96 -0
  113. package/types/engine/index-records.d.mts +68 -0
  114. package/types/engine/index.d.mts +9 -3
  115. package/types/engine/item-compiler.d.mts +131 -0
  116. package/types/engine/journals.d.mts +47 -9
  117. package/types/engine/metadata-index.d.mts +226 -0
  118. package/types/engine/note-claims.d.mts +57 -11
  119. package/types/engine/note-ids.d.mts +38 -0
  120. package/types/engine/note-renames.d.mts +102 -0
  121. package/types/engine/note-vocabulary.d.mts +44 -8
  122. package/types/engine/retired-fields.d.mts +75 -0
  123. package/types/engine/scenes.d.mts +3 -2
  124. package/types/engine/schema-check.d.mts +25 -4
  125. package/types/engine/site-build.d.mts +4 -4
  126. package/types/engine/site-index.d.mts +1 -1
  127. package/types/engine/sql-tables.d.mts +185 -0
  128. package/types/engine/subtype-registry.d.mts +49 -0
  129. package/types/engine/system-block.d.mts +40 -1
  130. package/types/engine/systems.d.mts +106 -0
  131. package/types/engine/web-wikilinks.d.mts +4 -2
  132. package/types/engine/wikilink-syntax.d.mts +10 -3
  133. package/types/engine/wikilinks.d.mts +41 -13
  134. package/types/engine/yaml-lint.d.mts +107 -0
  135. package/types/hm3/actors.d.mts +48 -0
  136. package/types/hm3/default-item-art.d.mts +42 -0
  137. package/types/hm3/document-subtypes.d.mts +24 -0
  138. package/types/hm3/index.d.mts +7 -0
  139. package/types/hm3/item-builders.d.mts +11 -0
  140. package/types/hm3/item-fields.d.mts +12 -0
  141. package/types/hm3/items.d.mts +23 -0
  142. package/types/hm3/template-priority.d.mts +21 -0
  143. package/types/manifest.d.mts +22 -1
  144. package/types/release.d.mts +9 -4
  145. package/types/sohl/actors.d.mts +7 -74
  146. package/types/sohl/being-info.d.mts +8 -27
  147. package/types/sohl/default-item-art.d.mts +5 -3
  148. package/types/sohl/items.d.mts +17 -32
  149. package/engine/foreign-manifests.mjs +0 -126
  150. package/engine/kb-manifest.mjs +0 -490
  151. package/types/engine/foreign-manifests.d.mts +0 -43
  152. package/types/engine/kb-manifest.d.mts +0 -241
package/hm3/actors.mjs ADDED
@@ -0,0 +1,352 @@
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
+ * **HM3's Actor pass** — what an HM3 `character` or `creature` holds.
16
+ *
17
+ * The machinery is {@link module:engine/actor-compiler}'s and is shared with
18
+ * the SoHL pass: the predefined-item catalogue, the note-vocabulary →
19
+ * document-subtype translation each embedded reference goes through (#140), the
20
+ * merge and the stable embedded ids. This module states HM3's data model and
21
+ * nothing else.
22
+ *
23
+ * **A being note is one Actor per system, and the subtype is authored.** HM3
24
+ * splits what a note calls a `being` into a `character` and a `creature`, and
25
+ * nothing in the note's own vocabulary partitions cleanly onto that split — so
26
+ * the note says which, by writing `hm3.type`. An absent one is an error naming
27
+ * the note, never a default (#139).
28
+ *
29
+ * **What is emitted, and what is deliberately not.** Four rows of the content
30
+ * format's `being` mapping table give HM3 a destination — `data.portrait` →
31
+ * `system.bioImage`, `data.species`, `data.gender`, `data.occupation`, and
32
+ * `data.templatePriority` → `flags.hm3.templatePriority` — and, since #305,
33
+ * three of them are *declared as those sources* rather than as the bare key the
34
+ * corpus writes, so the specification's mapping is executable rather than
35
+ * aspirational. Plus the two anchored prose sections: `{#appearance}` is HM3's `description` and `{#dossier}` its
36
+ * `biography`. Everything else an HM3 actor carries — the thirteen abilities,
37
+ * the sunsign, `move`, `fatigue`, `shockIndex`, a creature's `loadRating` — has
38
+ * no shared source stated anywhere, so it is authored at its own path under
39
+ * `hm3.system` and reaches the document through the verbatim passthrough. It is
40
+ * checked against HM3's published `schema.json` like everything else; it simply
41
+ * is not invented here.
42
+ *
43
+ * `gender` and `occupation` are written **only on a `character`**, because that
44
+ * is the only subtype HM3 declares them on. Emitting them on a creature would
45
+ * be a key Foundry discards at load without a word — which is exactly what the
46
+ * emitted-`system` check would report, on every creature in the pack.
47
+ *
48
+ * @module
49
+ */
50
+
51
+ import { resolveName, resolveImg } from "../engine/helpers.mjs";
52
+ import { buildFromFields, STRING } from "../engine/field-spec.mjs";
53
+ import { SystemActorCompiler } from "../engine/actor-compiler.mjs";
54
+ import { renderSection } from "../engine/anchored-sections.mjs";
55
+ import { documentSubtype } from "../engine/document-subtypes.mjs";
56
+ import { HM3_DOCUMENT_SUBTYPES } from "./document-subtypes.mjs";
57
+ import { templateFlags } from "./template-priority.mjs";
58
+ // The retirement window's report, shared with the frontmatter lint so the
59
+ // two cannot say different things about the same key (#305).
60
+ import { legacyKeyMessage, locateFrontmatterKey } from "../engine/retired-fields.mjs";
61
+ // The note-level `hm3:` block: `hm3.system` onto the document's `system`
62
+ // verbatim, and `hm3.img` / `hm3.items` / `hm3.effects` / `hm3.flags`
63
+ // overriding their shared top-level forms for this system alone (#58).
64
+ import { blockField, blockProperty, mergeSystemData } from "../engine/system-block.mjs";
65
+
66
+ /**
67
+ * The shared `data:` facts every HM3 actor takes, whatever its subtype.
68
+ *
69
+ * Declared rather than read by hand, so a value resolves by the same order
70
+ * every other declared field does — `hm3.system.<to>` first, then the in-block
71
+ * position, then the shared source, then the default (#58) — and so the
72
+ * author-facing reference can be generated from the same statement the compiler
73
+ * obeys (#22).
74
+ *
75
+ * **Both positions, named separately (#305).** The specification maps
76
+ * `data.species` onto `system.species`, and every HM3 note in the corpus writes
77
+ * `hm3.species`. Until #126 sweeps them those are two live positions for one
78
+ * field, so the declaration names both: `name` is the shared source, `legacyKey`
79
+ * the key the block still carries, and the block wins while it is there. Naming
80
+ * only one of them is what made the row unreadable — a plain `species` could
81
+ * not see `data.species`, and a dotted `data.species` could not see the 2,512
82
+ * notes that carry `hm3.species`, each yielding `""` in silence.
83
+ *
84
+ * @type {readonly import("../engine/field-spec.mjs").FieldSpec[]}
85
+ */
86
+ const ACTOR_FIELDS = Object.freeze([
87
+ {
88
+ name: "data.species",
89
+ legacyKey: "species",
90
+ to: "species",
91
+ ...STRING,
92
+ default: "",
93
+ describe: "The kind of creature this is.",
94
+ },
95
+ ]);
96
+
97
+ /**
98
+ * The two `data:` facts HM3 declares on a `character` and not on a `creature`.
99
+ *
100
+ * Both name their shared source and their legacy in-block key, for the reason
101
+ * {@link ACTOR_FIELDS} does.
102
+ *
103
+ * @type {readonly import("../engine/field-spec.mjs").FieldSpec[]}
104
+ */
105
+ const CHARACTER_FIELDS = Object.freeze([
106
+ {
107
+ name: "data.gender",
108
+ legacyKey: "gender",
109
+ to: "gender",
110
+ ...STRING,
111
+ default: "",
112
+ describe: "The character's gender.",
113
+ },
114
+ {
115
+ name: "data.occupation",
116
+ legacyKey: "occupation",
117
+ to: "occupation",
118
+ ...STRING,
119
+ default: "",
120
+ describe: "What the character does for a living.",
121
+ },
122
+ ]);
123
+
124
+ /**
125
+ * Default art per HM3 actor **subtype**, applied when the note supplies no
126
+ * `img` / `portrait`.
127
+ *
128
+ * The two paths HM3's own actor model and migration use, so a compiled actor
129
+ * looks like one created in the client.
130
+ *
131
+ * @type {Readonly<Record<string, string>>}
132
+ */
133
+ const DEFAULT_IMG = Object.freeze({
134
+ character: "systems/hm3/images/svg/knight-silhouette.svg",
135
+ creature: "systems/hm3/images/svg/monster-silhouette.svg",
136
+ });
137
+
138
+ /**
139
+ * The default art for an HM3 actor subtype.
140
+ *
141
+ * Fail-fast, for the reason the item art map is: a subtype with no art would
142
+ * otherwise ship a document with no image and nothing said about it.
143
+ *
144
+ * @param {string} subType - The Foundry actor subtype.
145
+ * @returns {string} The default image path.
146
+ * @throws {Error} When this map pairs no art with the subtype.
147
+ */
148
+ function defaultActorImg(subType) {
149
+ const img = /** @type {Record<string, string|undefined>} */ (DEFAULT_IMG)[subType];
150
+ if (!img) {
151
+ throw new Error(
152
+ `No default art for HM3 actor subtype "${subType}" — add an entry ` +
153
+ `to \`DEFAULT_IMG\` in hm3/actors.mjs, beside the map row that ` +
154
+ `introduced the subtype.`,
155
+ );
156
+ }
157
+ return img;
158
+ }
159
+
160
+ export class Hm3Actors extends SystemActorCompiler {
161
+ /**
162
+ * HM3's note-type → document-subtype map — the one declaration that says
163
+ * which block this pass reads, which notes it claims, and what each
164
+ * becomes. It is also what every embedded reference is translated through.
165
+ *
166
+ * @type {import("../engine/document-subtypes.mjs").DocumentSubtypeMap}
167
+ */
168
+ static documentSubtypes = HM3_DOCUMENT_SUBTYPES;
169
+
170
+ /**
171
+ * Compile one `being` note into its HM3 actor document.
172
+ *
173
+ * @param {object} fm - The note's frontmatter.
174
+ * @param {string} markdown - The body, tables expanded and wikilinks
175
+ * resolved.
176
+ * @returns {object} The actor document, keyed for the pack.
177
+ */
178
+ buildEntry(fm, markdown) {
179
+ return this.buildActor(this.itemsMap, fm, markdown);
180
+ }
181
+
182
+ /**
183
+ * Build every embedded item an HM3 actor carries, from `hm3.items`.
184
+ *
185
+ * One list, not two: HM3 keeps a character's abilities in `system.abilities`
186
+ * rather than as embedded documents, so there is no attributes map to
187
+ * expand the way SoHL's pass expands `sohl.attributes`.
188
+ *
189
+ * @param {Map<string, object>} itemsMap - The predefined items, by address.
190
+ * @param {string} actorId - The owning actor's id, seeding embedded ids.
191
+ * @param {object} fm - The note's frontmatter.
192
+ * @param {string} ctx - Diagnostic context (the actor's label).
193
+ * @returns {object[]} The embedded items.
194
+ */
195
+ buildEmbeddedItems(itemsMap, actorId, fm, ctx) {
196
+ const items = [];
197
+ const declared = blockField(fm, this.system, "items", null);
198
+ if (!Array.isArray(declared)) return items;
199
+
200
+ declared.forEach((entry, index) => {
201
+ if (!entry || typeof entry !== "object") {
202
+ this.noteError(`${ctx}: ${this.system}.items[${index}] is not an object`);
203
+ this.errorCount++;
204
+ return;
205
+ }
206
+ const { shortcode, type, ...rest } = entry;
207
+ if (!type) {
208
+ this.noteError(`${ctx}: ${this.system}.items[${index}] missing type`);
209
+ this.errorCount++;
210
+ return;
211
+ }
212
+ const embedded = this.resolveEmbedded(
213
+ itemsMap,
214
+ actorId,
215
+ type,
216
+ shortcode || null,
217
+ rest,
218
+ `items:${index}`,
219
+ ctx,
220
+ { fmKey: "items" },
221
+ );
222
+ if (embedded) items.push(embedded);
223
+ });
224
+ return items;
225
+ }
226
+
227
+ /**
228
+ * Build one HM3 actor document from a `being` note.
229
+ *
230
+ * @param {Map<string, object>} itemsMap - The predefined items, by address.
231
+ * @param {object} fm - The note's frontmatter.
232
+ * @param {string} body - The note body, converted.
233
+ * @returns {object} The actor document, keyed for the pack.
234
+ */
235
+ buildActor(itemsMap, fm, body) {
236
+ const block = this.system;
237
+ const name = resolveName(fm);
238
+ const id = fm.id;
239
+ const ctx = `actor "${name}"`;
240
+ // The document's own subtype, and the art that goes with it. Both are
241
+ // looked up from the note's `type` rather than spelled here (#79); for
242
+ // HM3 the row is one-to-many, so the note's `hm3.type` decides.
243
+ const subType = /** @type {string} */ (
244
+ documentSubtype(this.documentSubtypes, fm.type, fm, {
245
+ absPath: this.currentNote?.absPath,
246
+ })
247
+ );
248
+ const defaultImg = defaultActorImg(subType);
249
+
250
+ const items = this.buildEmbeddedItems(itemsMap, id, fm, ctx);
251
+
252
+ // The sweep's progress signal, reported where an author meets it
253
+ // soonest — every consumer runs the compile, and not every one runs the
254
+ // lint (#142). A warning: the note compiles to the correct document
255
+ // either way, and refusing the position comes once no tree writes it.
256
+ const onLegacyKey = (field) =>
257
+ this.noteWarn(
258
+ legacyKeyMessage(block, field),
259
+ locateFrontmatterKey(this.currentNote?.absPath, field.legacyKey),
260
+ );
261
+
262
+ // Both spellings, as everywhere else: `packFolder` is a folder note's
263
+ // address and `folder` a Foundry id, and which one applies comes from
264
+ // the field it was written in rather than from the string (#251, #255).
265
+ // This pass read only the id, so an HM3 tree could not file an actor by
266
+ // address at all — which its own sweep needs.
267
+ const packFolderAddress = blockField(fm, block, "packFolder", null);
268
+ const folder =
269
+ packFolderAddress ?
270
+ this.folderResolver(packFolderAddress, { isAddress: true })
271
+ : this.folderResolver(blockField(fm, block, "folder", null));
272
+
273
+ const system = {
274
+ // Nullish, not `||` (#218): a note that names no portrait gets the
275
+ // subtype's default, one that writes `""` ships blank on purpose.
276
+ bioImage: resolveImg(blockProperty(fm, block, "portrait")) ?? defaultImg,
277
+ description: renderSection(body || "", "appearance"),
278
+ biography: renderSection(body || "", "dossier"),
279
+ ...buildFromFields(ACTOR_FIELDS, { block, onLegacyKey })(fm),
280
+ // Declared on `character` alone, so written there alone — see the
281
+ // module note.
282
+ ...(subType === "character" ?
283
+ buildFromFields(CHARACTER_FIELDS, { block, onLegacyKey })(fm)
284
+ : {}),
285
+ };
286
+
287
+ // Whatever the note authors under `hm3.system`, at the DataModel's own
288
+ // paths (#58). This pass has no field declaration, so it claims
289
+ // nothing: every authored path is the author's, and the fields above
290
+ // are what a note that authors none still gets.
291
+ mergeSystemData(system, fm, { block });
292
+ this.reportUndeclaredSystemData(fm, block, "Actor", subType);
293
+ // And what this pass wrote itself — there is no field declaration for a
294
+ // being at all, so every key above is a compiler emission (#155).
295
+ this.reportEmittedSystemData(system, {
296
+ fm,
297
+ block,
298
+ documentType: "Actor",
299
+ subType,
300
+ type: fm.type,
301
+ });
302
+
303
+ const effects = blockProperty(fm, block, "effects");
304
+ const img = resolveImg(blockProperty(fm, block, "img")) ?? defaultImg;
305
+
306
+ return {
307
+ name,
308
+ type: subType,
309
+ img,
310
+ _id: id,
311
+ system,
312
+ items,
313
+ prototypeToken: {
314
+ name,
315
+ displayName: 0,
316
+ actorLink: false,
317
+ texture: { src: img },
318
+ width: 1,
319
+ height: 1,
320
+ sight: { enabled: false },
321
+ detectionModes: [],
322
+ },
323
+ effects: Array.isArray(effects) ? [...effects] : [],
324
+ folder,
325
+ sort: 0,
326
+ ownership: { default: 0 },
327
+ // Whatever the note authors, plus the template priority — which HM3
328
+ // keeps in flags rather than in `system`, so there is nowhere in
329
+ // the data model for it to go.
330
+ flags: this.actorFlags(fm, block),
331
+ _stats: this.stats,
332
+ _key: `!actors!${id}`,
333
+ };
334
+ }
335
+
336
+ /**
337
+ * The document's `flags`: whatever the note authors, plus this system's
338
+ * template priority.
339
+ *
340
+ * The rule itself is {@link module:hm3/template-priority.templateFlags},
341
+ * because the Item pass writes the same flag from the same statement (#283)
342
+ * and two copies of it were one copy too many — this pass had the only one,
343
+ * and the Item pass had none.
344
+ *
345
+ * @param {object} fm - The note's frontmatter.
346
+ * @param {string} block - This pass's system block.
347
+ * @returns {object} The flags to emit.
348
+ */
349
+ actorFlags(fm, block) {
350
+ return templateFlags(fm, block);
351
+ }
352
+ }
@@ -0,0 +1,75 @@
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
+ * Default HM3 item artwork, keyed by the **note** type that compiles into it.
16
+ *
17
+ * The registry contract is fail-fast (see {@link module:engine/item-registry.itemArt}):
18
+ * a type whose entry pairs no art aborts the pack build for a note that carries
19
+ * no `img:` of its own, rather than shipping a mismatched icon. So every type
20
+ * `item-builders.mjs` declares has a row here.
21
+ *
22
+ * **Paths are fully resolved.** `resolveImg` rewrites a leading `icons/` or
23
+ * `images/` to the *consuming package's* asset root, which is not where these
24
+ * live: they are shipped by the HM3 system. Written as `systems/hm3/images/…`
25
+ * they pass through untouched and address the icons HM3's own compendiums
26
+ * already use, so an item compiled from a note looks like its hand-authored
27
+ * neighbours.
28
+ *
29
+ * **A one-to-many type gets one default**, because art is keyed by note type
30
+ * and a note type is what a registry entry addresses. `weapongear` compiles
31
+ * into a weapon or a missile and defaults to the sword either way; a note whose
32
+ * subject is a thrown spear says so with an `img:` of its own. Keying art by
33
+ * document subtype instead would mean a second lookup that could disagree with
34
+ * the registry's, and the note-level override already answers the case.
35
+ *
36
+ * @module
37
+ */
38
+
39
+ /**
40
+ * Note type → the image an HM3 item of that type ships with.
41
+ *
42
+ * @type {Readonly<Record<string, string>>}
43
+ */
44
+ export const HM3_DEFAULT_ITEM_ART = Object.freeze({
45
+ armor: "systems/hm3/images/icons/svg/armor.svg",
46
+ armorlocation: "systems/hm3/images/icons/svg/anatomy.svg",
47
+ containergear: "systems/hm3/images/icons/svg/sack.svg",
48
+ miscgear: "systems/hm3/images/icons/svg/miscgear.svg",
49
+ mysticalability: "systems/hm3/images/icons/svg/psionics.svg",
50
+ projectile: "systems/hm3/images/icons/svg/arrow.svg",
51
+ skill: "systems/hm3/images/icons/svg/skills.svg",
52
+ trauma: "systems/hm3/images/icons/svg/injury.svg",
53
+ weapongear: "systems/hm3/images/icons/svg/sword.svg",
54
+ });
55
+
56
+ /**
57
+ * The default art path for an HM3 item type, or throw when the type is unknown.
58
+ *
59
+ * Reading this from `item-builders.mjs` is what keeps the two lists one: that
60
+ * module cannot declare a type this map does not cover, because this throws and
61
+ * that module evaluates at import.
62
+ *
63
+ * @param {string} type - The note type.
64
+ * @returns {string} The default image path for it.
65
+ * @throws {Error} When no art is paired with the type.
66
+ */
67
+ export function hm3DefaultItemArt(type) {
68
+ if (!(type in HM3_DEFAULT_ITEM_ART)) {
69
+ throw new Error(
70
+ `No default art for HM3 item type "${type}" — add one to ` +
71
+ `@heroiclands/package-build/hm3/default-item-art`,
72
+ );
73
+ }
74
+ return /** @type {Record<string, string>} */ (HM3_DEFAULT_ITEM_ART)[type];
75
+ }
@@ -0,0 +1,134 @@
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
+ * **HM3's note-type → document-subtype map** — which Foundry document, and
16
+ * which subtype of it, a note of each content type compiles into for HârnMaster
17
+ * 3 (#139).
18
+ *
19
+ * The mechanism is `engine/document-subtypes.mjs`, the same one SoHL's
20
+ * declaration uses; the two halves share it and share nothing else. That is the
21
+ * `engine/` ÷ system line this package draws everywhere (#36), and this file is
22
+ * the first evidence that the line holds: a second system declared its map
23
+ * without a line of the mechanism changing.
24
+ *
25
+ * **Where this map differs from SoHL's, and why that matters.**
26
+ *
27
+ * SoHL's map was the identity in every row until #78 renamed three of them,
28
+ * which was a fact about SoHL rather than a rule (#79) — and while it stayed
29
+ * true nothing proved the map was doing any work. HM3's was never close. Four
30
+ * of its rows are one-to-many, one renames outright (`projectile` →
31
+ * `missilegear`), and five of its type names are SoHL's own with a *different
32
+ * data model* behind them. A build that inferred a subtype from a note's `type`
33
+ * would be wrong on nine rows out of ten here, and — worse — silently
34
+ * right-looking on the five shared names.
35
+ *
36
+ * **The five shared names.** `skill`, `weapongear`, `armor`, `containergear`
37
+ * and `miscgear` are declared by both systems. A SoHL skill
38
+ * stores `masteryLevelBase`; an HM3 skill stores `masteryLevel` and a `type` of
39
+ * `"Craft"`/`"Physical"`/…. Name-matching between the two would not fail — it
40
+ * would succeed wrongly, emitting a document Foundry accepts and then strips to
41
+ * nothing at load. So each name resolves through *its own system's* map and is
42
+ * built by *its own system's* registry, and the schema check reads that
43
+ * system's published `schema.json`.
44
+ *
45
+ * **The four one-to-many rows are authored, never derived.** A note says which
46
+ * HM3 subtype it is, in its own `hm3:` block, by writing `hm3.type`. Nothing is
47
+ * inferred from the note's `subType`: an author who says nothing gets an error
48
+ * naming the note and listing the permitted values, because a default would
49
+ * pick one of them and be right about half the time.
50
+ *
51
+ * `hm3.type` is not a new spelling. `type` is already one of the properties a
52
+ * system block may carry ({@link module:engine/system-block.BLOCK_DOCUMENT_PROPERTIES}),
53
+ * meaning "this system's document type"; the discriminator is that property,
54
+ * read where it was always going to be written.
55
+ *
56
+ * **What this map is not.** It says which document a note becomes, never what
57
+ * that document contains: the `system` block comes from `item-builders.mjs`
58
+ * here exactly as it does in `sohl/`.
59
+ *
60
+ * @module
61
+ */
62
+
63
+ import { defineDocumentSubtypes } from "../engine/document-subtypes.mjs";
64
+
65
+ /**
66
+ * The frontmatter key inside the `hm3:` block that resolves a one-to-many row.
67
+ *
68
+ * One key for all four rows, deliberately: an author who has learned it on a
69
+ * weapon has learned it on a trauma. It is the block's `type` property — the
70
+ * document type this system compiles the note into — which is what the content
71
+ * format has always called it.
72
+ *
73
+ * @type {string}
74
+ */
75
+ export const HM3_TYPE_KEY = "type";
76
+
77
+ /**
78
+ * Every content type HM3 compiles into a Foundry document, and what it becomes.
79
+ *
80
+ * The Item rows are the nine types `item-builders.mjs` declares; the one Actor
81
+ * row is `being`, which HM3 splits into `character` and `creature`. Types this
82
+ * map does not name — `affiliation`, `affliction`, `attribute`,
83
+ * `concoction`, `mystery`, and every core type — compile into no HM3
84
+ * document at all, silently and correctly: HM3 has no form of them, and a
85
+ * finding on every such note would be the noise #79's rule exists to prevent.
86
+ *
87
+ * @type {import("../engine/document-subtypes.mjs").DocumentSubtypeMap}
88
+ */
89
+ export const HM3_DOCUMENT_SUBTYPES = defineDocumentSubtypes({
90
+ system: "hm3",
91
+ types: {
92
+ armor: { document: "Item", subType: "armorgear" },
93
+ // HM3-only: SoHL resolves a hit location from a being's own body
94
+ // structure, so it has no item of this kind and no note type for one.
95
+ armorlocation: { document: "Item", subType: "armorlocation" },
96
+ containergear: { document: "Item", subType: "containergear" },
97
+ miscgear: { document: "Item", subType: "miscgear" },
98
+ // The one outright rename. A note calls it a projectile; HM3 calls the
99
+ // document a missile.
100
+ projectile: { document: "Item", subType: "missilegear" },
101
+ skill: { document: "Item", subType: "skill" },
102
+
103
+ // ── the four one-to-many rows ───────────────────────────────────────
104
+ // SoHL keeps one `mysticalability` with a `system.subType`; HM3 has
105
+ // three separate documents, and nothing in the note's own subType
106
+ // vocabulary partitions cleanly onto them.
107
+ mysticalability: {
108
+ document: "Item",
109
+ discriminator: HM3_TYPE_KEY,
110
+ subTypes: ["psionic", "spell", "invocation"],
111
+ },
112
+ // SoHL's `trauma` gathers eleven kinds of harm under one item; HM3 has
113
+ // an `injury` for the physical and a `trait` for everything standing.
114
+ trauma: {
115
+ document: "Item",
116
+ discriminator: HM3_TYPE_KEY,
117
+ subTypes: ["injury", "trait"],
118
+ },
119
+ // SoHL distinguishes a weapon's uses with strike modes on one item; HM3
120
+ // assumes one usage per document, so a thrown spear is a *second*
121
+ // document. Which one a given note describes is the note's to say.
122
+ weapongear: {
123
+ document: "Item",
124
+ discriminator: HM3_TYPE_KEY,
125
+ subTypes: ["weapongear", "missilegear"],
126
+ },
127
+
128
+ being: {
129
+ document: "Actor",
130
+ discriminator: HM3_TYPE_KEY,
131
+ subTypes: ["character", "creature"],
132
+ },
133
+ },
134
+ });
package/hm3/index.mjs ADDED
@@ -0,0 +1,56 @@
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
+ * The HM3-specific half of the toolchain: the knowledge of the HârnMaster 3
16
+ * data model that a generic content module must never receive (#139).
17
+ *
18
+ * The second half, and the first evidence that `engine/` ÷ system is a real
19
+ * line rather than an aspiration: HM3's item-type registry and its builders,
20
+ * its note-type → document-subtype map, its two compilers and its default-art
21
+ * map live here, and nothing in `@heroiclands/package-build/engine` exports any
22
+ * of it. It imports nothing from `sohl/` and `sohl/` imports nothing from it —
23
+ * the only thing the two share is the engine between them.
24
+ *
25
+ * A consuming repository that ships content for both systems names both
26
+ * registries — `itemBuilders: [sohl, hm3]` — and declares one pack per system
27
+ * per document type. Everything after that is the engine's: which system's map
28
+ * claims a note, which system's builder shapes it, which system's schema it is
29
+ * checked against, and which system's version stamps it.
30
+ *
31
+ * Namespaced rather than flattened, for the reason the engine barrel gives.
32
+ *
33
+ * @module
34
+ */
35
+
36
+ /** The item-type registry: every type that compiles into an HM3 Item, and its builder. */
37
+ export * as itemBuilders from "./item-builders.mjs";
38
+
39
+ /** The `hm3:` frontmatter vocabulary of every HM3 item type. */
40
+ export * as itemFields from "./item-fields.mjs";
41
+
42
+ /** This system's note-type → document-subtype map, one-to-many rows and all (#139). */
43
+ export * as documentSubtypes from "./document-subtypes.mjs";
44
+
45
+ /** The Item compiler. */
46
+ export * as items from "./items.mjs";
47
+
48
+ /** The Actor compiler. */
49
+ export * as actors from "./actors.mjs";
50
+
51
+ /** Where HM3 records the template priority — one rule, called by both compilers. */
52
+ export * as templatePriority from "./template-priority.mjs";
53
+
54
+ // Flat as well as namespaced, matching the SoHL barrel: the default-art map is
55
+ // the one export a Foundry runtime would import by name.
56
+ export { HM3_DEFAULT_ITEM_ART, hm3DefaultItemArt } from "./default-item-art.mjs";