@heroiclands/package-build 0.6.1 → 3.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-content-build.md +965 -0
- package/CHANGELOG.md +58 -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,528 @@
|
|
|
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
|
+
* Scenes pack compiler — map notes in `assets/content/` → Foundry `Scene`
|
|
16
|
+
* documents, and the `Adventure` bundles that make their references resolve
|
|
17
|
+
* (issue #1525).
|
|
18
|
+
*
|
|
19
|
+
* The translation itself lives in the framework-free `map-notes.mjs`; this
|
|
20
|
+
* module is the pass that walks the tree, resolves what one note says about
|
|
21
|
+
* another, and writes the JSON the compendium CLI compiles.
|
|
22
|
+
*
|
|
23
|
+
* **Two outputs, for two different jobs.**
|
|
24
|
+
*
|
|
25
|
+
* - The **`scenes` pack** holds every map note's Scene. It is what a wikilink
|
|
26
|
+
* to a map addresses, and what a GM browses.
|
|
27
|
+
* - The **`adventures` pack** holds one Adventure per *place* — a group of map
|
|
28
|
+
* notes sharing a `place:`, defaulting to the note's own shortcode — bundling
|
|
29
|
+
* those scenes with the JournalEntries their prose compiled into. A map with
|
|
30
|
+
* `locations:` **must** be imported this way: `Adventure#importContent`
|
|
31
|
+
* creates with `keepId: true`, and a pin's `Note.entryId` / `pageId`, a
|
|
32
|
+
* `teleportToken` destination and a `toggleBehavior` target are all
|
|
33
|
+
* id-based. Dragged out of the bare `scenes` pack, a pinned scene lands with
|
|
34
|
+
* pins pointing at ids no document in the world carries.
|
|
35
|
+
*
|
|
36
|
+
* **Cross-references are addresses, never UUIDs.** A note says
|
|
37
|
+
* `to: {map: manorsolar, region: stair-landing}`; this pass turns that into
|
|
38
|
+
* `Scene.<id>.Region.<id>` from an index built before any scene is compiled,
|
|
39
|
+
* because every embedded id is derived from the scene id and the authored key
|
|
40
|
+
* rather than stored. It is the same vocabulary the link manifest uses.
|
|
41
|
+
*
|
|
42
|
+
* Not a standalone script — exports the `Scenes` compiler class, imported and
|
|
43
|
+
* driven by `packages/content-build/engine/generate.mjs` (via `npm run build:compiledb`).
|
|
44
|
+
*
|
|
45
|
+
* The walk itself — filtering by package and type, skipping drafts,
|
|
46
|
+
* expanding tables, converting wikilinks, writing the JSON and counting
|
|
47
|
+
* errors — belongs to {@link sohl.utils.packs.BasePackCompiler}; this module
|
|
48
|
+
* states only what makes this pass its own (#1509).
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
import fs from "fs";
|
|
52
|
+
import path from "path";
|
|
53
|
+
import log from "loglevel";
|
|
54
|
+
|
|
55
|
+
import {
|
|
56
|
+
walkMarkdownTree,
|
|
57
|
+
sohlField,
|
|
58
|
+
resolveName,
|
|
59
|
+
slugify,
|
|
60
|
+
defaultStats,
|
|
61
|
+
} from "./helpers.mjs";
|
|
62
|
+
import { BasePackCompiler } from "./base-compiler.mjs";
|
|
63
|
+
import { buildJournalEntry, splitPages, journalPageId } from "./journals.mjs";
|
|
64
|
+
import { compendiumUuid, makeId, packForType } from "./ids.mjs";
|
|
65
|
+
import { packRouter } from "./pack-router.mjs";
|
|
66
|
+
import { contentPackage, foundryPackageId } from "./content-package.mjs";
|
|
67
|
+
import { itemDocEntryId } from "./item-docs.mjs";
|
|
68
|
+
import {
|
|
69
|
+
behaviorDocId,
|
|
70
|
+
buildScene,
|
|
71
|
+
isMapType,
|
|
72
|
+
regionDocId,
|
|
73
|
+
} from "./map-notes.mjs";
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Every SoHL action name this build knows about, for the `action:` warning on a
|
|
77
|
+
* region trigger.
|
|
78
|
+
*
|
|
79
|
+
* Deliberately a **superset**, gathered from the localization keys every
|
|
80
|
+
* intrinsic action's `title:` points at and from the `shortcode:` / `executor:`
|
|
81
|
+
* string literals the action definitions carry. A warning that fires on a real
|
|
82
|
+
* action would be worse than one that misses a typo, so the wider net is the
|
|
83
|
+
* right one: this only has to recognise the names that exist, not enumerate
|
|
84
|
+
* them exactly.
|
|
85
|
+
*
|
|
86
|
+
* @param {string} repoRoot - The repository root.
|
|
87
|
+
* @returns {Set<string>} The known action names.
|
|
88
|
+
*/
|
|
89
|
+
export function collectKnownActionNames(repoRoot) {
|
|
90
|
+
const names = new Set();
|
|
91
|
+
const langFile = path.join(repoRoot, "lang", "en.json");
|
|
92
|
+
if (fs.existsSync(langFile)) {
|
|
93
|
+
const lang = JSON.parse(fs.readFileSync(langFile, "utf8"));
|
|
94
|
+
for (const key of Object.keys(lang)) {
|
|
95
|
+
const at = key.indexOf(".Action.");
|
|
96
|
+
if (at < 0) continue;
|
|
97
|
+
const [leaf] = key.slice(at + ".Action.".length).split(".");
|
|
98
|
+
if (/^[A-Za-z][A-Za-z0-9]*$/.test(leaf)) names.add(leaf);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const srcRoot = path.join(repoRoot, "src");
|
|
102
|
+
const pattern = /(?:shortcode|executor):\s*"([A-Za-z][A-Za-z0-9]*)"/g;
|
|
103
|
+
const walk = (dir) => {
|
|
104
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
105
|
+
const abs = path.join(dir, entry.name);
|
|
106
|
+
if (entry.isDirectory()) walk(abs);
|
|
107
|
+
else if (entry.name.endsWith(".ts")) {
|
|
108
|
+
const text = fs.readFileSync(abs, "utf8");
|
|
109
|
+
for (const m of text.matchAll(pattern)) names.add(m[1]);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
if (fs.existsSync(srcRoot)) walk(srcRoot);
|
|
114
|
+
return names;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Strip the LevelDB keys from a document tree.
|
|
119
|
+
*
|
|
120
|
+
* An Adventure's members are inline source data in a `SetField`, not sublevel
|
|
121
|
+
* documents, so they carry no `_key` — the CLI's hierarchy does not recurse
|
|
122
|
+
* into an adventure, and Foundry's schema has no such field to hold it.
|
|
123
|
+
*
|
|
124
|
+
* @param {*} value - A document, array, or scalar.
|
|
125
|
+
* @returns {*} The same shape with every `_key` removed.
|
|
126
|
+
*/
|
|
127
|
+
function stripKeys(value) {
|
|
128
|
+
if (Array.isArray(value)) return value.map(stripKeys);
|
|
129
|
+
if (value && typeof value === "object") {
|
|
130
|
+
return Object.fromEntries(
|
|
131
|
+
Object.entries(value)
|
|
132
|
+
.filter(([k]) => k !== "_key")
|
|
133
|
+
.map(([k, v]) => [k, stripKeys(v)]),
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
return value;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export class Scenes extends BasePackCompiler {
|
|
140
|
+
static id = "scenes";
|
|
141
|
+
static label = "map";
|
|
142
|
+
|
|
143
|
+
/** @type {string} */
|
|
144
|
+
adventureDir;
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Adventures this pass bundled, for the summary.
|
|
148
|
+
*
|
|
149
|
+
* @type {number}
|
|
150
|
+
*/
|
|
151
|
+
adventureCount = 0;
|
|
152
|
+
|
|
153
|
+
constructor({
|
|
154
|
+
contentBase,
|
|
155
|
+
dest,
|
|
156
|
+
companionDests = {},
|
|
157
|
+
folderResolver = () => null,
|
|
158
|
+
repoRoot = process.cwd(),
|
|
159
|
+
}) {
|
|
160
|
+
super({ contentBase, dest, folderResolver });
|
|
161
|
+
if (!companionDests.adventures) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
"Scenes compiler requires an `adventures` companion destination",
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
Object.defineProperty(this, "adventureDir", {
|
|
167
|
+
value: companionDests.adventures,
|
|
168
|
+
writable: false,
|
|
169
|
+
});
|
|
170
|
+
Object.defineProperty(this, "repoRoot", {
|
|
171
|
+
value: repoRoot,
|
|
172
|
+
writable: false,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* @param {object} fm - The note's frontmatter.
|
|
178
|
+
* @returns {boolean} True for a map note.
|
|
179
|
+
*/
|
|
180
|
+
selects(fm) {
|
|
181
|
+
return isMapType(fm.type);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Collect every map note in the tree, and every item note's Active Effects.
|
|
186
|
+
*
|
|
187
|
+
* @returns {{maps: Array<object>, effectsByAddress: Map<string, object>}}
|
|
188
|
+
*/
|
|
189
|
+
#collect() {
|
|
190
|
+
const maps = [];
|
|
191
|
+
const effectsByAddress = new Map();
|
|
192
|
+
for (const { frontmatter: fm, body, absPath } of walkMarkdownTree(
|
|
193
|
+
this.contentBase,
|
|
194
|
+
)) {
|
|
195
|
+
if (!fm || fm.package !== contentPackage() || !fm.id) continue;
|
|
196
|
+
if (
|
|
197
|
+
fm.shortcode &&
|
|
198
|
+
Array.isArray(fm.effects) &&
|
|
199
|
+
fm.effects.length
|
|
200
|
+
) {
|
|
201
|
+
effectsByAddress.set(`${fm.type}-${fm.shortcode}`, {
|
|
202
|
+
id: fm.id,
|
|
203
|
+
type: fm.type,
|
|
204
|
+
// Where the owning item landed, so a region behaviour's
|
|
205
|
+
// effect reference addresses the right pack when a
|
|
206
|
+
// repository ships several of one type (#1566).
|
|
207
|
+
pack: packRouter().resolveOrNull(
|
|
208
|
+
fm,
|
|
209
|
+
packForType(fm.type).docType,
|
|
210
|
+
),
|
|
211
|
+
effects: fm.effects,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
if (!isMapType(fm.type)) continue;
|
|
215
|
+
if (fm.draft === true) {
|
|
216
|
+
log.debug(`Skipping draft map: ${absPath}`);
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
maps.push({ fm, body, absPath });
|
|
220
|
+
}
|
|
221
|
+
return { maps, effectsByAddress };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Index every map's derived region and behaviour ids, keyed by shortcode.
|
|
226
|
+
*
|
|
227
|
+
* Built before any scene is compiled, because a cross-reference in the
|
|
228
|
+
* first note may address the last. Every id is a pure function of the
|
|
229
|
+
* scene id and the authored key (or an authored `_id`), so this needs
|
|
230
|
+
* nothing but frontmatter.
|
|
231
|
+
*
|
|
232
|
+
* @param {Array<object>} maps - The collected map notes.
|
|
233
|
+
* @returns {Map<string, object>} shortcode → `{sceneId, name, regions}`.
|
|
234
|
+
*/
|
|
235
|
+
#indexMaps(maps) {
|
|
236
|
+
const index = new Map();
|
|
237
|
+
for (const { fm, absPath } of maps) {
|
|
238
|
+
if (!fm.shortcode) {
|
|
239
|
+
throw new Error(`Map note missing shortcode: ${absPath}`);
|
|
240
|
+
}
|
|
241
|
+
if (index.has(fm.shortcode)) {
|
|
242
|
+
throw new Error(
|
|
243
|
+
`Two map notes share the shortcode "${fm.shortcode}"`,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
const regions = new Map();
|
|
247
|
+
for (const [key, spec] of Object.entries(fm.sohl?.regions ?? {})) {
|
|
248
|
+
const id = regionDocId(fm.id, key, spec?._id);
|
|
249
|
+
const behaviors = new Map();
|
|
250
|
+
for (const [bKey, bSpec] of Object.entries(
|
|
251
|
+
spec?.behaviors ?? {},
|
|
252
|
+
)) {
|
|
253
|
+
behaviors.set(bKey, behaviorDocId(id, bKey, bSpec?._id));
|
|
254
|
+
}
|
|
255
|
+
regions.set(key, { id, behaviors });
|
|
256
|
+
}
|
|
257
|
+
index.set(fm.shortcode, {
|
|
258
|
+
sceneId: fm.id,
|
|
259
|
+
name: resolveName(fm),
|
|
260
|
+
regions,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
return index;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Build the three address → UUID resolvers a scene's behaviours need.
|
|
268
|
+
*
|
|
269
|
+
* @param {Map<string, object>} index - From {@link Scenes#indexMaps}.
|
|
270
|
+
* @param {Map<string, object>} effectsByAddress - Item notes with effects.
|
|
271
|
+
* @param {string} selfShortcode - The note being compiled, so `map:` may be
|
|
272
|
+
* omitted for a reference within the same map.
|
|
273
|
+
* @returns {object} `{resolveRegionRef, resolveBehaviorRef, resolveEffectRef}`.
|
|
274
|
+
*/
|
|
275
|
+
#resolvers(index, effectsByAddress, selfShortcode) {
|
|
276
|
+
const lookupRegion = (addr, label) => {
|
|
277
|
+
if (!addr || typeof addr !== "object" || !addr.region) {
|
|
278
|
+
throw new Error(
|
|
279
|
+
`${label}: a cross-reference is {map, region} — the target ` +
|
|
280
|
+
`map's shortcode and the region's key, never a UUID`,
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
const mapKey = addr.map ?? selfShortcode;
|
|
284
|
+
const target = index.get(mapKey);
|
|
285
|
+
if (!target) {
|
|
286
|
+
throw new Error(
|
|
287
|
+
`${label}: no map note has the shortcode "${mapKey}"`,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
const region = target.regions.get(addr.region);
|
|
291
|
+
if (!region) {
|
|
292
|
+
throw new Error(
|
|
293
|
+
`${label}: map "${mapKey}" has no region "${addr.region}" — ` +
|
|
294
|
+
`it has ${[...target.regions.keys()].join(", ") || "none"}`,
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
return { target, region };
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
resolveRegionRef: (addr, label) => {
|
|
302
|
+
const { target, region } = lookupRegion(addr, label);
|
|
303
|
+
return `Scene.${target.sceneId}.Region.${region.id}`;
|
|
304
|
+
},
|
|
305
|
+
resolveBehaviorRef: (addr, label) => {
|
|
306
|
+
const { target, region } = lookupRegion(addr, label);
|
|
307
|
+
if (!addr.behavior) {
|
|
308
|
+
throw new Error(
|
|
309
|
+
`${label}: a behaviour reference is {map, region, behavior}`,
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
const behaviorId = region.behaviors.get(addr.behavior);
|
|
313
|
+
if (!behaviorId) {
|
|
314
|
+
throw new Error(
|
|
315
|
+
`${label}: region "${addr.region}" has no behaviour ` +
|
|
316
|
+
`"${addr.behavior}"`,
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
return (
|
|
320
|
+
`Scene.${target.sceneId}.Region.${region.id}` +
|
|
321
|
+
`.RegionBehavior.${behaviorId}`
|
|
322
|
+
);
|
|
323
|
+
},
|
|
324
|
+
resolveEffectRef: (addr, label) => {
|
|
325
|
+
if (!addr || typeof addr !== "object" || !addr.item) {
|
|
326
|
+
throw new Error(
|
|
327
|
+
`${label}: an effect reference is {item, effect} — the ` +
|
|
328
|
+
`owning item's \`type-shortcode\` address and the ` +
|
|
329
|
+
`effect's name, never a UUID`,
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
const item = effectsByAddress.get(addr.item);
|
|
333
|
+
if (!item) {
|
|
334
|
+
throw new Error(
|
|
335
|
+
`${label}: no content note addressed "${addr.item}" ` +
|
|
336
|
+
`carries Active Effects`,
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
const effect = item.effects.find(
|
|
340
|
+
(e) => e.name === addr.effect || e._id === addr.effect,
|
|
341
|
+
);
|
|
342
|
+
if (!effect?._id) {
|
|
343
|
+
throw new Error(
|
|
344
|
+
`${label}: "${addr.item}" has no Active Effect named ` +
|
|
345
|
+
`"${addr.effect}" with an \`_id\``,
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
return `${compendiumUuid(foundryPackageId(), item.type, item.id, item.pack)}.ActiveEffect.${effect._id}`;
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* The heading keys a map pin may name, mapped to the page each compiled to.
|
|
355
|
+
*
|
|
356
|
+
* A location key matches a heading's `{#anchor}` slug, or the slug of its
|
|
357
|
+
* text. Both passes split the *converted* markdown, so the ids agree with
|
|
358
|
+
* the journals pack without either reading the other's output.
|
|
359
|
+
*
|
|
360
|
+
* @param {string} markdown - The converted body.
|
|
361
|
+
* @param {string} entryId - The JournalEntry's id.
|
|
362
|
+
* @param {string} name - The map's name (its lead page).
|
|
363
|
+
* @returns {Map<string, string>} heading key → page id.
|
|
364
|
+
*/
|
|
365
|
+
#pageIds(markdown, entryId, name) {
|
|
366
|
+
const pageIds = new Map();
|
|
367
|
+
splitPages(markdown, name).forEach((page, index) => {
|
|
368
|
+
const id = journalPageId(entryId, page, index);
|
|
369
|
+
if (page.anchorSlug) pageIds.set(page.anchorSlug, id);
|
|
370
|
+
const slug = slugify(page.name);
|
|
371
|
+
if (slug && !pageIds.has(slug)) pageIds.set(slug, id);
|
|
372
|
+
});
|
|
373
|
+
return pageIds;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Index every cross-reference before the walk: a reference in the first
|
|
378
|
+
* note may address the last, and the effects an authored `to:` names live
|
|
379
|
+
* on item notes this pass does not otherwise read.
|
|
380
|
+
*
|
|
381
|
+
* @returns {Promise<void>}
|
|
382
|
+
*/
|
|
383
|
+
async prepare() {
|
|
384
|
+
await super.prepare();
|
|
385
|
+
const { maps, effectsByAddress } = this.#collect();
|
|
386
|
+
this.index = this.#indexMaps(maps);
|
|
387
|
+
this.effectsByAddress = effectsByAddress;
|
|
388
|
+
this.knownActions = collectKnownActionNames(this.repoRoot);
|
|
389
|
+
/** place key → `{name, img, scenes: [], journal: []}` */
|
|
390
|
+
this.places = new Map();
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Compile one map note into a Scene, and accumulate the place it belongs
|
|
395
|
+
* to — a map's Scene and the JournalEntry its prose compiles into ship
|
|
396
|
+
* together in an Adventure, which is the only import that preserves the
|
|
397
|
+
* ids its pins address.
|
|
398
|
+
*
|
|
399
|
+
* @param {object} fm - The note's frontmatter.
|
|
400
|
+
* @param {string} markdown - The body, tables expanded and wikilinks
|
|
401
|
+
* resolved.
|
|
402
|
+
* @returns {object} The Scene document.
|
|
403
|
+
*/
|
|
404
|
+
compileNote(fm, markdown) {
|
|
405
|
+
const name = resolveName(fm);
|
|
406
|
+
const hasBody = Boolean(String(markdown).trim());
|
|
407
|
+
// The same doc-entry id the journals pass derives, from the
|
|
408
|
+
// shared `docEntryTypes` arrangement (#1514) — so neither
|
|
409
|
+
// pass has to read the other's output.
|
|
410
|
+
const entryId = hasBody ? itemDocEntryId(fm.id) : undefined;
|
|
411
|
+
const folder = this.folderResolver(sohlField(fm, "folder", null));
|
|
412
|
+
const warnings = [];
|
|
413
|
+
const scene = buildScene(fm, {
|
|
414
|
+
packageId: foundryPackageId(),
|
|
415
|
+
name,
|
|
416
|
+
folder,
|
|
417
|
+
stats: defaultStats(),
|
|
418
|
+
journalEntryId: entryId,
|
|
419
|
+
// A map note's prose is a derived JournalEntry: it lands in the
|
|
420
|
+
// default JournalEntry pack, not in whichever Scene pack the map
|
|
421
|
+
// itself was routed to (#1566).
|
|
422
|
+
journalPack: packRouter().defaultOf("JournalEntry"),
|
|
423
|
+
pageIds:
|
|
424
|
+
hasBody ? this.#pageIds(markdown, entryId, name) : new Map(),
|
|
425
|
+
knownActions: this.knownActions,
|
|
426
|
+
warnings,
|
|
427
|
+
...this.#resolvers(this.index, this.effectsByAddress, fm.shortcode),
|
|
428
|
+
});
|
|
429
|
+
for (const message of warnings) {
|
|
430
|
+
// Named by file, like every other note diagnostic (#17). A map
|
|
431
|
+
// warning is about the note's frontmatter, which carries no
|
|
432
|
+
// offset, so it names the file and stops there rather than
|
|
433
|
+
// pointing at a line it cannot establish.
|
|
434
|
+
this.noteWarn(`map "${name}": ${message}`);
|
|
435
|
+
}
|
|
436
|
+
this.writeEntry(scene);
|
|
437
|
+
|
|
438
|
+
// The journal the pins point at, derived here exactly as the
|
|
439
|
+
// journals pass derives it, so the Adventure bundles the same
|
|
440
|
+
// document that pack ships.
|
|
441
|
+
const journal =
|
|
442
|
+
hasBody ?
|
|
443
|
+
buildJournalEntry({
|
|
444
|
+
id: entryId,
|
|
445
|
+
name,
|
|
446
|
+
markdown,
|
|
447
|
+
leadName: name,
|
|
448
|
+
folder: sohlField(fm, "folder", null),
|
|
449
|
+
flags: fm.flags,
|
|
450
|
+
})
|
|
451
|
+
: null;
|
|
452
|
+
|
|
453
|
+
const placeKey = sohlField(fm, "place", null) || fm.shortcode;
|
|
454
|
+
if (!this.places.has(placeKey)) {
|
|
455
|
+
this.places.set(placeKey, {
|
|
456
|
+
key: placeKey,
|
|
457
|
+
name: sohlField(fm, "placeName", null) || name,
|
|
458
|
+
img: sohlField(fm, "image", null),
|
|
459
|
+
pinned: false,
|
|
460
|
+
scenes: [],
|
|
461
|
+
journal: [],
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
const place = this.places.get(placeKey);
|
|
465
|
+
place.scenes.push(stripKeys(scene));
|
|
466
|
+
if (journal) place.journal.push(stripKeys(journal));
|
|
467
|
+
if (Object.keys(fm.sohl?.locations ?? {}).length) {
|
|
468
|
+
place.pinned = true;
|
|
469
|
+
}
|
|
470
|
+
return scene;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Bundle each pinned place into an Adventure, once every map of it has
|
|
475
|
+
* compiled.
|
|
476
|
+
*
|
|
477
|
+
* @returns {Promise<void>}
|
|
478
|
+
*/
|
|
479
|
+
async finish() {
|
|
480
|
+
this.adventureCount = 0;
|
|
481
|
+
for (const place of this.places.values()) {
|
|
482
|
+
// A scene that references nothing ships fine as a plain `scenes`
|
|
483
|
+
// entry; only pins need the id-preserving import an Adventure gives.
|
|
484
|
+
if (!place.pinned) continue;
|
|
485
|
+
this.writeTo(this.adventureDir, this.#buildAdventure(place));
|
|
486
|
+
this.adventureCount++;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/** @inheritdoc */
|
|
491
|
+
reportCompiled(stats) {
|
|
492
|
+
log.info(
|
|
493
|
+
`Compiled ${stats.compiled} scene(s) and ${this.adventureCount} adventure(s)`,
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Bundle one place's scenes and journals into an Adventure.
|
|
499
|
+
*
|
|
500
|
+
* @param {object} place - The accumulated place.
|
|
501
|
+
* @returns {object} The Adventure document, keyed for the pack.
|
|
502
|
+
*/
|
|
503
|
+
#buildAdventure(place) {
|
|
504
|
+
const id = makeId("map-adventure", place.key);
|
|
505
|
+
return {
|
|
506
|
+
name: place.name,
|
|
507
|
+
img: place.img ?? null,
|
|
508
|
+
caption: "",
|
|
509
|
+
description: "",
|
|
510
|
+
actors: [],
|
|
511
|
+
combats: [],
|
|
512
|
+
items: [],
|
|
513
|
+
journal: place.journal,
|
|
514
|
+
scenes: place.scenes,
|
|
515
|
+
tables: [],
|
|
516
|
+
macros: [],
|
|
517
|
+
cards: [],
|
|
518
|
+
playlists: [],
|
|
519
|
+
folders: [],
|
|
520
|
+
folder: null,
|
|
521
|
+
sort: 0,
|
|
522
|
+
flags: {},
|
|
523
|
+
_id: id,
|
|
524
|
+
_stats: defaultStats(),
|
|
525
|
+
_key: `!adventures!${id}`,
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
}
|