@heroiclands/package-build 8.1.0 → 10.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +748 -0
- package/CONTENT.md +273 -13
- package/bin/content-build.mjs +479 -123
- package/bin/package-build.mjs +27 -69
- package/bin/report.mjs +1 -2
- package/bundle.mjs +2 -10
- package/config.mjs +31 -106
- package/container.mjs +13 -57
- package/content-config.mjs +300 -188
- package/coverage.mjs +14 -55
- package/deploy.mjs +4 -13
- package/docs/content-format.md +1418 -0
- package/e2e.mjs +16 -55
- package/engine/address-charset.mjs +62 -0
- package/engine/address-diff.mjs +1 -4
- package/engine/alias-index.mjs +153 -0
- package/engine/base-compiler.mjs +203 -31
- package/engine/code-fences.mjs +4 -13
- package/engine/compendiums.mjs +13 -37
- package/engine/content-address.mjs +6 -10
- package/engine/content-format-check.mjs +570 -0
- package/engine/content-format.mjs +253 -0
- package/engine/content-links.mjs +144 -99
- package/engine/content-lint.mjs +12 -16
- package/engine/content-slug.mjs +2 -6
- package/engine/content-tables.mjs +26 -79
- package/engine/diagnostics.mjs +37 -15
- package/engine/document-subtypes.mjs +440 -0
- package/engine/field-reference.mjs +6 -20
- package/engine/field-spec.mjs +49 -45
- package/engine/foreign-catalog.mjs +7 -22
- package/engine/foreign-manifests.mjs +1 -4
- package/engine/frontmatter-lint.mjs +347 -43
- package/engine/frontmatter.mjs +3 -8
- package/engine/generate.mjs +36 -20
- package/engine/helpers.mjs +54 -81
- package/engine/homepage.mjs +4 -15
- package/engine/ids.mjs +21 -12
- package/engine/index.mjs +15 -0
- package/engine/item-registry.mjs +72 -9
- package/engine/journals.mjs +4 -14
- package/engine/kb-manifest.mjs +41 -24
- package/engine/macros.mjs +2 -10
- package/engine/manifest-emit.mjs +6 -17
- package/engine/map-notes.mjs +53 -87
- package/engine/note-claims.mjs +383 -0
- package/engine/note-package.mjs +1 -4
- package/engine/note-vocabulary.mjs +678 -0
- package/engine/pack-config.mjs +56 -60
- package/engine/pack-router.mjs +18 -8
- package/engine/prose-config.mjs +20 -4
- package/engine/prose-lint.mjs +61 -17
- package/engine/region-events.mjs +1 -3
- package/engine/retired-fields.mjs +117 -3
- package/engine/scene-levels.mjs +8 -22
- package/engine/scenes.mjs +31 -47
- package/engine/schema-check.mjs +348 -7
- package/engine/schema-extract.mjs +11 -39
- package/engine/site-build.mjs +13 -38
- package/engine/site-index.mjs +40 -35
- package/engine/system-block.mjs +513 -0
- package/engine/web-wikilinks.mjs +115 -92
- package/engine/wikilink-syntax.mjs +30 -0
- package/engine/wikilinks.mjs +147 -183
- package/index.mjs +1 -5
- package/lang.mjs +1 -3
- package/manifest.mjs +10 -37
- package/markdownlint-config.mjs +1 -5
- package/package.json +6 -2
- package/sohl/actors.mjs +251 -68
- package/sohl/being-info.mjs +3 -6
- package/sohl/document-subtypes.mjs +82 -0
- package/sohl/index.mjs +4 -6
- package/sohl/item-builders.mjs +1 -3
- package/sohl/item-fields.mjs +16 -34
- package/sohl/items.mjs +111 -17
- package/sohl/kb-passes.mjs +29 -39
- package/sohl/note-schemas.mjs +11 -7
- package/sohl/skill-base.mjs +7 -23
- package/stage.mjs +3 -13
- package/templates.mjs +4 -15
- package/types/bundle.d.mts +1 -1
- package/types/container.d.mts +2 -2
- package/types/content-config.d.mts +48 -4
- package/types/coverage.d.mts +1 -1
- package/types/e2e.d.mts +4 -4
- package/types/engine/address-charset.d.mts +45 -0
- package/types/engine/alias-index.d.mts +122 -0
- package/types/engine/base-compiler.d.mts +132 -4
- package/types/engine/content-address.d.mts +2 -2
- package/types/engine/content-format-check.d.mts +163 -0
- package/types/engine/content-format.d.mts +101 -0
- package/types/engine/content-links.d.mts +16 -1
- package/types/engine/content-lint.d.mts +6 -0
- package/types/engine/diagnostics.d.mts +29 -0
- package/types/engine/document-subtypes.d.mts +233 -0
- package/types/engine/field-spec.d.mts +76 -23
- package/types/engine/frontmatter-lint.d.mts +47 -2
- package/types/engine/generate.d.mts +15 -2
- package/types/engine/helpers.d.mts +22 -14
- package/types/engine/ids.d.mts +10 -0
- package/types/engine/index.d.mts +5 -0
- package/types/engine/item-registry.d.mts +21 -2
- package/types/engine/kb-manifest.d.mts +35 -8
- package/types/engine/map-notes.d.mts +21 -11
- package/types/engine/note-claims.d.mts +113 -0
- package/types/engine/note-vocabulary.d.mts +251 -0
- package/types/engine/pack-config.d.mts +4 -3
- package/types/engine/pack-router.d.mts +4 -4
- package/types/engine/prose-lint.d.mts +6 -2
- package/types/engine/retired-fields.d.mts +73 -2
- package/types/engine/schema-check.d.mts +182 -0
- package/types/engine/schema-extract.d.mts +1 -1
- package/types/engine/site-index.d.mts +1 -1
- package/types/engine/system-block.d.mts +281 -0
- package/types/engine/web-wikilinks.d.mts +23 -12
- package/types/engine/wikilink-syntax.d.mts +29 -0
- package/types/manifest.d.mts +1 -1
- package/types/sohl/actors.d.mts +62 -6
- package/types/sohl/document-subtypes.d.mts +14 -0
- package/types/sohl/index.d.mts +1 -0
- package/types/sohl/items.d.mts +21 -0
package/engine/journals.mjs
CHANGED
|
@@ -45,13 +45,7 @@
|
|
|
45
45
|
|
|
46
46
|
import log from "loglevel";
|
|
47
47
|
|
|
48
|
-
import {
|
|
49
|
-
sohlField,
|
|
50
|
-
makeId,
|
|
51
|
-
resolveName,
|
|
52
|
-
defaultStats,
|
|
53
|
-
md,
|
|
54
|
-
} from "./helpers.mjs";
|
|
48
|
+
import { sohlField, makeId, resolveName, defaultStats, md } from "./helpers.mjs";
|
|
55
49
|
import { BasePackCompiler } from "./base-compiler.mjs";
|
|
56
50
|
import { anchorPageId } from "./wikilinks.mjs";
|
|
57
51
|
import { hasDocEntry, itemDocEntryId } from "./item-docs.mjs";
|
|
@@ -98,12 +92,10 @@ export function splitPages(body, leadName = "Introduction") {
|
|
|
98
92
|
// An H1 starts a page, as does any heading carrying an `{#slug}`
|
|
99
93
|
// anchor: a Foundry UUID can only address a page, so a linkable
|
|
100
94
|
// section has to be one.
|
|
101
|
-
const headingMatch =
|
|
102
|
-
!inCodeBlock ? line.match(/^\s*(#{1,6})\s+(.+?)\s*#*\s*$/) : null;
|
|
95
|
+
const headingMatch = !inCodeBlock ? line.match(/^\s*(#{1,6})\s+(.+?)\s*#*\s*$/) : null;
|
|
103
96
|
const rawHeading = headingMatch?.[2]?.trim();
|
|
104
97
|
const anchorMatch = rawHeading?.match(/^(.*?)\s*\{#([^}]+)\}\s*$/);
|
|
105
|
-
const startsPage =
|
|
106
|
-
headingMatch && (headingMatch[1].length === 1 || anchorMatch);
|
|
98
|
+
const startsPage = headingMatch && (headingMatch[1].length === 1 || anchorMatch);
|
|
107
99
|
if (startsPage) {
|
|
108
100
|
closeCurrent();
|
|
109
101
|
current = {
|
|
@@ -371,8 +363,6 @@ export class Journals extends BasePackCompiler {
|
|
|
371
363
|
|
|
372
364
|
/** @inheritdoc */
|
|
373
365
|
reportDetail(stats) {
|
|
374
|
-
log.debug(
|
|
375
|
-
`Skipped ${stats.skippedOther} non-doc file(s) (not type:doc)`,
|
|
376
|
-
);
|
|
366
|
+
log.debug(`Skipped ${stats.skippedOther} non-doc file(s) (not type:doc)`);
|
|
377
367
|
}
|
|
378
368
|
}
|
package/engine/kb-manifest.mjs
CHANGED
|
@@ -90,18 +90,47 @@ export function canonicalKey(pkg, type, shortcode) {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
/**
|
|
93
|
-
*
|
|
93
|
+
* How many segments a canonical key has, and therefore how many the reader
|
|
94
|
+
* below counts.
|
|
94
95
|
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
96
|
+
* Named rather than written as a literal because it is the *grammar*, not an
|
|
97
|
+
* implementation detail of one function: it is the number a change to the
|
|
98
|
+
* address form would move, and the thing a reader of that change has to find.
|
|
97
99
|
*
|
|
98
|
-
* @
|
|
99
|
-
|
|
100
|
-
|
|
100
|
+
* @type {number}
|
|
101
|
+
*/
|
|
102
|
+
export const CANONICAL_KEY_SEGMENTS = 3;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Reads a canonical key back into its parts.
|
|
106
|
+
*
|
|
107
|
+
* Parsing is plain positional counting: split on the separator, require
|
|
108
|
+
* {@link CANONICAL_KEY_SEGMENTS} of them, and assign each position its field.
|
|
109
|
+
* **The charset rule is what makes that sound** — every segment is
|
|
110
|
+
* `^[A-Za-z0-9]+$` (`ADDRESS_SEGMENT_PATTERN` in `engine/address-charset.mjs`),
|
|
111
|
+
* so the hyphen is purely a separator and the count alone determines every
|
|
112
|
+
* field. That is enforced at each of the three sources rather than assumed of
|
|
113
|
+
* the data: shortcodes by `content-lint.mjs` (#1397), `contentPackage` by
|
|
114
|
+
* `defineConfig` (#59), and types are bare words. Were any of them free to
|
|
115
|
+
* carry a hyphen, no amount of counting would recover the fields and the reader
|
|
116
|
+
* would need a vocabulary to match against instead.
|
|
117
|
+
*
|
|
118
|
+
* **Nothing to read and nothing readable are different answers.** A key that
|
|
119
|
+
* cannot be canonical — `harn-adventures-skill-melee`, four segments — yields
|
|
120
|
+
* `null`, while an absent or blank input yields `undefined`. Both are falsy, so
|
|
121
|
+
* every call site (all of which test the result for truthiness) is unaffected;
|
|
122
|
+
* the distinction is there so a caller reporting "this key is unreadable" can
|
|
123
|
+
* tell that it has a key to report about.
|
|
124
|
+
*
|
|
125
|
+
* @param {unknown} key - A canonical key, or nothing.
|
|
126
|
+
* @returns {{package: string, type: string, shortcode: string}|null|undefined}
|
|
127
|
+
* The parts; `null` when there is a string that is not in canonical form;
|
|
128
|
+
* `undefined` when there is no key at all.
|
|
101
129
|
*/
|
|
102
130
|
export function readCanonicalKey(key) {
|
|
131
|
+
if (key == null || key === "") return undefined;
|
|
103
132
|
const parts = String(key).split("-");
|
|
104
|
-
if (parts.length !==
|
|
133
|
+
if (parts.length !== CANONICAL_KEY_SEGMENTS) return null;
|
|
105
134
|
const [pkg, type, shortcode] = parts;
|
|
106
135
|
if (!pkg || !type || !shortcode) return null;
|
|
107
136
|
return { package: pkg, type, shortcode };
|
|
@@ -180,9 +209,7 @@ export const PACKAGE_BASE = Object.freeze({
|
|
|
180
209
|
*/
|
|
181
210
|
function checkBase(base, what) {
|
|
182
211
|
if (typeof base !== "string" || !base.endsWith("/")) {
|
|
183
|
-
throw new Error(
|
|
184
|
-
`${what}: package base ${JSON.stringify(base)} must end in a slash`,
|
|
185
|
-
);
|
|
212
|
+
throw new Error(`${what}: package base ${JSON.stringify(base)} must end in a slash`);
|
|
186
213
|
}
|
|
187
214
|
return base;
|
|
188
215
|
}
|
|
@@ -226,8 +253,7 @@ export function resolvePackageUrl(rel, base) {
|
|
|
226
253
|
checkBase(base, "resolvePackageUrl");
|
|
227
254
|
if (typeof rel !== "string" || !rel || rel.startsWith("/")) {
|
|
228
255
|
throw new Error(
|
|
229
|
-
`resolvePackageUrl: ${JSON.stringify(rel)} is not a package-` +
|
|
230
|
-
`relative address`,
|
|
256
|
+
`resolvePackageUrl: ${JSON.stringify(rel)} is not a package-` + `relative address`,
|
|
231
257
|
);
|
|
232
258
|
}
|
|
233
259
|
return `${base}${rel}`;
|
|
@@ -289,8 +315,7 @@ export function buildManifest(pkg, entries, base, foundryPackage) {
|
|
|
289
315
|
// address, so there is no fact being restated, and an anchor is not
|
|
290
316
|
// required to live inside its own entry. Publishing the complete link
|
|
291
317
|
// also keeps the page-id hash out of the published contract entirely.
|
|
292
|
-
if (e.anchors && Object.keys(e.anchors).length)
|
|
293
|
-
entry.anchors = e.anchors;
|
|
318
|
+
if (e.anchors && Object.keys(e.anchors).length) entry.anchors = e.anchors;
|
|
294
319
|
out[e.key ?? canonicalKey(pkg, type, shortcode)] = entry;
|
|
295
320
|
}
|
|
296
321
|
return {
|
|
@@ -327,12 +352,7 @@ export function writeManifests(entriesByPackage, dir, bases, foundryPackages) {
|
|
|
327
352
|
fs.mkdirSync(dir, { recursive: true });
|
|
328
353
|
const written = [];
|
|
329
354
|
for (const [pkg, entries] of entriesByPackage) {
|
|
330
|
-
const doc = buildManifest(
|
|
331
|
-
pkg,
|
|
332
|
-
entries,
|
|
333
|
-
bases?.[pkg],
|
|
334
|
-
foundryPackages?.[pkg],
|
|
335
|
-
);
|
|
355
|
+
const doc = buildManifest(pkg, entries, bases?.[pkg], foundryPackages?.[pkg]);
|
|
336
356
|
const file = path.join(dir, `${pkg}.json`);
|
|
337
357
|
fs.writeFileSync(file, `${JSON.stringify(doc, null, 2)}\n`);
|
|
338
358
|
written.push({
|
|
@@ -430,10 +450,7 @@ export function loadForeignManifests(dir, localPackages, bases = PACKAGE_BASE) {
|
|
|
430
450
|
// Absent for an entry with no page. A consumer must
|
|
431
451
|
// tolerate that rather than invent an href, exactly as
|
|
432
452
|
// it already tolerates an entry with no `uuid`.
|
|
433
|
-
url:
|
|
434
|
-
v.path == null ?
|
|
435
|
-
undefined
|
|
436
|
-
: resolvePackageUrl(v.path, base),
|
|
453
|
+
url: v.path == null ? undefined : resolvePackageUrl(v.path, base),
|
|
437
454
|
uuid: v.uuid,
|
|
438
455
|
doc: v.doc,
|
|
439
456
|
anchors: v.anchors,
|
package/engine/macros.mjs
CHANGED
|
@@ -51,12 +51,7 @@
|
|
|
51
51
|
|
|
52
52
|
import log from "loglevel";
|
|
53
53
|
|
|
54
|
-
import {
|
|
55
|
-
sohlField,
|
|
56
|
-
resolveName,
|
|
57
|
-
resolveImg,
|
|
58
|
-
defaultStats,
|
|
59
|
-
} from "./helpers.mjs";
|
|
54
|
+
import { sohlField, resolveName, resolveImg, defaultStats } from "./helpers.mjs";
|
|
60
55
|
import { BasePackCompiler } from "./base-compiler.mjs";
|
|
61
56
|
import { splitPages } from "./journals.mjs";
|
|
62
57
|
|
|
@@ -256,10 +251,7 @@ export function resolveMacroScope(fm, label) {
|
|
|
256
251
|
* @returns {MacroDocument} The Macro document.
|
|
257
252
|
* @throws {Error} When the frontmatter's macro type or scope is unusable.
|
|
258
253
|
*/
|
|
259
|
-
export function buildMacroEntry(
|
|
260
|
-
fm,
|
|
261
|
-
{ command, folder = null, stats = defaultStats() },
|
|
262
|
-
) {
|
|
254
|
+
export function buildMacroEntry(fm, { command, folder = null, stats = defaultStats() }) {
|
|
263
255
|
const name = resolveName(fm);
|
|
264
256
|
const id = fm.id;
|
|
265
257
|
return {
|
package/engine/manifest-emit.mjs
CHANGED
|
@@ -142,11 +142,7 @@ export function entriesForNote(fm, name, address, body, ctx) {
|
|
|
142
142
|
: undefined;
|
|
143
143
|
|
|
144
144
|
if (hasDocEntry(fm.type)) {
|
|
145
|
-
const docKey = canonicalKey(
|
|
146
|
-
contentPackage,
|
|
147
|
-
`doc${fm.type}`,
|
|
148
|
-
fm.shortcode,
|
|
149
|
-
);
|
|
145
|
+
const docKey = canonicalKey(contentPackage, `doc${fm.type}`, fm.shortcode);
|
|
150
146
|
const docEntryId = fm.id ? itemDocEntryId(fm.id) : undefined;
|
|
151
147
|
const docUuid = uuidFor("doc", docEntryId);
|
|
152
148
|
return [
|
|
@@ -166,10 +162,7 @@ export function entriesForNote(fm, name, address, body, ctx) {
|
|
|
166
162
|
// documentation, so both addresses resolve to the same URL.
|
|
167
163
|
url,
|
|
168
164
|
uuid: docUuid,
|
|
169
|
-
anchors:
|
|
170
|
-
docUuid ?
|
|
171
|
-
anchorsOf(docUuid, docEntryId, body ?? "", name)
|
|
172
|
-
: undefined,
|
|
165
|
+
anchors: docUuid ? anchorsOf(docUuid, docEntryId, body ?? "", name) : undefined,
|
|
173
166
|
},
|
|
174
167
|
];
|
|
175
168
|
}
|
|
@@ -184,10 +177,7 @@ export function entriesForNote(fm, name, address, body, ctx) {
|
|
|
184
177
|
name,
|
|
185
178
|
url,
|
|
186
179
|
uuid: own,
|
|
187
|
-
anchors:
|
|
188
|
-
own && fm.type === "doc" ?
|
|
189
|
-
anchorsOf(own, fm.id, body ?? "", name)
|
|
190
|
-
: undefined,
|
|
180
|
+
anchors: own && fm.type === "doc" ? anchorsOf(own, fm.id, body ?? "", name) : undefined,
|
|
191
181
|
},
|
|
192
182
|
];
|
|
193
183
|
}
|
|
@@ -221,10 +211,9 @@ export function collectManifestEntries(contentBase, ctx) {
|
|
|
221
211
|
// note yields two entries, so reporting one as the other overstates how
|
|
222
212
|
// much of the tree is published.
|
|
223
213
|
let notes = 0;
|
|
224
|
-
for (const { frontmatter: fm, body, absPath } of walkMarkdownTree(
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
)) {
|
|
214
|
+
for (const { frontmatter: fm, body, absPath } of walkMarkdownTree(contentBase, {
|
|
215
|
+
skipDirectories: ctx.skipDirectories,
|
|
216
|
+
})) {
|
|
228
217
|
if (!fm) continue;
|
|
229
218
|
const rel = path.relative(contentBase, absPath);
|
|
230
219
|
assertNoDeclaredPackage(fm, {
|
package/engine/map-notes.mjs
CHANGED
|
@@ -51,14 +51,14 @@
|
|
|
51
51
|
|
|
52
52
|
import crypto from "crypto";
|
|
53
53
|
|
|
54
|
-
import { compendiumUuid, makeId, MAP_TYPES } from "./ids.mjs";
|
|
54
|
+
import { compendiumUuid, makeId, MAP_SUBTYPES, MAP_TYPES } from "./ids.mjs";
|
|
55
55
|
// The curated region-event vocabulary is shared verbatim with the runtime
|
|
56
56
|
// bridge (`SohlRegionTriggerBehavior`), so an event this build accepts is
|
|
57
57
|
// exactly one the bridge forwards.
|
|
58
|
-
import {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
} from "./
|
|
58
|
+
import { CURATED_REGION_EVENTS, EXCLUDED_REGION_EVENTS } from "./region-events.mjs";
|
|
59
|
+
// A map's background art is `img`, as every other note type's art is; `image`
|
|
60
|
+
// is the retired spelling, still read through the retirement window (#142).
|
|
61
|
+
import { readAliasedField } from "./retired-fields.mjs";
|
|
62
62
|
|
|
63
63
|
/* -------------------------------------------------------------------- */
|
|
64
64
|
/* Note types and their canvas profiles */
|
|
@@ -66,7 +66,7 @@ import {
|
|
|
66
66
|
|
|
67
67
|
// The set itself lives in `ids.mjs`, a leaf both this module and the
|
|
68
68
|
// doc-carrying type set in `item-docs.mjs` can depend on without a cycle.
|
|
69
|
-
export { MAP_TYPES };
|
|
69
|
+
export { MAP_SUBTYPES, MAP_TYPES };
|
|
70
70
|
|
|
71
71
|
/**
|
|
72
72
|
* Whether a content note's type compiles into a Scene.
|
|
@@ -79,7 +79,7 @@ export function isMapType(type) {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
/**
|
|
82
|
-
* Per-
|
|
82
|
+
* Per-subtype canvas defaults, emitted **explicitly** on every scene.
|
|
83
83
|
*
|
|
84
84
|
* This is not a convenience. `grid.type`, `grid.distance` and `grid.units` all
|
|
85
85
|
* declare `initial: () => game.system.grid.*`, and there is no `game` at build
|
|
@@ -89,7 +89,7 @@ export function isMapType(type) {
|
|
|
89
89
|
*
|
|
90
90
|
* @type {Readonly<Record<string, object>>}
|
|
91
91
|
*/
|
|
92
|
-
export const
|
|
92
|
+
export const MAP_SUBTYPE_PROFILES = Object.freeze({
|
|
93
93
|
battlemap: Object.freeze({
|
|
94
94
|
grid: { type: 1 /* SQUARE */, distance: 5, units: "ft" },
|
|
95
95
|
tokenVision: true,
|
|
@@ -111,18 +111,23 @@ export const MAP_TYPE_PROFILES = Object.freeze({
|
|
|
111
111
|
});
|
|
112
112
|
|
|
113
113
|
/**
|
|
114
|
-
* The canvas profile for a map
|
|
114
|
+
* The canvas profile for a map subType.
|
|
115
115
|
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
116
|
+
* Keyed on the subType rather than the type since #174: every map note is
|
|
117
|
+
* `type: map`, and which canvas it derives is the one thing the three
|
|
118
|
+
* spellings ever decided.
|
|
119
|
+
*
|
|
120
|
+
* @param {string} subType - The note's `subType`.
|
|
121
|
+
* @returns {object} The profile from {@link MAP_SUBTYPE_PROFILES}.
|
|
122
|
+
* @throws {Error} When the subType is not a map subType — the build's
|
|
123
|
+
* fail-fast contract, so a typo never ships a scene with Foundry's own
|
|
124
|
+
* defaults.
|
|
120
125
|
*/
|
|
121
|
-
export function mapProfile(
|
|
122
|
-
const profile =
|
|
126
|
+
export function mapProfile(subType) {
|
|
127
|
+
const profile = MAP_SUBTYPE_PROFILES[String(subType)];
|
|
123
128
|
if (!profile) {
|
|
124
129
|
throw new Error(
|
|
125
|
-
`unknown map
|
|
130
|
+
`unknown map subtype "${subType}" — expected one of ${MAP_SUBTYPES.join(", ")}`,
|
|
126
131
|
);
|
|
127
132
|
}
|
|
128
133
|
return profile;
|
|
@@ -266,10 +271,7 @@ export function assertPixelGeometry(coords, geom) {
|
|
|
266
271
|
* @throws {Error} When the coordinates read as pixels.
|
|
267
272
|
*/
|
|
268
273
|
export function assertGridLocation(at, geom) {
|
|
269
|
-
const [gx, gy] = [
|
|
270
|
-
geom.dimensions[0] / geom.pxPerGrid,
|
|
271
|
-
geom.dimensions[1] / geom.pxPerGrid,
|
|
272
|
-
];
|
|
274
|
+
const [gx, gy] = [geom.dimensions[0] / geom.pxPerGrid, geom.dimensions[1] / geom.pxPerGrid];
|
|
273
275
|
if (at[0] > gx || at[1] > gy) {
|
|
274
276
|
throw new Error(
|
|
275
277
|
`${geom.label}: [${at.join(", ")}] lies outside the map's ` +
|
|
@@ -408,9 +410,7 @@ function buildWall(segment, spec, geom, { sceneId, id }) {
|
|
|
408
410
|
* The shape forms a map note may author. `rect` is the short spelling of
|
|
409
411
|
* `rectangle`, as the design's own examples use.
|
|
410
412
|
*/
|
|
411
|
-
const SHAPE_FORMS = Object.freeze(
|
|
412
|
-
new Set(["rect", "rectangle", "circle", "ellipse", "polygon"]),
|
|
413
|
-
);
|
|
413
|
+
const SHAPE_FORMS = Object.freeze(new Set(["rect", "rectangle", "circle", "ellipse", "polygon"]));
|
|
414
414
|
|
|
415
415
|
/**
|
|
416
416
|
* Compile one authored shape into a Foundry shape record.
|
|
@@ -448,20 +448,14 @@ export function buildShape(spec, geom) {
|
|
|
448
448
|
}
|
|
449
449
|
const raw = spec[form];
|
|
450
450
|
if (!Array.isArray(raw) || raw.some((n) => !Number.isFinite(n))) {
|
|
451
|
-
throw new Error(
|
|
452
|
-
`${geom.label}: ${form} takes a flat list of numbers in pixels`,
|
|
453
|
-
);
|
|
451
|
+
throw new Error(`${geom.label}: ${form} takes a flat list of numbers in pixels`);
|
|
454
452
|
}
|
|
455
453
|
assertPixelGeometry(raw, geom);
|
|
456
454
|
|
|
457
455
|
switch (form) {
|
|
458
456
|
case "rect":
|
|
459
457
|
case "rectangle":
|
|
460
|
-
expectLength(
|
|
461
|
-
raw,
|
|
462
|
-
4,
|
|
463
|
-
`${geom.label}: rectangle is [x, y, width, height]`,
|
|
464
|
-
);
|
|
458
|
+
expectLength(raw, 4, `${geom.label}: rectangle is [x, y, width, height]`);
|
|
465
459
|
return {
|
|
466
460
|
type: "rectangle",
|
|
467
461
|
x: raw[0],
|
|
@@ -506,8 +500,7 @@ export function buildShape(spec, geom) {
|
|
|
506
500
|
// which is a line segment and encloses nothing.
|
|
507
501
|
if (raw.length < 6) {
|
|
508
502
|
throw new Error(
|
|
509
|
-
`${geom.label}: a polygon needs at least 3 points; got ` +
|
|
510
|
-
`${raw.length / 2}`,
|
|
503
|
+
`${geom.label}: a polygon needs at least 3 points; got ` + `${raw.length / 2}`,
|
|
511
504
|
);
|
|
512
505
|
}
|
|
513
506
|
return { type: "polygon", points: [...raw], hole };
|
|
@@ -601,9 +594,7 @@ const BEHAVIOR_SPECS = Object.freeze({
|
|
|
601
594
|
*
|
|
602
595
|
* @type {ReadonlySet<string>}
|
|
603
596
|
*/
|
|
604
|
-
export const REGION_BEHAVIOR_TYPES = Object.freeze(
|
|
605
|
-
new Set(Object.keys(BEHAVIOR_SPECS)),
|
|
606
|
-
);
|
|
597
|
+
export const REGION_BEHAVIOR_TYPES = Object.freeze(new Set(Object.keys(BEHAVIOR_SPECS)));
|
|
607
598
|
|
|
608
599
|
/**
|
|
609
600
|
* Behaviour types a map note may **never** carry, and why.
|
|
@@ -657,8 +648,7 @@ function buildBehavior(key, spec, ctx) {
|
|
|
657
648
|
const [type] = types;
|
|
658
649
|
|
|
659
650
|
const banned = BANNED_REGION_BEHAVIOR_TYPES.get(type);
|
|
660
|
-
if (banned)
|
|
661
|
-
throw new Error(`${label}: "${type}" is not permitted — ${banned}`);
|
|
651
|
+
if (banned) throw new Error(`${label}: "${type}" is not permitted — ${banned}`);
|
|
662
652
|
const behaviorSpec = BEHAVIOR_SPECS[type];
|
|
663
653
|
if (!behaviorSpec) {
|
|
664
654
|
throw new Error(
|
|
@@ -680,13 +670,7 @@ function buildBehavior(key, spec, ctx) {
|
|
|
680
670
|
}
|
|
681
671
|
}
|
|
682
672
|
|
|
683
|
-
const system = compileBehaviorSystem(
|
|
684
|
-
type,
|
|
685
|
-
authored,
|
|
686
|
-
behaviorSpec,
|
|
687
|
-
label,
|
|
688
|
-
ctx,
|
|
689
|
-
);
|
|
673
|
+
const system = compileBehaviorSystem(type, authored, behaviorSpec, label, ctx);
|
|
690
674
|
const id = behaviorDocId(ctx.regionId, key, spec._id);
|
|
691
675
|
const doc = {
|
|
692
676
|
_id: id,
|
|
@@ -730,23 +714,17 @@ function compileBehaviorSystem(type, authored, behaviorSpec, label, ctx) {
|
|
|
730
714
|
const { to, ...rest } = authored;
|
|
731
715
|
return {
|
|
732
716
|
...rest,
|
|
733
|
-
destinations: toList(to).map((addr) =>
|
|
734
|
-
ctx.resolveRegionRef(addr, label),
|
|
735
|
-
),
|
|
717
|
+
destinations: toList(to).map((addr) => ctx.resolveRegionRef(addr, label)),
|
|
736
718
|
};
|
|
737
719
|
}
|
|
738
720
|
case "toggleBehavior": {
|
|
739
721
|
const { enable, disable, ...rest } = authored;
|
|
740
722
|
const out = { ...rest, events: [...authored.events] };
|
|
741
723
|
if (enable) {
|
|
742
|
-
out.enable = toList(enable).map((a) =>
|
|
743
|
-
ctx.resolveBehaviorRef(a, label),
|
|
744
|
-
);
|
|
724
|
+
out.enable = toList(enable).map((a) => ctx.resolveBehaviorRef(a, label));
|
|
745
725
|
}
|
|
746
726
|
if (disable) {
|
|
747
|
-
out.disable = toList(disable).map((a) =>
|
|
748
|
-
ctx.resolveBehaviorRef(a, label),
|
|
749
|
-
);
|
|
727
|
+
out.disable = toList(disable).map((a) => ctx.resolveBehaviorRef(a, label));
|
|
750
728
|
}
|
|
751
729
|
return out;
|
|
752
730
|
}
|
|
@@ -765,9 +743,7 @@ function compileBehaviorSystem(type, authored, behaviorSpec, label, ctx) {
|
|
|
765
743
|
}
|
|
766
744
|
case "applyActiveEffect": {
|
|
767
745
|
return {
|
|
768
|
-
effects: toList(authored.effects).map((a) =>
|
|
769
|
-
ctx.resolveEffectRef(a, label),
|
|
770
|
-
),
|
|
746
|
+
effects: toList(authored.effects).map((a) => ctx.resolveEffectRef(a, label)),
|
|
771
747
|
};
|
|
772
748
|
}
|
|
773
749
|
default:
|
|
@@ -821,13 +797,7 @@ const REGION_VISIBILITY = Object.freeze({
|
|
|
821
797
|
});
|
|
822
798
|
|
|
823
799
|
/** `CONST.EDGE_RESTRICTION_TYPES`. */
|
|
824
|
-
const RESTRICTION_TYPES = Object.freeze([
|
|
825
|
-
"light",
|
|
826
|
-
"darkness",
|
|
827
|
-
"sight",
|
|
828
|
-
"sound",
|
|
829
|
-
"move",
|
|
830
|
-
]);
|
|
800
|
+
const RESTRICTION_TYPES = Object.freeze(["light", "darkness", "sight", "sound", "move"]);
|
|
831
801
|
|
|
832
802
|
/**
|
|
833
803
|
* Compile one authored region into a Region document with its behaviours.
|
|
@@ -917,7 +887,7 @@ export function buildScene(fm, ctx) {
|
|
|
917
887
|
const sohl = fm.sohl ?? {};
|
|
918
888
|
const sceneId = fm.id;
|
|
919
889
|
if (!sceneId) throw new Error("a map note needs an `id`");
|
|
920
|
-
const profile = mapProfile(fm.
|
|
890
|
+
const profile = mapProfile(fm.subType);
|
|
921
891
|
|
|
922
892
|
const dimensions = sohl.dimensions;
|
|
923
893
|
if (
|
|
@@ -925,9 +895,7 @@ export function buildScene(fm, ctx) {
|
|
|
925
895
|
dimensions.length !== 2 ||
|
|
926
896
|
!dimensions.every((n) => Number.isInteger(n) && n > 0)
|
|
927
897
|
) {
|
|
928
|
-
throw new Error(
|
|
929
|
-
"`dimensions` is [width, height] in whole pixels — the map's own size",
|
|
930
|
-
);
|
|
898
|
+
throw new Error("`dimensions` is [width, height] in whole pixels — the map's own size");
|
|
931
899
|
}
|
|
932
900
|
const pxPerGrid = sohl.pxPerGrid;
|
|
933
901
|
if (!Number.isInteger(pxPerGrid) || pxPerGrid <= 0) {
|
|
@@ -936,7 +904,11 @@ export function buildScene(fm, ctx) {
|
|
|
936
904
|
"match the art",
|
|
937
905
|
);
|
|
938
906
|
}
|
|
939
|
-
|
|
907
|
+
// Read from the note rather than from its `sohl:` block: art is not
|
|
908
|
+
// system-specific, so `img` is authored at the top level like every other
|
|
909
|
+
// type's, and `sohlField` honours the block for anything already there.
|
|
910
|
+
const img = readAliasedField(fm, "img");
|
|
911
|
+
if (!img) throw new Error("a map note needs an `img`");
|
|
940
912
|
|
|
941
913
|
const warn = (message) => {
|
|
942
914
|
if (ctx.warnings) ctx.warnings.push(message);
|
|
@@ -961,7 +933,7 @@ export function buildScene(fm, ctx) {
|
|
|
961
933
|
tokenVision: profile.tokenVision,
|
|
962
934
|
fog: { mode: profile.fog.mode },
|
|
963
935
|
initialLevel: DEFAULT_LEVEL_ID,
|
|
964
|
-
levels: [buildLevel(sohl, sceneId)],
|
|
936
|
+
levels: [buildLevel(sohl, sceneId, img)],
|
|
965
937
|
drawings: [],
|
|
966
938
|
tokens: [],
|
|
967
939
|
lights: buildLights(sohl, geom, inner),
|
|
@@ -981,9 +953,7 @@ export function buildScene(fm, ctx) {
|
|
|
981
953
|
|
|
982
954
|
if (ctx.journalEntryId) {
|
|
983
955
|
if (!ctx.packageId) {
|
|
984
|
-
throw new Error(
|
|
985
|
-
"a map note with a journal needs `packageId` in its compile context",
|
|
986
|
-
);
|
|
956
|
+
throw new Error("a map note with a journal needs `packageId` in its compile context");
|
|
987
957
|
}
|
|
988
958
|
// `Scene.journal` is a plain ForeignDocumentField, which
|
|
989
959
|
// `ForeignDocumentField#initialize` unconditionally nulls inside a
|
|
@@ -992,19 +962,14 @@ export function buildScene(fm, ctx) {
|
|
|
992
962
|
// pack needs the flag to find the entry at all.
|
|
993
963
|
scene.journal = ctx.journalEntryId;
|
|
994
964
|
scene.flags[ctx.packageId] = {
|
|
995
|
-
docUuid: compendiumUuid(
|
|
996
|
-
ctx.packageId,
|
|
997
|
-
"doc",
|
|
998
|
-
ctx.journalEntryId,
|
|
999
|
-
ctx.journalPack,
|
|
1000
|
-
),
|
|
965
|
+
docUuid: compendiumUuid(ctx.packageId, "doc", ctx.journalEntryId, ctx.journalPack),
|
|
1001
966
|
};
|
|
1002
967
|
}
|
|
1003
968
|
return scene;
|
|
1004
969
|
}
|
|
1005
970
|
|
|
1006
971
|
/**
|
|
1007
|
-
* Synthesise the scene's single embedded Level from `
|
|
972
|
+
* Synthesise the scene's single embedded Level from `img:` / `overlay:`.
|
|
1008
973
|
*
|
|
1009
974
|
* Authors never write `levels:`. A scene must ship at least one Level — the
|
|
1010
975
|
* client-side `_preCreate` net that would create one does not run for offline
|
|
@@ -1014,16 +979,20 @@ export function buildScene(fm, ctx) {
|
|
|
1014
979
|
*
|
|
1015
980
|
* @param {object} sohl - The note's `sohl:` block.
|
|
1016
981
|
* @param {string} sceneId - The owning scene's `_id`.
|
|
982
|
+
* @param {string} [img] - The background art, already resolved from the note.
|
|
983
|
+
* Passed by {@link buildScene}, which reads it from the note rather than from
|
|
984
|
+
* the block; defaults to whichever spelling the block itself carries, so a
|
|
985
|
+
* direct two-argument call still works (#142).
|
|
1017
986
|
* @returns {object} The Level document, keyed for the pack.
|
|
1018
987
|
*/
|
|
1019
|
-
export function buildLevel(sohl, sceneId) {
|
|
988
|
+
export function buildLevel(sohl, sceneId, img = readAliasedField({ sohl }, "img")) {
|
|
1020
989
|
const level = {
|
|
1021
990
|
_id: DEFAULT_LEVEL_ID,
|
|
1022
991
|
name: sohl.levelName ?? "Ground",
|
|
1023
992
|
elevation: { bottom: 0, top: 20 },
|
|
1024
993
|
background: {
|
|
1025
994
|
color: sohl.backgroundColor ?? "#999999",
|
|
1026
|
-
src:
|
|
995
|
+
src: img,
|
|
1027
996
|
},
|
|
1028
997
|
foreground: { src: sohl.overlay ?? null },
|
|
1029
998
|
sort: 0,
|
|
@@ -1050,9 +1019,7 @@ export function buildWalls(sohl, geom, ctx) {
|
|
|
1050
1019
|
const label = `walls.${key}`;
|
|
1051
1020
|
const segments = toList(spec.segments);
|
|
1052
1021
|
if (!segments.length) {
|
|
1053
|
-
throw new Error(
|
|
1054
|
-
`${label}: a wall group needs at least one segment`,
|
|
1055
|
-
);
|
|
1022
|
+
throw new Error(`${label}: a wall group needs at least one segment`);
|
|
1056
1023
|
}
|
|
1057
1024
|
segments.forEach((segment, i) => {
|
|
1058
1025
|
const id = makeId("scene-wall", `${ctx.sceneId}:${key}:${i}`);
|
|
@@ -1171,8 +1138,7 @@ export function buildSounds(sohl, geom, ctx) {
|
|
|
1171
1138
|
return Object.entries(sohl.sounds ?? {}).map(([key, spec]) => {
|
|
1172
1139
|
const label = `sounds.${key}`;
|
|
1173
1140
|
const [x, y] = requirePosition(spec.position, { ...geom, label });
|
|
1174
|
-
if (!spec.path)
|
|
1175
|
-
throw new Error(`${label}: an ambient sound needs a path`);
|
|
1141
|
+
if (!spec.path) throw new Error(`${label}: an ambient sound needs a path`);
|
|
1176
1142
|
const id = spec._id || makeId("scene-sound", `${ctx.sceneId}:${key}`);
|
|
1177
1143
|
return {
|
|
1178
1144
|
_id: id,
|