@heroiclands/package-build 0.6.1 → 3.0.1
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-content-build.md +965 -0
- package/CHANGELOG.md +68 -0
- package/CONTENT.md +824 -0
- package/MIGRATING.md +127 -0
- package/README.md +29 -17
- package/bin/content-build.mjs +1082 -0
- package/bin/package-build.mjs +2 -2
- package/bin/report.mjs +2 -2
- package/config.mjs +21 -18
- package/content-config.mjs +1424 -0
- package/coverage.mjs +1 -1
- package/e2e.mjs +1 -1
- package/engine/abbreviations.mjs +0 -0
- package/engine/base-compiler.mjs +608 -0
- package/engine/code-fences.mjs +311 -0
- package/engine/compendiums.mjs +353 -0
- package/engine/content-address.mjs +141 -0
- package/engine/content-links.mjs +438 -0
- package/engine/content-lint.mjs +204 -0
- package/engine/content-package.mjs +67 -0
- package/engine/content-slug.mjs +169 -0
- package/engine/content-tables.mjs +1216 -0
- package/engine/content-tree.mjs +56 -0
- package/engine/diagnostics.mjs +270 -0
- package/engine/field-reference.mjs +242 -0
- package/engine/field-spec.mjs +218 -0
- package/engine/foreign-catalog.mjs +450 -0
- package/engine/foreign-manifests.mjs +129 -0
- package/engine/frontmatter-lint.mjs +343 -0
- package/engine/frontmatter.mjs +258 -0
- package/engine/generate.mjs +305 -0
- package/engine/helpers.mjs +809 -0
- package/engine/ids.mjs +192 -0
- package/engine/index.mjs +142 -0
- package/engine/item-docs.mjs +153 -0
- package/engine/item-registry.mjs +129 -0
- package/engine/journals.mjs +374 -0
- package/engine/kb-manifest.mjs +473 -0
- package/engine/macros.mjs +333 -0
- package/engine/manifest-emit.mjs +339 -0
- package/engine/map-notes.mjs +1302 -0
- package/engine/pack-config.mjs +479 -0
- package/engine/pack-router.mjs +254 -0
- package/engine/prose-config.mjs +238 -0
- package/engine/prose-lint.mjs +294 -0
- package/engine/region-events.mjs +79 -0
- package/engine/scene-levels.mjs +420 -0
- package/engine/scenes.mjs +528 -0
- package/engine/site-build.mjs +778 -0
- package/engine/site-index.mjs +332 -0
- package/engine/web-wikilinks.mjs +330 -0
- package/engine/wikilink-syntax.mjs +105 -0
- package/engine/wikilinks.mjs +645 -0
- package/index.mjs +42 -25
- package/lang.mjs +1 -1
- package/manifest.mjs +79 -5
- package/markdownlint-config.mjs +45 -0
- package/package.json +78 -35
- package/prettier-config.mjs +34 -0
- package/sohl/actors.mjs +581 -0
- package/sohl/affiliation-standings.mjs +44 -0
- package/sohl/being-info.mjs +173 -0
- package/sohl/default-item-art.mjs +82 -0
- package/sohl/index.mjs +55 -0
- package/sohl/item-builders.mjs +114 -0
- package/sohl/item-fields.mjs +867 -0
- package/sohl/items.mjs +197 -0
- package/sohl/kb-passes.mjs +207 -0
- package/sohl/note-schemas.mjs +322 -0
- package/templates.mjs +1 -1
- package/types/config.d.mts +1 -1
- package/types/content-config.d.mts +571 -0
- package/types/engine/abbreviations.d.mts +44 -0
- package/types/engine/base-compiler.d.mts +320 -0
- package/types/engine/code-fences.d.mts +102 -0
- package/types/engine/compendiums.d.mts +77 -0
- package/types/engine/content-address.d.mts +61 -0
- package/types/engine/content-links.d.mts +87 -0
- package/types/engine/content-lint.d.mts +47 -0
- package/types/engine/content-package.d.mts +36 -0
- package/types/engine/content-slug.d.mts +59 -0
- package/types/engine/content-tables.d.mts +131 -0
- package/types/engine/content-tree.d.mts +12 -0
- package/types/engine/diagnostics.d.mts +152 -0
- package/types/engine/field-reference.d.mts +20 -0
- package/types/engine/field-spec.d.mts +181 -0
- package/types/engine/foreign-catalog.d.mts +103 -0
- package/types/engine/foreign-manifests.d.mts +43 -0
- package/types/engine/frontmatter-lint.d.mts +74 -0
- package/types/engine/frontmatter.d.mts +99 -0
- package/types/engine/generate.d.mts +56 -0
- package/types/engine/helpers.d.mts +348 -0
- package/types/engine/ids.d.mts +126 -0
- package/types/engine/index.d.mts +33 -0
- package/types/engine/item-docs.d.mts +73 -0
- package/types/engine/item-registry.d.mts +57 -0
- package/types/engine/journals.d.mts +121 -0
- package/types/engine/kb-manifest.d.mts +214 -0
- package/types/engine/macros.d.mts +201 -0
- package/types/engine/manifest-emit.d.mts +137 -0
- package/types/engine/map-notes.d.mts +283 -0
- package/types/engine/pack-config.d.mts +77 -0
- package/types/engine/pack-router.d.mts +53 -0
- package/types/engine/prose-config.d.mts +121 -0
- package/types/engine/prose-lint.d.mts +59 -0
- package/types/engine/region-events.d.mts +43 -0
- package/types/engine/scene-levels.d.mts +36 -0
- package/types/engine/scenes.d.mts +48 -0
- package/types/engine/site-build.d.mts +214 -0
- package/types/engine/site-index.d.mts +141 -0
- package/types/engine/web-wikilinks.d.mts +67 -0
- package/types/engine/wikilink-syntax.d.mts +101 -0
- package/types/engine/wikilinks.d.mts +172 -0
- package/types/index.d.mts +3 -0
- package/types/manifest.d.mts +52 -4
- package/types/markdownlint-config.d.mts +8 -0
- package/types/prettier-config.d.mts +1 -0
- package/types/sohl/actors.d.mts +65 -0
- package/types/sohl/affiliation-standings.d.mts +22 -0
- package/types/sohl/being-info.d.mts +78 -0
- package/types/sohl/default-item-art.d.mts +28 -0
- package/types/sohl/index.d.mts +7 -0
- package/types/sohl/item-builders.d.mts +11 -0
- package/types/sohl/item-fields.d.mts +20 -0
- package/types/sohl/items.d.mts +19 -0
- package/types/sohl/kb-passes.d.mts +71 -0
- package/types/sohl/note-schemas.d.mts +7 -0
|
@@ -0,0 +1,173 @@
|
|
|
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
|
+
* **A being's info-block fields**, derived from the items it embeds.
|
|
16
|
+
*
|
|
17
|
+
* A `being` note carries its embedded documents as `sohl.items` — a flat list
|
|
18
|
+
* of `{ shortcode, type, system? }` — but the shared theme's sidebar reads
|
|
19
|
+
* *resolved* shapes: a `skills` map, `gear` grouped by kind, and `spells` /
|
|
20
|
+
* `talents` split out of the mystical abilities. This is the translation
|
|
21
|
+
* between the two, and it is SoHL data-model knowledge: which item type is a
|
|
22
|
+
* skill, where a mastery level lives, what distinguishes a spell from a talent.
|
|
23
|
+
*
|
|
24
|
+
* **It lives here because it was living in two places.** Both
|
|
25
|
+
* `Song-of-Heroic-Lands-FoundryVTT` and `sohl-thalorna` carried a copy, and the
|
|
26
|
+
* copies drifted: SoHL's caller still gated the derivation on `character` and
|
|
27
|
+
* `creature`, the two types #1580 merged into `being`, so it had matched
|
|
28
|
+
* nothing since the merge and all 95 of its being pages published with empty
|
|
29
|
+
* sidebar sections (SoHL#1696). thalorna's copy checked `being` and was right.
|
|
30
|
+
* Nothing failed in either repository; the pages built and shipped.
|
|
31
|
+
*
|
|
32
|
+
* {@link isBeing} exists for that reason. The bug was not in the derivation —
|
|
33
|
+
* it was in each caller's idea of what a being *is*, written out per repository
|
|
34
|
+
* where it could rot independently. One definition, imported.
|
|
35
|
+
*
|
|
36
|
+
* @module
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The note `type` whose pages carry a being info block.
|
|
41
|
+
*
|
|
42
|
+
* One name, since #1580 merged `character` and `creature` into the `being` they
|
|
43
|
+
* had always compiled into. The retired names are deliberately **not** accepted
|
|
44
|
+
* as aliases: they throw elsewhere in the system, and tolerating them here
|
|
45
|
+
* would hide the next drift of this kind rather than surface it.
|
|
46
|
+
*/
|
|
47
|
+
export const BEING_TYPE = "being";
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Whether a note's frontmatter describes a being.
|
|
51
|
+
*
|
|
52
|
+
* @param {{type?: unknown}|null|undefined} fm - A note's frontmatter.
|
|
53
|
+
* @returns {boolean} `true` when the note is a being.
|
|
54
|
+
*/
|
|
55
|
+
export function isBeing(fm) {
|
|
56
|
+
return Boolean(fm) && fm.type === BEING_TYPE;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The sidebar group each gear item type is displayed under.
|
|
61
|
+
*
|
|
62
|
+
* Presentation naming, not data-model naming: the model says `weapongear`, the
|
|
63
|
+
* sidebar heading says "weapons". Kept as one table so a new gear type is added
|
|
64
|
+
* in a single place rather than in each consumer's site build.
|
|
65
|
+
*
|
|
66
|
+
* @type {Readonly<Record<string, string>>}
|
|
67
|
+
*/
|
|
68
|
+
export const GEAR_TYPE_TO_KEY = Object.freeze({
|
|
69
|
+
weapongear: "weapons",
|
|
70
|
+
armorgear: "armor",
|
|
71
|
+
projectilegear: "projectiles",
|
|
72
|
+
miscgear: "misc",
|
|
73
|
+
containergear: "containers",
|
|
74
|
+
concoctiongear: "concoctions",
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
/** Whether a value is a plain mapping. */
|
|
78
|
+
const isMap = (v) => Boolean(v) && typeof v === "object" && !Array.isArray(v);
|
|
79
|
+
|
|
80
|
+
/** Whether a value is a non-empty array. */
|
|
81
|
+
const nonEmpty = (v) => Array.isArray(v) && v.length > 0;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Derive a being's info-block fields from its raw `sohl.items[]`.
|
|
85
|
+
*
|
|
86
|
+
* Each item's `shortcode` is resolved against `index` — keyed
|
|
87
|
+
* `"<type>:<shortcode>"` — for a display name and a link to the item's own
|
|
88
|
+
* page. `attributes` already match the sidebar shape and pass through
|
|
89
|
+
* untouched.
|
|
90
|
+
*
|
|
91
|
+
* **Authored values win.** Only fields the author did not supply are derived,
|
|
92
|
+
* so a note that hand-writes `sohl.skills` keeps exactly what it wrote. An
|
|
93
|
+
* item's inline `name` beats the index, and an unresolved shortcode falls back
|
|
94
|
+
* to *itself* rather than being dropped — a page that names an item the index
|
|
95
|
+
* has not heard of is better than a page silently missing a row.
|
|
96
|
+
*
|
|
97
|
+
* Returns a new object; the input is not mutated.
|
|
98
|
+
*
|
|
99
|
+
* @param {object|null|undefined} sohl - The note's `sohl` frontmatter block.
|
|
100
|
+
* @param {Map<string, {name?: string, url?: string}>} index - Content index,
|
|
101
|
+
* `"<type>:<shortcode>"` → the item's page.
|
|
102
|
+
* @returns {object|null|undefined} The block with its info-block fields filled
|
|
103
|
+
* in, or the input unchanged when there is nothing to derive from.
|
|
104
|
+
*/
|
|
105
|
+
export function deriveBeingInfo(sohl, index) {
|
|
106
|
+
if (!isMap(sohl)) return sohl;
|
|
107
|
+
const out = { ...sohl };
|
|
108
|
+
const items = Array.isArray(out.items) ? out.items : [];
|
|
109
|
+
if (items.length === 0) return out;
|
|
110
|
+
|
|
111
|
+
const lookup = (type, shortcode) =>
|
|
112
|
+
shortcode ? index.get(`${type}:${shortcode}`) : undefined;
|
|
113
|
+
|
|
114
|
+
/** An item's display name: its own, then the index's, then its shortcode. */
|
|
115
|
+
const displayName = (it, ref, shortcode) =>
|
|
116
|
+
(typeof it.name === "string" && it.name) || ref?.name || shortcode;
|
|
117
|
+
|
|
118
|
+
// Skills: { shortcode: masteryLevelBase }.
|
|
119
|
+
if (!(isMap(out.skills) && Object.keys(out.skills).length > 0)) {
|
|
120
|
+
const skills = {};
|
|
121
|
+
for (const it of items) {
|
|
122
|
+
if (!isMap(it) || it.type !== "skill") continue;
|
|
123
|
+
const level = it.system?.masteryLevelBase;
|
|
124
|
+
if (typeof it.shortcode === "string" && typeof level === "number") {
|
|
125
|
+
skills[it.shortcode] = level;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (Object.keys(skills).length > 0) out.skills = skills;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Gear: { weapons: [{ name, shortcode?, url? }], armor: [...], … }.
|
|
132
|
+
if (!isMap(out.gear)) {
|
|
133
|
+
const gear = {};
|
|
134
|
+
for (const it of items) {
|
|
135
|
+
if (!isMap(it)) continue;
|
|
136
|
+
const key = GEAR_TYPE_TO_KEY[it.type];
|
|
137
|
+
if (!key) continue;
|
|
138
|
+
const shortcode =
|
|
139
|
+
typeof it.shortcode === "string" ? it.shortcode : undefined;
|
|
140
|
+
const ref = lookup(it.type, shortcode);
|
|
141
|
+
const name = displayName(it, ref, shortcode);
|
|
142
|
+
if (!name) continue;
|
|
143
|
+
const entry = { name };
|
|
144
|
+
if (shortcode) entry.shortcode = shortcode;
|
|
145
|
+
if (ref?.url) entry.url = ref.url;
|
|
146
|
+
(gear[key] ??= []).push(entry);
|
|
147
|
+
}
|
|
148
|
+
if (Object.keys(gear).length > 0) out.gear = gear;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Mystical abilities, split by subType into spells / talents.
|
|
152
|
+
const spells = [];
|
|
153
|
+
const talents = [];
|
|
154
|
+
for (const it of items) {
|
|
155
|
+
if (!isMap(it) || it.type !== "mysticalability") continue;
|
|
156
|
+
const shortcode =
|
|
157
|
+
typeof it.shortcode === "string" ? it.shortcode : undefined;
|
|
158
|
+
const ref = lookup("mysticalability", shortcode);
|
|
159
|
+
// No shortcode fallback here: an ability with neither an inline name
|
|
160
|
+
// nor an index entry has nothing to show, and a row reading like a
|
|
161
|
+
// shortcode is worse than no row.
|
|
162
|
+
const name = (typeof it.name === "string" && it.name) || ref?.name;
|
|
163
|
+
if (!name) continue;
|
|
164
|
+
const entry = { name };
|
|
165
|
+
if (ref?.url) entry.url = ref.url;
|
|
166
|
+
if (it.subType === "arcaneincantation") spells.push(entry);
|
|
167
|
+
else if (it.subType === "arcanetalent") talents.push(entry);
|
|
168
|
+
}
|
|
169
|
+
if (spells.length > 0 && !nonEmpty(out.spells)) out.spells = spells;
|
|
170
|
+
if (talents.length > 0 && !nonEmpty(out.talents)) out.talents = talents;
|
|
171
|
+
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
@@ -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
|
+
* Default item artwork, keyed by item `type` — the single source of truth for
|
|
16
|
+
* both the compendium builder (the items pack compiler, at build time) and the
|
|
17
|
+
* runtime `SohlItem.getDefaultArtwork` override (fresh, ad-hoc-created items).
|
|
18
|
+
*
|
|
19
|
+
* Foundry's `Item.DEFAULT_ICON` is the white `icons/svg/item-bag.svg`, which is
|
|
20
|
+
* invisible on the light Manuscript sheet and does not adapt to theme; every
|
|
21
|
+
* SoHL item type instead defaults to a themed `systems/sohl/assets/icons/**`
|
|
22
|
+
* SVG here (dark ink in light mode, cream in dark, via the build-time
|
|
23
|
+
* adaptive-fill injection in `utils/svg-theme.mjs`).
|
|
24
|
+
*
|
|
25
|
+
* This module is deliberately **plain ESM** — no TypeScript, no `@src` aliases,
|
|
26
|
+
* no Foundry — precisely so the plain-`node` pack scripts *and* the bundled TS
|
|
27
|
+
* runtime can both import it. A `.ts` module could not be required by the build
|
|
28
|
+
* scripts (they run under bare `node`, without the bundler that resolves the
|
|
29
|
+
* `@src` alias and strips types). Keeping one map here is what prevents the
|
|
30
|
+
* build-time and runtime defaults from drifting apart (issue #932 was exactly
|
|
31
|
+
* that drift: the builder had a default, runtime did not).
|
|
32
|
+
*
|
|
33
|
+
* It lives in this package rather than in the system's `src/` tree because the
|
|
34
|
+
* pack pipeline is installed as a dependency (#1501): a relative path out of
|
|
35
|
+
* the package would resolve to garbage once the pipeline runs from
|
|
36
|
+
* `node_modules`. The runtime imports it back through the package's
|
|
37
|
+
* `./sohl/default-item-art` entry point, so there is still exactly one map
|
|
38
|
+
* (#1510). Injecting the map through configuration instead would re-open #932,
|
|
39
|
+
* because the two sides would once more be free to disagree.
|
|
40
|
+
*
|
|
41
|
+
* Paths are fully resolved (`systems/sohl/...`) — the served path both layers
|
|
42
|
+
* need. If a new item type is added, add its default here.
|
|
43
|
+
*/
|
|
44
|
+
export const DEFAULT_ITEM_ART = {
|
|
45
|
+
affiliation: "systems/sohl/assets/icons/noun/shield.svg",
|
|
46
|
+
affliction: "systems/sohl/assets/icons/other/sick.svg",
|
|
47
|
+
armorgear: "systems/sohl/assets/icons/game-icons/lorc/breastplate.svg",
|
|
48
|
+
attribute: "systems/sohl/assets/icons/other/charm.svg",
|
|
49
|
+
concoctiongear: "systems/sohl/assets/icons/game-icons/badges/flask.svg",
|
|
50
|
+
containergear: "systems/sohl/assets/icons/other/sack.svg",
|
|
51
|
+
miscgear: "systems/sohl/assets/icons/other/question-mark.svg",
|
|
52
|
+
mystery: "systems/sohl/assets/icons/other/sparkles.svg",
|
|
53
|
+
mysticalability: "systems/sohl/assets/icons/other/hand-sparkles.svg",
|
|
54
|
+
projectilegear: "systems/sohl/assets/icons/noun/arrow.svg",
|
|
55
|
+
skill: "systems/sohl/assets/icons/other/head-gear.svg",
|
|
56
|
+
trauma: "systems/sohl/assets/icons/other/injury.svg",
|
|
57
|
+
weapongear: "systems/sohl/assets/icons/other/sword.svg",
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The default art path for an item `type`, or throw when the type is unknown —
|
|
62
|
+
* the build's fail-fast contract, so an unrecognized type is never silently
|
|
63
|
+
* defaulted (aborting the pack build rather than shipping a mismatched icon).
|
|
64
|
+
*
|
|
65
|
+
* Runtime callers that must tolerate unknown or `base`-typed items should read
|
|
66
|
+
* {@link DEFAULT_ITEM_ART} directly and fall back to Foundry's default instead
|
|
67
|
+
* of calling this — see `SohlItem.getDefaultArtwork`.
|
|
68
|
+
*
|
|
69
|
+
* @param {string} type - the item type.
|
|
70
|
+
* @returns {string} the default image path for that type.
|
|
71
|
+
*/
|
|
72
|
+
export function defaultItemArt(type) {
|
|
73
|
+
if (!(type in DEFAULT_ITEM_ART)) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`No default art for item type "${type}" — add one to @heroiclands/package-build/sohl/default-item-art`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
// The map's inferred type has no index signature — deliberately, so callers
|
|
79
|
+
// reading it directly get the precise key set. The `in` guard above has
|
|
80
|
+
// already proved this key is present.
|
|
81
|
+
return /** @type {Record<string, string>} */ (DEFAULT_ITEM_ART)[type];
|
|
82
|
+
}
|
package/sohl/index.mjs
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
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 SoHL-specific half of the toolchain: the knowledge of the Song of Heroic
|
|
16
|
+
* Lands data model that a generic content module must never receive.
|
|
17
|
+
*
|
|
18
|
+
* The item-type registry and its builders, the items and actors compilers, the
|
|
19
|
+
* default-art map, and the affiliation standings live here (#1512). Nothing in
|
|
20
|
+
* `@heroiclands/package-build/engine` exports any of it, so an adventure module
|
|
21
|
+
* that builds journals, macros, and scenes never receives `buildWeaponGear`.
|
|
22
|
+
*
|
|
23
|
+
* A consuming repository hands its own registry to the engine as configuration
|
|
24
|
+
* — `itemBuilders` in `package-build.config.yaml` — which is how the engine
|
|
25
|
+
* composes the one doc-carrying-type set without holding any package's data
|
|
26
|
+
* model.
|
|
27
|
+
*
|
|
28
|
+
* Namespaced rather than flattened, for the reason the engine barrel gives.
|
|
29
|
+
*
|
|
30
|
+
* @module
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/** The item-type registry: every type that compiles into an Item, and its builder. */
|
|
34
|
+
export * as itemBuilders from "./item-builders.mjs";
|
|
35
|
+
|
|
36
|
+
/** The Item compiler. */
|
|
37
|
+
export * as items from "./items.mjs";
|
|
38
|
+
|
|
39
|
+
/** The Actor compiler. */
|
|
40
|
+
export * as actors from "./actors.mjs";
|
|
41
|
+
|
|
42
|
+
/** This package's own knowledgebase body passes, named from `site.pass`. */
|
|
43
|
+
export * as kbPasses from "./kb-passes.mjs";
|
|
44
|
+
|
|
45
|
+
// Flat as well as namespaced: the Foundry runtime imports these by name through
|
|
46
|
+
// their own entry points, and they were this barrel's surface before the
|
|
47
|
+
// compilers arrived (#1510).
|
|
48
|
+
export { DEFAULT_ITEM_ART, defaultItemArt } from "./default-item-art.mjs";
|
|
49
|
+
export { AFFILIATION_STANDINGS } from "./affiliation-standings.mjs";
|
|
50
|
+
export {
|
|
51
|
+
BEING_TYPE,
|
|
52
|
+
GEAR_TYPE_TO_KEY,
|
|
53
|
+
deriveBeingInfo,
|
|
54
|
+
isBeing,
|
|
55
|
+
} from "./being-info.mjs";
|
|
@@ -0,0 +1,114 @@
|
|
|
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 item-type registry** — every content type that compiles into a Foundry
|
|
16
|
+
* Item, keyed to the builder that produces its `system` block.
|
|
17
|
+
*
|
|
18
|
+
* There is one list, not two. This repository hands {@link ITEM_BUILDERS} to
|
|
19
|
+
* the build as `itemBuilders` in `package-build.config.yaml`, and `itemTypes()`
|
|
20
|
+
* — the whitelist — is derived from that table's own keys, so a type cannot be
|
|
21
|
+
* whitelisted for compilation without a builder to compile it. Previously the
|
|
22
|
+
* whitelist and the builder table were maintained by hand and had already
|
|
23
|
+
* drifted: `trait` — an item type **retired in #651**, absent from
|
|
24
|
+
* `documentTypes.Item` and reported by world migration as unrecognized — was
|
|
25
|
+
* still advertised as compilable, so a `type: trait` note passed the gate and
|
|
26
|
+
* then died on `BUILDERS[type] is not a function`, swallowed as a per-file
|
|
27
|
+
* error (#1504).
|
|
28
|
+
*
|
|
29
|
+
* **The builders are generated, not written.** Each type's `system` builder
|
|
30
|
+
* comes from its field declaration in `item-fields.mjs` by way of
|
|
31
|
+
* {@link buildFromFields}, so the vocabulary a note may write is readable data
|
|
32
|
+
* rather than statements buried in a function body — which is what lets the
|
|
33
|
+
* authoring reference be generated and a note be linted against its type
|
|
34
|
+
* (#22). One consequence worth naming: the registry's keys are now
|
|
35
|
+
* {@link ITEM_FIELDS}'s keys, so adding an item type is one edit *there* (plus
|
|
36
|
+
* its `documentTypes.Item` declaration and its default art), and removing one
|
|
37
|
+
* is likewise a single deletion.
|
|
38
|
+
*
|
|
39
|
+
* **A leaf module, and the reason the seam works.** It imports only the pure
|
|
40
|
+
* declaration primitives and the field table — never `helpers.mjs`, and never
|
|
41
|
+
* the resolved configuration. The config file imports *this* module, so a read
|
|
42
|
+
* back out of the configuration here would close a cycle around the config's
|
|
43
|
+
* own evaluation. The table travels into configuration; the engine's
|
|
44
|
+
* `item-registry.mjs` reads it back out and the Item compiler dispatches
|
|
45
|
+
* through that, which is how a consumer's own table is the one its notes
|
|
46
|
+
* compile with (#1563).
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
import { defaultItemArt } from "./default-item-art.mjs";
|
|
50
|
+
import { buildFromFields, readField } from "../engine/field-spec.mjs";
|
|
51
|
+
import { COMBAT_TECHNIQUE_STRIKE_MODE, ITEM_FIELDS } from "./item-fields.mjs";
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The conditionals a flat field list cannot state, keyed by item type.
|
|
55
|
+
*
|
|
56
|
+
* A finalizer runs after the declaration has produced the `system` block, and
|
|
57
|
+
* exists only for a field whose *presence* depends on another field's value.
|
|
58
|
+
* There is exactly one: a combat technique is authored as a `skill` of subtype
|
|
59
|
+
* `combattechnique`, and carries a strike mode that no other skill has.
|
|
60
|
+
*
|
|
61
|
+
* Keeping this list short is the point. Anything expressible as a field
|
|
62
|
+
* declaration is said there, where a generator and a linter can read it; a
|
|
63
|
+
* finalizer is opaque again, so it earns its place one conditional at a time.
|
|
64
|
+
*
|
|
65
|
+
* @type {Readonly<Record<string, (fm: object, out: object) => object>>}
|
|
66
|
+
*/
|
|
67
|
+
const FINALIZERS = Object.freeze({
|
|
68
|
+
skill(fm, out) {
|
|
69
|
+
if (out.subType === "combattechnique") {
|
|
70
|
+
out.strikeMode = readField(COMBAT_TECHNIQUE_STRIKE_MODE, fm);
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Build one registry entry: the declared fields, the builder they generate, and
|
|
78
|
+
* the default art for the type.
|
|
79
|
+
*
|
|
80
|
+
* Reading {@link defaultItemArt} here is what makes the two lists one: this
|
|
81
|
+
* module cannot name a type the art map does not cover, because that function
|
|
82
|
+
* throws and this module evaluates at import. A drift that a test used to catch
|
|
83
|
+
* is unrepresentable.
|
|
84
|
+
*
|
|
85
|
+
* Importing the art map keeps this module a leaf — it is plain data, not the
|
|
86
|
+
* resolved configuration, and the cycle the module note above warns about is
|
|
87
|
+
* only ever closed by reading configuration back out.
|
|
88
|
+
*
|
|
89
|
+
* @param {string} type - The item type, and the art map's key.
|
|
90
|
+
* @returns {Readonly<{system: (fm: object) => object, img: string, fields: readonly object[]}>}
|
|
91
|
+
* The registry entry.
|
|
92
|
+
*/
|
|
93
|
+
function entryFor(type) {
|
|
94
|
+
const fields = ITEM_FIELDS[type];
|
|
95
|
+
const build = buildFromFields(fields);
|
|
96
|
+
const finalize = FINALIZERS[type];
|
|
97
|
+
return Object.freeze({
|
|
98
|
+
system: finalize ? (fm) => finalize(fm, build(fm)) : build,
|
|
99
|
+
img: defaultItemArt(type),
|
|
100
|
+
fields,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Every item type, paired with the builder for its `system` block, the default
|
|
106
|
+
* art for the type, and the frontmatter fields it declares.
|
|
107
|
+
*
|
|
108
|
+
* @type {Readonly<Record<string, Readonly<{system: (fm: object) => object, img: string, fields: readonly object[]}>>>}
|
|
109
|
+
*/
|
|
110
|
+
export const ITEM_BUILDERS = Object.freeze(
|
|
111
|
+
Object.fromEntries(
|
|
112
|
+
Object.keys(ITEM_FIELDS).map((type) => [type, entryFor(type)]),
|
|
113
|
+
),
|
|
114
|
+
);
|