@heroiclands/package-build 9.0.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 +694 -0
- package/CONTENT.md +273 -13
- package/bin/content-build.mjs +437 -7
- package/content-config.mjs +259 -28
- package/docs/content-format.md +1418 -0
- package/engine/address-charset.mjs +62 -0
- package/engine/alias-index.mjs +153 -0
- package/engine/base-compiler.mjs +194 -4
- package/engine/content-address.mjs +4 -4
- package/engine/content-format-check.mjs +570 -0
- package/engine/content-format.mjs +253 -0
- package/engine/content-links.mjs +132 -56
- package/engine/content-lint.mjs +8 -1
- package/engine/diagnostics.mjs +33 -0
- package/engine/document-subtypes.mjs +440 -0
- package/engine/field-spec.mjs +49 -43
- package/engine/frontmatter-lint.mjs +343 -27
- package/engine/generate.mjs +32 -4
- package/engine/helpers.mjs +41 -29
- package/engine/ids.mjs +19 -1
- package/engine/index.mjs +15 -0
- package/engine/item-registry.mjs +72 -5
- package/engine/kb-manifest.mjs +36 -7
- package/engine/map-notes.mjs +34 -18
- package/engine/note-claims.mjs +383 -0
- package/engine/note-vocabulary.mjs +678 -0
- package/engine/pack-config.mjs +39 -22
- package/engine/pack-router.mjs +17 -6
- package/engine/prose-lint.mjs +55 -3
- package/engine/retired-fields.mjs +117 -3
- package/engine/scenes.mjs +19 -1
- package/engine/schema-check.mjs +347 -3
- package/engine/site-build.mjs +1 -1
- package/engine/site-index.mjs +38 -21
- package/engine/system-block.mjs +513 -0
- package/engine/web-wikilinks.mjs +112 -80
- package/engine/wikilink-syntax.mjs +30 -0
- package/engine/wikilinks.mjs +67 -51
- package/package.json +6 -2
- package/sohl/actors.mjs +249 -36
- package/sohl/document-subtypes.mjs +82 -0
- package/sohl/index.mjs +3 -0
- package/sohl/items.mjs +110 -14
- package/sohl/note-schemas.mjs +11 -7
- package/types/content-config.d.mts +48 -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 +14 -1
- package/types/engine/helpers.d.mts +21 -13
- 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/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/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/ids.mjs
CHANGED
|
@@ -51,7 +51,18 @@ export function makeId(namespace, value) {
|
|
|
51
51
|
*
|
|
52
52
|
* @type {ReadonlySet<string>}
|
|
53
53
|
*/
|
|
54
|
-
export const MAP_TYPES = Object.freeze(new Set(["
|
|
54
|
+
export const MAP_TYPES = Object.freeze(new Set(["map"]));
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The map subTypes, which differ only in the canvas defaults derived for them.
|
|
58
|
+
*
|
|
59
|
+
* They were three *types* until #174, which cost three entries in the pack
|
|
60
|
+
* router, three in the claims set and three in every consumer's section config
|
|
61
|
+
* — for one idea that the specification had always described as one type.
|
|
62
|
+
*
|
|
63
|
+
* @type {readonly string[]}
|
|
64
|
+
*/
|
|
65
|
+
export const MAP_SUBTYPES = Object.freeze(["battlemap", "localmap", "regionalmap"]);
|
|
55
66
|
|
|
56
67
|
/**
|
|
57
68
|
* Content type → the pack its documents compile into, and the document type
|
|
@@ -95,6 +106,13 @@ export const PACK_BY_TYPE = Object.freeze({
|
|
|
95
106
|
export const RETIRED_TYPES = Object.freeze({
|
|
96
107
|
character: "being",
|
|
97
108
|
creature: "being",
|
|
109
|
+
// The three map spellings, retired in favour of the single `map` whose
|
|
110
|
+
// subType they became (#174). Recorded rather than deleted for the same
|
|
111
|
+
// reason as the two above: an unnamed type falls through to the open item
|
|
112
|
+
// set, so a note or link left on one would be routed to the items pack.
|
|
113
|
+
battlemap: "map",
|
|
114
|
+
localmap: "map",
|
|
115
|
+
regionalmap: "map",
|
|
98
116
|
});
|
|
99
117
|
|
|
100
118
|
/**
|
package/engine/index.mjs
CHANGED
|
@@ -36,6 +36,9 @@
|
|
|
36
36
|
/** Deterministic document ids, the conventional pack map, and compendium UUIDs. */
|
|
37
37
|
export * as ids from "./ids.mjs";
|
|
38
38
|
|
|
39
|
+
/** The per-system frontmatter block, and the declared shared→system mapping. */
|
|
40
|
+
export * as systemBlock from "./system-block.mjs";
|
|
41
|
+
|
|
39
42
|
/** Fenced-code detection, so a rewrite never edits a code block. */
|
|
40
43
|
export * as codeFences from "./code-fences.mjs";
|
|
41
44
|
|
|
@@ -51,6 +54,9 @@ export * as packConfig from "./pack-config.mjs";
|
|
|
51
54
|
/** Which pack of a document type a note's document lands in (#1566). */
|
|
52
55
|
export * as packRouter from "./pack-router.mjs";
|
|
53
56
|
|
|
57
|
+
/** Which note types a configuration compiles, and the notes nothing claims (#146). */
|
|
58
|
+
export * as noteClaims from "./note-claims.mjs";
|
|
59
|
+
|
|
54
60
|
/** The content package a build compiles, and the Foundry package it ships in. */
|
|
55
61
|
export * as contentPackage from "./content-package.mjs";
|
|
56
62
|
|
|
@@ -66,6 +72,9 @@ export * as homepage from "./homepage.mjs";
|
|
|
66
72
|
/** The note types the engine itself declares, whatever a consumer registers. */
|
|
67
73
|
export * as noteSchemas from "./note-schemas.mjs";
|
|
68
74
|
|
|
75
|
+
/** The closed frontmatter regions: the `data:` container and each type's `subType`. */
|
|
76
|
+
export * as noteVocabulary from "./note-vocabulary.mjs";
|
|
77
|
+
|
|
69
78
|
/** The shipped Foundry manifest: locating it, reading it, guarding its id. */
|
|
70
79
|
|
|
71
80
|
/** The URL a content note is published at — the one web-address rule. */
|
|
@@ -104,6 +113,9 @@ export * as helpers from "./helpers.mjs";
|
|
|
104
113
|
/** The consumer's resolved item-type registry: the whitelist and its builders. */
|
|
105
114
|
export * as itemRegistry from "./item-registry.mjs";
|
|
106
115
|
|
|
116
|
+
/** The per-system note-type → document-subtype map, and looking a note up in it. */
|
|
117
|
+
export * as documentSubtypes from "./document-subtypes.mjs";
|
|
118
|
+
|
|
107
119
|
/** Which types carry documentation of their own, and where it is addressed. */
|
|
108
120
|
export * as itemDocs from "./item-docs.mjs";
|
|
109
121
|
|
|
@@ -113,6 +125,9 @@ export * as wikilinks from "./wikilinks.mjs";
|
|
|
113
125
|
/** What a `[[…]]` is, before either resolver decides where it points. */
|
|
114
126
|
export * as wikilinkSyntax from "./wikilink-syntax.mjs";
|
|
115
127
|
|
|
128
|
+
/** The alias namespace: what a note may be called, and who may claim a name. */
|
|
129
|
+
export * as aliasIndex from "./alias-index.mjs";
|
|
130
|
+
|
|
116
131
|
/** The address index a site build resolves its wikilinks against. */
|
|
117
132
|
export * as siteIndex from "./site-index.mjs";
|
|
118
133
|
|
package/engine/item-registry.mjs
CHANGED
|
@@ -18,7 +18,11 @@
|
|
|
18
18
|
* Both are read from the one resolved configuration, so they are literally the
|
|
19
19
|
* same object's keys and values: a type cannot be whitelisted for compilation
|
|
20
20
|
* without the builder that compiles it, which is the guarantee #1504 exists
|
|
21
|
-
* for.
|
|
21
|
+
* for. Where a consumer declares **several** registries, one per system (#58),
|
|
22
|
+
* the vocabulary is their union and every lookup below takes the system that is
|
|
23
|
+
* asking — a type both systems declare has two builders, and answering with one
|
|
24
|
+
* of them because it was declared first is the silent-wrong-output failure this
|
|
25
|
+
* package spends its time removing. The Item compiler dispatches through {@link itemBuilder}, so the table a
|
|
22
26
|
* consumer configured is the table its notes compile with — the whitelist and
|
|
23
27
|
* the dispatch used to come from different places, and a consumer supplying its
|
|
24
28
|
* own registry got the types it asked for and the builders it did not (#1563).
|
|
@@ -60,6 +64,43 @@ export function itemTypes() {
|
|
|
60
64
|
return loadPackConfig().itemTypes;
|
|
61
65
|
}
|
|
62
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Look one type up in the table a system declared, or in the flat one.
|
|
69
|
+
*
|
|
70
|
+
* The flat table is the union with the first declaring registry winning, which
|
|
71
|
+
* is the right answer for every single-system build and no answer at all where
|
|
72
|
+
* two systems declare the type. So a contested type without a system **throws**
|
|
73
|
+
* rather than resolving: the alternative is a document built by one system's
|
|
74
|
+
* builder and stamped with another's, which is exactly the shape of defect the
|
|
75
|
+
* document-subtype map exists to stop (#79).
|
|
76
|
+
*
|
|
77
|
+
* @param {string} what - What is being looked up, for the message.
|
|
78
|
+
* @param {"itemBuilders"|"itemArt"|"itemFields"} table - Which table.
|
|
79
|
+
* @param {string} type - The item type.
|
|
80
|
+
* @param {string} [system] - The system asking, where a build has more than one.
|
|
81
|
+
* @returns {any} The entry, or `undefined`.
|
|
82
|
+
* @throws {Error} When several systems declare the type and none is named.
|
|
83
|
+
*/
|
|
84
|
+
function lookup(what, table, type, system) {
|
|
85
|
+
const config = loadPackConfig();
|
|
86
|
+
if (system !== undefined) {
|
|
87
|
+
const perSystem = /** @type {Record<string, Record<string, unknown>>} */ (
|
|
88
|
+
config[`${table}BySystem`]
|
|
89
|
+
);
|
|
90
|
+
// A consumer declaring one, system-less registry keeps answering for
|
|
91
|
+
// every system: it *is* the only vocabulary in the build.
|
|
92
|
+
if (Object.keys(perSystem).length) return perSystem[system]?.[type];
|
|
93
|
+
} else if (config.itemTypesBySeveralSystems.has(type)) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`More than one declared registry defines the item type "${type}", so ` +
|
|
96
|
+
`the ${what} of a "${type}" depends on which system is asking. ` +
|
|
97
|
+
`Name the system at the call site, or stop declaring the type ` +
|
|
98
|
+
`twice.`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
return /** @type {Record<string, unknown>} */ (config[table])[type];
|
|
102
|
+
}
|
|
103
|
+
|
|
63
104
|
/**
|
|
64
105
|
* The builder the consuming repository registered for an item type.
|
|
65
106
|
*
|
|
@@ -68,11 +109,16 @@ export function itemTypes() {
|
|
|
68
109
|
* failing as an anonymous `is not a function` (#1504).
|
|
69
110
|
*
|
|
70
111
|
* @param {string} type - The note's `type` frontmatter.
|
|
112
|
+
* @param {string} [system] - The system compiling it, where a build declares
|
|
113
|
+
* more than one registry. Omitted, a type only one registry declares still
|
|
114
|
+
* resolves; a contested one throws rather than picking a side.
|
|
71
115
|
* @returns {(fm: object) => object} The builder for that type.
|
|
72
116
|
* @throws {Error} When the configuration registers no builder for `type`.
|
|
73
117
|
*/
|
|
74
|
-
export function itemBuilder(type) {
|
|
75
|
-
const builder = /** @type {
|
|
118
|
+
export function itemBuilder(type, system) {
|
|
119
|
+
const builder = /** @type {Function|undefined} */ (
|
|
120
|
+
lookup("builder", "itemBuilders", type, system)
|
|
121
|
+
);
|
|
76
122
|
if (typeof builder !== "function") {
|
|
77
123
|
throw new Error(
|
|
78
124
|
`No builder registered for item type "${type}" — add one to the ` +
|
|
@@ -83,6 +129,25 @@ export function itemBuilder(type) {
|
|
|
83
129
|
return /** @type {(fm: object) => object} */ (builder);
|
|
84
130
|
}
|
|
85
131
|
|
|
132
|
+
/**
|
|
133
|
+
* The frontmatter fields a type's registry entry declares, if any.
|
|
134
|
+
*
|
|
135
|
+
* Sparse by design: a type whose entry declares none compiles normally and is
|
|
136
|
+
* simply undocumented (#22). What reads it is the `system`-block passthrough,
|
|
137
|
+
* which has to know which paths a declared field already writes before it
|
|
138
|
+
* writes the rest (#58).
|
|
139
|
+
*
|
|
140
|
+
* @param {string} type - The item type.
|
|
141
|
+
* @param {string} [system] - The system compiling it, where a build declares
|
|
142
|
+
* more than one registry.
|
|
143
|
+
* @returns {readonly object[]|undefined} The declaration, or `undefined`.
|
|
144
|
+
*/
|
|
145
|
+
export function itemFields(type, system) {
|
|
146
|
+
return /** @type {readonly object[]|undefined} */ (
|
|
147
|
+
lookup("field declaration", "itemFields", type, system)
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
86
151
|
/**
|
|
87
152
|
* The default art for an item type — the image a note of that type is given
|
|
88
153
|
* when it carries no `img:` of its own.
|
|
@@ -106,11 +171,13 @@ export function itemBuilder(type) {
|
|
|
106
171
|
* One spelling, one meaning, wherever it is written.
|
|
107
172
|
*
|
|
108
173
|
* @param {string} type - the item type.
|
|
174
|
+
* @param {string} [system] - The system compiling it, where a build declares
|
|
175
|
+
* more than one registry.
|
|
109
176
|
* @returns {string} The default image path for that type.
|
|
110
177
|
* @throws {Error} When the type's registry entry pairs no `img`.
|
|
111
178
|
*/
|
|
112
|
-
export function itemArt(type) {
|
|
113
|
-
const art = /** @type {
|
|
179
|
+
export function itemArt(type, system) {
|
|
180
|
+
const art = /** @type {string|undefined} */ (lookup("default art", "itemArt", type, system));
|
|
114
181
|
if (!art) {
|
|
115
182
|
throw new Error(
|
|
116
183
|
`No default art for item type "${type}" — the note carries no ` +
|
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.
|
|
95
|
+
*
|
|
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.
|
|
94
99
|
*
|
|
95
|
-
*
|
|
96
|
-
|
|
100
|
+
* @type {number}
|
|
101
|
+
*/
|
|
102
|
+
export const CANONICAL_KEY_SEGMENTS = 3;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Reads a canonical key back into its parts.
|
|
97
106
|
*
|
|
98
|
-
*
|
|
99
|
-
* @
|
|
100
|
-
*
|
|
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 };
|
package/engine/map-notes.mjs
CHANGED
|
@@ -51,11 +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
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";
|
|
59
62
|
|
|
60
63
|
/* -------------------------------------------------------------------- */
|
|
61
64
|
/* Note types and their canvas profiles */
|
|
@@ -63,7 +66,7 @@ import { CURATED_REGION_EVENTS, EXCLUDED_REGION_EVENTS } from "./region-events.m
|
|
|
63
66
|
|
|
64
67
|
// The set itself lives in `ids.mjs`, a leaf both this module and the
|
|
65
68
|
// doc-carrying type set in `item-docs.mjs` can depend on without a cycle.
|
|
66
|
-
export { MAP_TYPES };
|
|
69
|
+
export { MAP_SUBTYPES, MAP_TYPES };
|
|
67
70
|
|
|
68
71
|
/**
|
|
69
72
|
* Whether a content note's type compiles into a Scene.
|
|
@@ -76,7 +79,7 @@ export function isMapType(type) {
|
|
|
76
79
|
}
|
|
77
80
|
|
|
78
81
|
/**
|
|
79
|
-
* Per-
|
|
82
|
+
* Per-subtype canvas defaults, emitted **explicitly** on every scene.
|
|
80
83
|
*
|
|
81
84
|
* This is not a convenience. `grid.type`, `grid.distance` and `grid.units` all
|
|
82
85
|
* declare `initial: () => game.system.grid.*`, and there is no `game` at build
|
|
@@ -86,7 +89,7 @@ export function isMapType(type) {
|
|
|
86
89
|
*
|
|
87
90
|
* @type {Readonly<Record<string, object>>}
|
|
88
91
|
*/
|
|
89
|
-
export const
|
|
92
|
+
export const MAP_SUBTYPE_PROFILES = Object.freeze({
|
|
90
93
|
battlemap: Object.freeze({
|
|
91
94
|
grid: { type: 1 /* SQUARE */, distance: 5, units: "ft" },
|
|
92
95
|
tokenVision: true,
|
|
@@ -108,18 +111,23 @@ export const MAP_TYPE_PROFILES = Object.freeze({
|
|
|
108
111
|
});
|
|
109
112
|
|
|
110
113
|
/**
|
|
111
|
-
* The canvas profile for a map
|
|
114
|
+
* The canvas profile for a map subType.
|
|
112
115
|
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
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.
|
|
117
125
|
*/
|
|
118
|
-
export function mapProfile(
|
|
119
|
-
const profile =
|
|
126
|
+
export function mapProfile(subType) {
|
|
127
|
+
const profile = MAP_SUBTYPE_PROFILES[String(subType)];
|
|
120
128
|
if (!profile) {
|
|
121
129
|
throw new Error(
|
|
122
|
-
`unknown map
|
|
130
|
+
`unknown map subtype "${subType}" — expected one of ${MAP_SUBTYPES.join(", ")}`,
|
|
123
131
|
);
|
|
124
132
|
}
|
|
125
133
|
return profile;
|
|
@@ -879,7 +887,7 @@ export function buildScene(fm, ctx) {
|
|
|
879
887
|
const sohl = fm.sohl ?? {};
|
|
880
888
|
const sceneId = fm.id;
|
|
881
889
|
if (!sceneId) throw new Error("a map note needs an `id`");
|
|
882
|
-
const profile = mapProfile(fm.
|
|
890
|
+
const profile = mapProfile(fm.subType);
|
|
883
891
|
|
|
884
892
|
const dimensions = sohl.dimensions;
|
|
885
893
|
if (
|
|
@@ -896,7 +904,11 @@ export function buildScene(fm, ctx) {
|
|
|
896
904
|
"match the art",
|
|
897
905
|
);
|
|
898
906
|
}
|
|
899
|
-
|
|
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`");
|
|
900
912
|
|
|
901
913
|
const warn = (message) => {
|
|
902
914
|
if (ctx.warnings) ctx.warnings.push(message);
|
|
@@ -921,7 +933,7 @@ export function buildScene(fm, ctx) {
|
|
|
921
933
|
tokenVision: profile.tokenVision,
|
|
922
934
|
fog: { mode: profile.fog.mode },
|
|
923
935
|
initialLevel: DEFAULT_LEVEL_ID,
|
|
924
|
-
levels: [buildLevel(sohl, sceneId)],
|
|
936
|
+
levels: [buildLevel(sohl, sceneId, img)],
|
|
925
937
|
drawings: [],
|
|
926
938
|
tokens: [],
|
|
927
939
|
lights: buildLights(sohl, geom, inner),
|
|
@@ -957,7 +969,7 @@ export function buildScene(fm, ctx) {
|
|
|
957
969
|
}
|
|
958
970
|
|
|
959
971
|
/**
|
|
960
|
-
* Synthesise the scene's single embedded Level from `
|
|
972
|
+
* Synthesise the scene's single embedded Level from `img:` / `overlay:`.
|
|
961
973
|
*
|
|
962
974
|
* Authors never write `levels:`. A scene must ship at least one Level — the
|
|
963
975
|
* client-side `_preCreate` net that would create one does not run for offline
|
|
@@ -967,16 +979,20 @@ export function buildScene(fm, ctx) {
|
|
|
967
979
|
*
|
|
968
980
|
* @param {object} sohl - The note's `sohl:` block.
|
|
969
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).
|
|
970
986
|
* @returns {object} The Level document, keyed for the pack.
|
|
971
987
|
*/
|
|
972
|
-
export function buildLevel(sohl, sceneId) {
|
|
988
|
+
export function buildLevel(sohl, sceneId, img = readAliasedField({ sohl }, "img")) {
|
|
973
989
|
const level = {
|
|
974
990
|
_id: DEFAULT_LEVEL_ID,
|
|
975
991
|
name: sohl.levelName ?? "Ground",
|
|
976
992
|
elevation: { bottom: 0, top: 20 },
|
|
977
993
|
background: {
|
|
978
994
|
color: sohl.backgroundColor ?? "#999999",
|
|
979
|
-
src:
|
|
995
|
+
src: img,
|
|
980
996
|
},
|
|
981
997
|
foreground: { src: sohl.overlay ?? null },
|
|
982
998
|
sort: 0,
|