@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.
- package/CHANGELOG.md +1711 -0
- package/CONTENT.md +288 -72
- package/README.md +1 -1
- package/bin/content-build.mjs +218 -137
- package/bin/package-build.mjs +43 -0
- package/content-config.mjs +62 -67
- package/docs/content-format.md +768 -58
- package/engine/actor-compiler.mjs +586 -0
- package/engine/address-charset.mjs +11 -8
- package/engine/address-diff.mjs +266 -22
- package/engine/anchored-sections.mjs +83 -0
- package/engine/anchors.mjs +83 -0
- package/engine/base-compiler.mjs +148 -18
- package/engine/bundle-notes.mjs +276 -0
- package/engine/bundles.mjs +307 -0
- package/engine/code-fences.mjs +103 -0
- package/engine/compile-corpus.mjs +89 -0
- package/engine/content-address.mjs +267 -5
- package/engine/content-format-check.mjs +54 -11
- package/engine/content-format.mjs +37 -5
- package/engine/content-index.mjs +225 -127
- package/engine/content-links.mjs +185 -77
- package/engine/content-lint.mjs +232 -14
- package/engine/content-package.mjs +2 -1
- package/engine/content-tables.mjs +99 -3
- package/engine/document-subtypes.mjs +49 -6
- package/engine/field-reference.mjs +1 -1
- package/engine/field-spec.mjs +56 -6
- package/engine/folder-notes.mjs +470 -0
- package/engine/foreign-catalog.mjs +195 -4
- package/engine/{manifest-emit.mjs → foundry-entries.mjs} +55 -83
- package/engine/frontmatter-lint.mjs +245 -19
- package/engine/frontmatter.mjs +88 -8
- package/engine/generate.mjs +304 -13
- package/engine/helpers.mjs +293 -64
- package/engine/ids.mjs +137 -2
- package/engine/index-records.mjs +126 -0
- package/engine/index.mjs +23 -5
- package/engine/item-compiler.mjs +349 -0
- package/engine/item-docs.mjs +7 -1
- package/engine/item-registry.mjs +6 -0
- package/engine/journals.mjs +93 -27
- package/engine/macros.mjs +4 -2
- package/engine/metadata-index.mjs +495 -0
- package/engine/note-claims.mjs +146 -25
- package/engine/note-ids.mjs +112 -0
- package/engine/note-renames.mjs +134 -0
- package/engine/note-schemas.mjs +25 -0
- package/engine/note-vocabulary.mjs +110 -27
- package/engine/pack-config.mjs +4 -0
- package/engine/pack-router.mjs +23 -0
- package/engine/retired-fields.mjs +137 -3
- package/engine/scenes.mjs +66 -34
- package/engine/schema-check.mjs +58 -17
- package/engine/site-build.mjs +71 -12
- package/engine/site-index.mjs +20 -7
- package/engine/sql-tables.mjs +485 -0
- package/engine/subtype-registry.mjs +102 -0
- package/engine/system-block.mjs +96 -12
- package/engine/systems.mjs +201 -0
- package/engine/web-wikilinks.mjs +50 -20
- package/engine/wikilink-syntax.mjs +17 -8
- package/engine/wikilinks.mjs +124 -51
- package/engine/yaml-lint.mjs +282 -0
- package/hm3/actors.mjs +352 -0
- package/hm3/default-item-art.mjs +75 -0
- package/hm3/document-subtypes.mjs +134 -0
- package/hm3/index.mjs +56 -0
- package/hm3/item-builders.mjs +84 -0
- package/hm3/item-fields.mjs +180 -0
- package/hm3/items.mjs +96 -0
- package/hm3/template-priority.mjs +85 -0
- package/manifest.mjs +43 -2
- package/package.json +18 -4
- package/release.mjs +62 -7
- package/sohl/actors.mjs +33 -487
- package/sohl/being-info.mjs +16 -7
- package/sohl/default-item-art.mjs +14 -3
- package/sohl/document-subtypes.mjs +16 -10
- package/sohl/item-builders.mjs +14 -5
- package/sohl/item-fields.mjs +68 -7
- package/sohl/items.mjs +44 -258
- package/sohl/note-schemas.mjs +2 -2
- package/types/content-config.d.mts +21 -42
- package/types/engine/actor-compiler.d.mts +204 -0
- package/types/engine/address-charset.d.mts +11 -8
- package/types/engine/address-diff.d.mts +53 -5
- package/types/engine/anchored-sections.d.mts +21 -0
- package/types/engine/anchors.d.mts +20 -0
- package/types/engine/base-compiler.d.mts +17 -17
- package/types/engine/bundle-notes.d.mts +173 -0
- package/types/engine/bundles.d.mts +60 -0
- package/types/engine/code-fences.d.mts +43 -0
- package/types/engine/compile-corpus.d.mts +32 -0
- package/types/engine/content-address.d.mts +205 -5
- package/types/engine/content-format-check.d.mts +6 -2
- package/types/engine/content-format.d.mts +57 -1
- package/types/engine/content-index.d.mts +54 -49
- package/types/engine/content-links.d.mts +52 -7
- package/types/engine/content-lint.d.mts +10 -1
- package/types/engine/content-package.d.mts +2 -1
- package/types/engine/content-tables.d.mts +21 -39
- package/types/engine/document-subtypes.d.mts +37 -3
- package/types/engine/field-spec.d.mts +76 -5
- package/types/engine/folder-notes.d.mts +159 -0
- package/types/engine/foreign-catalog.d.mts +53 -0
- package/types/engine/{manifest-emit.d.mts → foundry-entries.d.mts} +2 -43
- package/types/engine/frontmatter-lint.d.mts +10 -2
- package/types/engine/frontmatter.d.mts +64 -0
- package/types/engine/generate.d.mts +38 -0
- package/types/engine/helpers.d.mts +94 -30
- package/types/engine/ids.d.mts +96 -0
- package/types/engine/index-records.d.mts +68 -0
- package/types/engine/index.d.mts +9 -3
- package/types/engine/item-compiler.d.mts +131 -0
- package/types/engine/journals.d.mts +47 -9
- package/types/engine/metadata-index.d.mts +226 -0
- package/types/engine/note-claims.d.mts +57 -11
- package/types/engine/note-ids.d.mts +38 -0
- package/types/engine/note-renames.d.mts +102 -0
- package/types/engine/note-vocabulary.d.mts +44 -8
- package/types/engine/retired-fields.d.mts +75 -0
- package/types/engine/scenes.d.mts +3 -2
- package/types/engine/schema-check.d.mts +25 -4
- package/types/engine/site-build.d.mts +4 -4
- package/types/engine/site-index.d.mts +1 -1
- package/types/engine/sql-tables.d.mts +185 -0
- package/types/engine/subtype-registry.d.mts +49 -0
- package/types/engine/system-block.d.mts +40 -1
- package/types/engine/systems.d.mts +106 -0
- package/types/engine/web-wikilinks.d.mts +4 -2
- package/types/engine/wikilink-syntax.d.mts +10 -3
- package/types/engine/wikilinks.d.mts +41 -13
- package/types/engine/yaml-lint.d.mts +107 -0
- package/types/hm3/actors.d.mts +48 -0
- package/types/hm3/default-item-art.d.mts +42 -0
- package/types/hm3/document-subtypes.d.mts +24 -0
- package/types/hm3/index.d.mts +7 -0
- package/types/hm3/item-builders.d.mts +11 -0
- package/types/hm3/item-fields.d.mts +12 -0
- package/types/hm3/items.d.mts +23 -0
- package/types/hm3/template-priority.d.mts +21 -0
- package/types/manifest.d.mts +22 -1
- package/types/release.d.mts +9 -4
- package/types/sohl/actors.d.mts +7 -74
- package/types/sohl/being-info.d.mts +8 -27
- package/types/sohl/default-item-art.d.mts +5 -3
- package/types/sohl/items.d.mts +17 -32
- package/engine/foreign-manifests.mjs +0 -126
- package/engine/kb-manifest.mjs +0 -490
- package/types/engine/foreign-manifests.d.mts +0 -43
- 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";
|