@heroiclands/package-build 20.7.0 → 21.1.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 +183 -0
- package/CONTENT.md +132 -44
- package/bin/content-build.mjs +37 -5
- package/bin/package-build.mjs +77 -0
- package/content-config.mjs +59 -1
- package/docs/api.md +149 -19
- package/docs/commands.md +75 -0
- package/docs/configuration.md +37 -10
- package/docs/content-format.md +450 -49
- package/engine/content-format.mjs +52 -3
- package/engine/content-images.mjs +699 -0
- package/engine/dependency-bump.mjs +218 -0
- package/engine/frontmatter-lint.mjs +89 -2
- package/engine/helpers.mjs +81 -142
- package/engine/index.mjs +15 -0
- package/engine/infobox-registry.mjs +81 -0
- package/engine/infobox-render.mjs +382 -0
- package/engine/infobox.mjs +963 -0
- package/engine/item-registry.mjs +5 -5
- package/engine/journals.mjs +22 -1
- package/engine/map-notes.mjs +11 -5
- package/engine/metadata-index.mjs +5 -0
- package/engine/note-vocabulary.mjs +57 -2
- package/engine/pathnames.mjs +374 -0
- package/engine/pdf-build.mjs +206 -9
- package/engine/pdf-render.mjs +453 -20
- package/engine/pdf-toc.mjs +77 -5
- package/engine/scenes.mjs +2 -1
- package/engine/site-build.mjs +106 -7
- package/engine/site-index.mjs +93 -4
- package/engine/wikilinks.mjs +93 -0
- package/hm3/default-item-art.mjs +14 -15
- package/hm3/index.mjs +3 -0
- package/hm3/infobox.mjs +64 -0
- package/package.json +1 -1
- package/sohl/default-item-art.mjs +18 -16
- package/sohl/index.mjs +3 -0
- package/sohl/infobox.mjs +499 -0
- package/types/content-config.d.mts +7 -0
- package/types/engine/content-format.d.mts +36 -0
- package/types/engine/content-images.d.mts +281 -0
- package/types/engine/dependency-bump.d.mts +89 -0
- package/types/engine/frontmatter-lint.d.mts +23 -0
- package/types/engine/helpers.d.mts +30 -72
- package/types/engine/index.d.mts +5 -0
- package/types/engine/infobox-registry.d.mts +36 -0
- package/types/engine/infobox-render.d.mts +87 -0
- package/types/engine/infobox.d.mts +443 -0
- package/types/engine/item-registry.d.mts +5 -5
- package/types/engine/journals.d.mts +9 -1
- package/types/engine/note-vocabulary.d.mts +51 -0
- package/types/engine/pathnames.d.mts +189 -0
- package/types/engine/pdf-build.d.mts +46 -0
- package/types/engine/pdf-render.d.mts +97 -1
- package/types/engine/pdf-toc.d.mts +10 -5
- package/types/engine/site-build.d.mts +11 -3
- package/types/engine/site-index.d.mts +35 -3
- package/types/engine/wikilinks.d.mts +22 -0
- package/types/hm3/default-item-art.d.mts +5 -6
- package/types/hm3/index.d.mts +1 -0
- package/types/hm3/infobox.d.mts +22 -0
- package/types/sohl/index.d.mts +1 -0
- package/types/sohl/infobox.d.mts +145 -0
package/hm3/infobox.mjs
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
|
|
3
|
+
* Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
|
|
4
|
+
*
|
|
5
|
+
* This work is licensed under the GNU General Public License v3.0 (GPLv3).
|
|
6
|
+
* You may copy, modify, and distribute it under the terms of that license.
|
|
7
|
+
*
|
|
8
|
+
* For full terms, see the LICENSE.md file in the project root or visit:
|
|
9
|
+
* https://www.gnu.org/licenses/gpl-3.0.html
|
|
10
|
+
*
|
|
11
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* **HM3's half of the infobox** — what this system's summary panel carries.
|
|
16
|
+
*
|
|
17
|
+
* Read straight off {@link HM3_ITEM_FIELDS}, the same list the item builders
|
|
18
|
+
* obey, so a field added to a type reaches the box with no second edit. HM3
|
|
19
|
+
* describes a weapon, a skill and a piece of gear with its own values, which
|
|
20
|
+
* is the whole reason a system box exists per system rather than once: the two
|
|
21
|
+
* systems disagree about impact, heft, reach and draw, so neither box can
|
|
22
|
+
* stand for the other.
|
|
23
|
+
*
|
|
24
|
+
* A note that says nothing about HM3 gets a box reading _Not available_, and a
|
|
25
|
+
* note type HM3 has no concept of — an affiliation, a mystery, an attribute —
|
|
26
|
+
* gets no box at all. Both answers come from the note-type → document-subtype
|
|
27
|
+
* map, so neither is stated here.
|
|
28
|
+
*
|
|
29
|
+
* @module
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { GEAR_UNITS, defineInfobox } from "../engine/infobox.mjs";
|
|
33
|
+
import { HM3_ITEM_FIELDS } from "./item-fields.mjs";
|
|
34
|
+
|
|
35
|
+
/** What this system's box is called. @type {string} */
|
|
36
|
+
export const HM3_INFOBOX_TITLE = "HârnMaster 3";
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* HM3's presentation overlay: what one of this system's fields is called where
|
|
40
|
+
* humanising its key gives the wrong word.
|
|
41
|
+
*
|
|
42
|
+
* **Not a second field list.** A field it does not mention still gets a row
|
|
43
|
+
* under its own humanised name, so a field added to {@link HM3_ITEM_FIELDS}
|
|
44
|
+
* reaches the box with no edit here.
|
|
45
|
+
*
|
|
46
|
+
* @type {Readonly<Record<string, {label?: string, withheld?: string}>>}
|
|
47
|
+
*/
|
|
48
|
+
export const HM3_FIELD_PRESENTATION = Object.freeze({
|
|
49
|
+
...GEAR_UNITS,
|
|
50
|
+
capacity: Object.freeze({ label: "Capacity", unit: " lbs" }),
|
|
51
|
+
masteryLevel: Object.freeze({ label: "Mastery" }),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* HM3's infobox declaration.
|
|
56
|
+
*
|
|
57
|
+
* @type {object}
|
|
58
|
+
*/
|
|
59
|
+
export const HM3_INFOBOX = defineInfobox({
|
|
60
|
+
system: "hm3",
|
|
61
|
+
title: HM3_INFOBOX_TITLE,
|
|
62
|
+
fields: HM3_ITEM_FIELDS,
|
|
63
|
+
presentation: HM3_FIELD_PRESENTATION,
|
|
64
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroiclands/package-build",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "21.1.0",
|
|
4
4
|
"description": "Shared toolchain for building and shipping a HeroicLands Foundry VTT package — content compilation, manifest, localization, staging, bundle, release and deployment.",
|
|
5
5
|
"license": "GPL-3.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*
|
|
19
19
|
* Foundry's `Item.DEFAULT_ICON` is the white `icons/svg/item-bag.svg`, which is
|
|
20
20
|
* invisible on the light Manuscript sheet and does not adapt to theme; every
|
|
21
|
-
* SoHL item type instead defaults to a themed `
|
|
21
|
+
* SoHL item type instead defaults to a themed `sohl/assets/icons/**`
|
|
22
22
|
* SVG here (dark ink in light mode, cream in dark, via the build-time
|
|
23
23
|
* adaptive-fill injection in `utils/svg-theme.mjs`).
|
|
24
24
|
*
|
|
@@ -38,8 +38,10 @@
|
|
|
38
38
|
* Injecting the map through configuration instead would re-open that drift,
|
|
39
39
|
* because the two sides would once more be free to disagree.
|
|
40
40
|
*
|
|
41
|
-
* Paths
|
|
42
|
-
*
|
|
41
|
+
* **Paths name the package that owns the file**, as every pathname does: these
|
|
42
|
+
* icons are the `sohl` package's, so they are written `sohl/assets/…` and each
|
|
43
|
+
* surface derives its own address from that. If a new item type is added, add
|
|
44
|
+
* its default here.
|
|
43
45
|
*
|
|
44
46
|
* **The keys are SoHL *document* subtypes, not markdown note types.** The
|
|
45
47
|
* runtime reads this with `itemData.type`, which is a Foundry Item subtype, and
|
|
@@ -51,19 +53,19 @@
|
|
|
51
53
|
* build translates once and no second copy of those three rows exists.
|
|
52
54
|
*/
|
|
53
55
|
export const DEFAULT_ITEM_ART = {
|
|
54
|
-
affiliation: "
|
|
55
|
-
affliction: "
|
|
56
|
-
armorgear: "
|
|
57
|
-
attribute: "
|
|
58
|
-
concoctiongear: "
|
|
59
|
-
containergear: "
|
|
60
|
-
miscgear: "
|
|
61
|
-
mystery: "
|
|
62
|
-
mysticalability: "
|
|
63
|
-
projectilegear: "
|
|
64
|
-
skill: "
|
|
65
|
-
trauma: "
|
|
66
|
-
weapongear: "
|
|
56
|
+
affiliation: "sohl/assets/icons/noun/shield.svg",
|
|
57
|
+
affliction: "sohl/assets/icons/other/sick.svg",
|
|
58
|
+
armorgear: "sohl/assets/icons/game-icons/lorc/breastplate.svg",
|
|
59
|
+
attribute: "sohl/assets/icons/other/charm.svg",
|
|
60
|
+
concoctiongear: "sohl/assets/icons/game-icons/badges/flask.svg",
|
|
61
|
+
containergear: "sohl/assets/icons/other/sack.svg",
|
|
62
|
+
miscgear: "sohl/assets/icons/other/question-mark.svg",
|
|
63
|
+
mystery: "sohl/assets/icons/other/sparkles.svg",
|
|
64
|
+
mysticalability: "sohl/assets/icons/other/hand-sparkles.svg",
|
|
65
|
+
projectilegear: "sohl/assets/icons/noun/arrow.svg",
|
|
66
|
+
skill: "sohl/assets/icons/other/head-gear.svg",
|
|
67
|
+
trauma: "sohl/assets/icons/other/injury.svg",
|
|
68
|
+
weapongear: "sohl/assets/icons/other/sword.svg",
|
|
67
69
|
};
|
|
68
70
|
|
|
69
71
|
/**
|
package/sohl/index.mjs
CHANGED
|
@@ -45,6 +45,9 @@ export * as actors from "./actors.mjs";
|
|
|
45
45
|
/** This package's own knowledgebase body passes, named from `site.pass`. */
|
|
46
46
|
export * as kbPasses from "./kb-passes.mjs";
|
|
47
47
|
|
|
48
|
+
/** Which of SoHL's facts a note's summary panel carries, and how they group. */
|
|
49
|
+
export * as infobox from "./infobox.mjs";
|
|
50
|
+
|
|
48
51
|
// Flat as well as namespaced: the Foundry runtime imports these by name through
|
|
49
52
|
// their own entry points, and they were this barrel's surface before the
|
|
50
53
|
// compilers arrived.
|
package/sohl/infobox.mjs
ADDED
|
@@ -0,0 +1,499 @@
|
|
|
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
|
+
* **SoHL's half of the infobox** — which of this system's facts a summary
|
|
16
|
+
* panel carries, and how they group.
|
|
17
|
+
*
|
|
18
|
+
* The mechanism is `engine/infobox.mjs`; this is the declaration, which is the
|
|
19
|
+
* `engine/` ÷ `sohl/` line everywhere else in the package. What a box looks
|
|
20
|
+
* like is note-format knowledge. That an attribute score lives at
|
|
21
|
+
* `system.scoreBase`, that a mystical ability is a practice or a knack, and
|
|
22
|
+
* that a weapon strikes in modes is SoHL's.
|
|
23
|
+
*
|
|
24
|
+
* **Almost every type is read straight off its own field declaration.**
|
|
25
|
+
* {@link NOTE_SCHEMAS} is the vocabulary the frontmatter linter checks against
|
|
26
|
+
* and, for an item type, the very list the compiler obeys — so a field added
|
|
27
|
+
* to a type reaches the box with no second edit anywhere.
|
|
28
|
+
*
|
|
29
|
+
* **Four types earn a builder of their own**, because their box is derived
|
|
30
|
+
* rather than read field by field:
|
|
31
|
+
*
|
|
32
|
+
* - a **being**, whose attributes, skills, mystical abilities and carried gear
|
|
33
|
+
* are one flat `sohl.items` list that has to be sorted before it can be
|
|
34
|
+
* shown;
|
|
35
|
+
* - **armour**, whose protection is shown as a full set of four aspects, with
|
|
36
|
+
* an unstated one rendered `0` rather than dropped — armour that stops
|
|
37
|
+
* nothing edged is a fact, not a gap;
|
|
38
|
+
* - a **weapon**, whose strike modes are shown one per line, with an unstated
|
|
39
|
+
* value rendered `—` for the same reason;
|
|
40
|
+
* - a **projectile**, whose impact is three declared fields composing into the
|
|
41
|
+
* one quantity a reader wants.
|
|
42
|
+
*
|
|
43
|
+
* Both placeholders are **data**, decided here, not a renderer's fallback.
|
|
44
|
+
* That is what lets one generic renderer draw them without knowing which field
|
|
45
|
+
* it is looking at.
|
|
46
|
+
*
|
|
47
|
+
* **What each field is called, and the few that carry no row**, is
|
|
48
|
+
* {@link SOHL_FIELD_PRESENTATION} — an overlay on the declaration rather than
|
|
49
|
+
* a list beside it.
|
|
50
|
+
*
|
|
51
|
+
* @module
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
import {
|
|
55
|
+
DURATION_LABELS,
|
|
56
|
+
GEAR_UNITS,
|
|
57
|
+
defineInfobox,
|
|
58
|
+
hasValue,
|
|
59
|
+
humanizeFieldName,
|
|
60
|
+
humanizeValue,
|
|
61
|
+
systemRowsSection,
|
|
62
|
+
} from "../engine/infobox.mjs";
|
|
63
|
+
import { currentType } from "../engine/ids.mjs";
|
|
64
|
+
import { subTypes } from "../engine/note-vocabulary.mjs";
|
|
65
|
+
import { systemBlock, systemData } from "../engine/system-block.mjs";
|
|
66
|
+
import { NOTE_SCHEMAS } from "./note-schemas.mjs";
|
|
67
|
+
import { GEAR_TYPE_TO_KEY } from "./being-info.mjs";
|
|
68
|
+
|
|
69
|
+
/** What this system's box is called. @type {string} */
|
|
70
|
+
export const SOHL_INFOBOX_TITLE = "Song of Heroic Lands";
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* What a value nobody stated is shown as, where showing nothing would be the
|
|
74
|
+
* wrong answer.
|
|
75
|
+
*
|
|
76
|
+
* Rule 4 drops an absent field, and that is right almost everywhere. It is
|
|
77
|
+
* wrong in a table of strike modes, where a column left blank on one row and
|
|
78
|
+
* filled on the next reads as a rendering fault rather than as a weapon that
|
|
79
|
+
* cannot be used that way.
|
|
80
|
+
*
|
|
81
|
+
* @type {string}
|
|
82
|
+
*/
|
|
83
|
+
export const UNSTATED = "—";
|
|
84
|
+
|
|
85
|
+
/** The prefix every protection aspect's declared name carries. @type {string} */
|
|
86
|
+
const PROTECTION_PREFIX = "protection.";
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The declarations of the four aspects armour is rated against, in the order a
|
|
90
|
+
* sheet shows them.
|
|
91
|
+
*
|
|
92
|
+
* Taken from the armour field declaration rather than listed: the declaration
|
|
93
|
+
* names `protection.blunt`, `protection.edged` and the rest, so the set, its
|
|
94
|
+
* order **and where each is authored** come from the same place the compiler
|
|
95
|
+
* reads them. Carrying the whole declaration rather than the aspect's word is
|
|
96
|
+
* what lets the grid resolve a value the way the compiler does — a note writes
|
|
97
|
+
* `sohl.system.protectionBase.blunt`, and a grid that read the declared
|
|
98
|
+
* *source* path instead would find nothing and call every aspect unstated.
|
|
99
|
+
*
|
|
100
|
+
* @type {readonly object[]}
|
|
101
|
+
*/
|
|
102
|
+
export const PROTECTION_FIELDS = Object.freeze(
|
|
103
|
+
(NOTE_SCHEMAS.armorgear ?? []).filter(
|
|
104
|
+
(field) => typeof field.name === "string" && field.name.startsWith(PROTECTION_PREFIX),
|
|
105
|
+
),
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
/** A skill family whose humanised name reads wrong. */
|
|
109
|
+
const SKILL_GROUP_LABELS = Object.freeze({ combattechnique: "Combat Techniques" });
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* SoHL's presentation overlay: what one of this system's fields is called, and
|
|
113
|
+
* the few that carry no row.
|
|
114
|
+
*
|
|
115
|
+
* **Not a second field list.** The fields come from {@link NOTE_SCHEMAS}, and
|
|
116
|
+
* a field this overlay does not mention still gets a row under its own
|
|
117
|
+
* humanised name — so a field added to a type reaches the box with no edit
|
|
118
|
+
* here. What the overlay adds is the two things a compiler's field list cannot
|
|
119
|
+
* say, because they are about a page rather than about a document:
|
|
120
|
+
*
|
|
121
|
+
* - **a reader's word** where the declaration's key is the compiler's. A key
|
|
122
|
+
* is named for the value it carries into a DataModel; `assocSkillCode` and
|
|
123
|
+
* `perceptionPenaltyBase` are exactly right there and wrong in a panel
|
|
124
|
+
* somebody reads.
|
|
125
|
+
* - **which facts belong on a page at all.** A value shown whole somewhere
|
|
126
|
+
* else in the same box — protection, strike modes, a projectile's impact —
|
|
127
|
+
* would otherwise be said twice, the second time a row at a time and worse;
|
|
128
|
+
* and a flag that steers a character sheet is not a fact about the subject.
|
|
129
|
+
*
|
|
130
|
+
* @type {Readonly<Record<string, {label?: string, withheld?: string}>>}
|
|
131
|
+
*/
|
|
132
|
+
export const SOHL_FIELD_PRESENTATION = Object.freeze({
|
|
133
|
+
...DURATION_LABELS,
|
|
134
|
+
...GEAR_UNITS,
|
|
135
|
+
|
|
136
|
+
"protection.blunt": Object.freeze({ withheld: "shown whole, in the Protection grid" }),
|
|
137
|
+
"protection.edged": Object.freeze({ withheld: "shown whole, in the Protection grid" }),
|
|
138
|
+
"protection.piercing": Object.freeze({ withheld: "shown whole, in the Protection grid" }),
|
|
139
|
+
"protection.fire": Object.freeze({ withheld: "shown whole, in the Protection grid" }),
|
|
140
|
+
strikeModes: Object.freeze({ withheld: "shown one per line, in the Strike Modes section" }),
|
|
141
|
+
"impact.die": Object.freeze({ withheld: "shown whole, as the Impact row" }),
|
|
142
|
+
"impact.modifier": Object.freeze({ withheld: "shown whole, as the Impact row" }),
|
|
143
|
+
"impact.aspect": Object.freeze({ withheld: "shown whole, as the Impact row" }),
|
|
144
|
+
improveFlag: Object.freeze({
|
|
145
|
+
withheld: "character-sheet machinery — whether the item is flagged for improvement",
|
|
146
|
+
}),
|
|
147
|
+
facing: Object.freeze({
|
|
148
|
+
withheld: "a body-location layout, which has no summary shape",
|
|
149
|
+
}),
|
|
150
|
+
|
|
151
|
+
subType: Object.freeze({ label: "Subtype" }),
|
|
152
|
+
flexloc: Object.freeze({ label: "Flexible locations" }),
|
|
153
|
+
rigidloc: Object.freeze({ label: "Rigid locations" }),
|
|
154
|
+
perceptionPenaltyBase: Object.freeze({ label: "Perception penalty" }),
|
|
155
|
+
detailMaterial: Object.freeze({ label: "Material detail" }),
|
|
156
|
+
maxCapacity: Object.freeze({ label: "Max capacity", unit: " lbs" }),
|
|
157
|
+
scoreBase: Object.freeze({ label: "Score" }),
|
|
158
|
+
masteryLevelBase: Object.freeze({ label: "Mastery" }),
|
|
159
|
+
levelBase: Object.freeze({ label: "Level" }),
|
|
160
|
+
healingRateBase: Object.freeze({ label: "Healing rate" }),
|
|
161
|
+
skillBaseFormula: Object.freeze({ label: "Skill base" }),
|
|
162
|
+
initSkillMult: Object.freeze({ label: "Init multiplier" }),
|
|
163
|
+
initDiceFormula: Object.freeze({ label: "Initiative dice" }),
|
|
164
|
+
valueDesc: Object.freeze({ label: "Scale" }),
|
|
165
|
+
parentSkillCode: Object.freeze({ label: "Specialises" }),
|
|
166
|
+
assocSkillCode: Object.freeze({ label: "Associated skill" }),
|
|
167
|
+
assocAffiliationCode: Object.freeze({ label: "Associated affiliation" }),
|
|
168
|
+
bodyLocationCode: Object.freeze({ label: "Body location" }),
|
|
169
|
+
impairedByRoles: Object.freeze({ label: "Impaired when" }),
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
/** Whether a value is a plain mapping. */
|
|
173
|
+
function isMapping(value) {
|
|
174
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* What one entry of `sohl.items` names.
|
|
179
|
+
*
|
|
180
|
+
* An entry addresses its item three ways and every tree uses all three: a
|
|
181
|
+
* `model` address whose last two hyphen-separated segments are always
|
|
182
|
+
* `<type>-<shortcode>`, explicit `type` / `shortcode` keys, or a locally
|
|
183
|
+
* authored item stating its type with the shortcode inside its own `system`
|
|
184
|
+
* block. One decode covers all of them, so nothing downstream has to know
|
|
185
|
+
* which form a note happened to use.
|
|
186
|
+
*
|
|
187
|
+
* @param {object} entry - One `sohl.items` entry.
|
|
188
|
+
* @returns {{type: string, shortcode: string, name: string, system: object}|undefined}
|
|
189
|
+
* What it names, or `undefined` when it names nothing addressable.
|
|
190
|
+
*/
|
|
191
|
+
export function decodeItem(entry) {
|
|
192
|
+
if (!isMapping(entry)) return undefined;
|
|
193
|
+
let type = typeof entry.type === "string" ? entry.type : "";
|
|
194
|
+
let shortcode = typeof entry.shortcode === "string" ? entry.shortcode : "";
|
|
195
|
+
if (!type && typeof entry.model === "string") {
|
|
196
|
+
const segments = entry.model.split("-");
|
|
197
|
+
if (segments.length >= 2) {
|
|
198
|
+
type = segments[segments.length - 2];
|
|
199
|
+
if (!shortcode) shortcode = segments[segments.length - 1];
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const system = isMapping(entry.system) ? entry.system : {};
|
|
203
|
+
if (!shortcode && typeof system.shortcode === "string") shortcode = system.shortcode;
|
|
204
|
+
if (!type) return undefined;
|
|
205
|
+
return {
|
|
206
|
+
type: currentType(type),
|
|
207
|
+
shortcode,
|
|
208
|
+
name: typeof entry.name === "string" ? entry.name : "",
|
|
209
|
+
system,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Every decoded item a being embeds, in the order the note wrote them.
|
|
215
|
+
*
|
|
216
|
+
* @param {object} fm - The note's frontmatter.
|
|
217
|
+
* @param {string} block - The system's block key.
|
|
218
|
+
* @returns {object[]} The items.
|
|
219
|
+
*/
|
|
220
|
+
function itemsOf(fm, block) {
|
|
221
|
+
const declared = systemBlock(fm, block)?.items;
|
|
222
|
+
return (Array.isArray(declared) ? declared : []).map(decodeItem).filter(Boolean);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* One item as a value a renderer can draw: its name, and a link where the
|
|
227
|
+
* index reached it.
|
|
228
|
+
*
|
|
229
|
+
* @param {object} item - A decoded item.
|
|
230
|
+
* @param {(ref: unknown, hint?: object) => object|undefined} resolve - The
|
|
231
|
+
* medium's resolver.
|
|
232
|
+
* @returns {{text: string, url?: string, uuid?: string, address?: string}} The value.
|
|
233
|
+
*/
|
|
234
|
+
function itemValue(item, resolve) {
|
|
235
|
+
const found = item.shortcode ? resolve?.(item.shortcode, { type: item.type }) : undefined;
|
|
236
|
+
const value = { text: item.name || found?.name || humanizeValue(item.shortcode) };
|
|
237
|
+
if (found?.url) value.url = found.url;
|
|
238
|
+
if (found?.uuid) value.uuid = found.uuid;
|
|
239
|
+
if (found?.address) value.address = found.address;
|
|
240
|
+
return value;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* A being's box: attributes, skills, mystical abilities and equipment.
|
|
245
|
+
*
|
|
246
|
+
* Each is a section of its own, because a section is the unit that flows — the
|
|
247
|
+
* panel breaks between `ATTRIBUTES` and `SKILLS` rather than through either.
|
|
248
|
+
* A section with nothing in it is not emitted at all, which is what makes a
|
|
249
|
+
* sparse creature's box short rather than mostly empty.
|
|
250
|
+
*
|
|
251
|
+
* @param {object} fm - The note's frontmatter.
|
|
252
|
+
* @param {object} ctx - `{ block, resolve }`.
|
|
253
|
+
* @returns {object[]} The sections.
|
|
254
|
+
*/
|
|
255
|
+
export function beingSections(fm, { block, resolve }) {
|
|
256
|
+
const items = itemsOf(fm, block);
|
|
257
|
+
const sections = [];
|
|
258
|
+
|
|
259
|
+
const cells = [];
|
|
260
|
+
for (const item of items) {
|
|
261
|
+
if (item.type !== "attribute") continue;
|
|
262
|
+
const score = item.system.scoreBase;
|
|
263
|
+
if (!hasValue(score)) continue;
|
|
264
|
+
cells.push({ label: String(item.shortcode).toUpperCase(), value: score });
|
|
265
|
+
}
|
|
266
|
+
if (cells.length) {
|
|
267
|
+
sections.push({ id: "attributes", label: "Attributes", layout: "grid", cells });
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Skill families come from the note vocabulary's own `subType` set, in its
|
|
271
|
+
// declared order — a family added to the format groups here with no second
|
|
272
|
+
// list to edit. A skill the index cannot place falls to the end under its
|
|
273
|
+
// own heading rather than vanishing from the panel.
|
|
274
|
+
const families = new Map((subTypes("skill") ?? []).map((value) => [value, []]));
|
|
275
|
+
const unplaced = [];
|
|
276
|
+
for (const item of items) {
|
|
277
|
+
if (item.type !== "skill") continue;
|
|
278
|
+
const mastery = item.system.masteryLevelBase;
|
|
279
|
+
if (!hasValue(mastery)) continue;
|
|
280
|
+
const found = item.shortcode ? resolve?.(item.shortcode, { type: "skill" }) : undefined;
|
|
281
|
+
const family = item.system.subType ?? found?.subType;
|
|
282
|
+
const value = itemValue(item, resolve);
|
|
283
|
+
const entry = { ...value, text: `${value.text} ${mastery}` };
|
|
284
|
+
if (family && families.has(family)) families.get(family).push(entry);
|
|
285
|
+
else unplaced.push(entry);
|
|
286
|
+
}
|
|
287
|
+
const groups = [];
|
|
288
|
+
for (const [family, entries] of families) {
|
|
289
|
+
if (!entries.length) continue;
|
|
290
|
+
groups.push({
|
|
291
|
+
label: SKILL_GROUP_LABELS[family] ?? humanizeFieldName(family),
|
|
292
|
+
entries,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
if (unplaced.length) groups.push({ label: "Other", entries: unplaced });
|
|
296
|
+
if (groups.length) sections.push({ id: "skills", label: "Skills", layout: "runin", groups });
|
|
297
|
+
|
|
298
|
+
const mystical = [];
|
|
299
|
+
for (const item of items) {
|
|
300
|
+
if (item.type !== "mysticalability") continue;
|
|
301
|
+
mystical.push(itemValue(item, resolve));
|
|
302
|
+
}
|
|
303
|
+
if (mystical.length) {
|
|
304
|
+
sections.push({
|
|
305
|
+
id: "mysticalabilities",
|
|
306
|
+
label: "Mystical Abilities",
|
|
307
|
+
layout: "list",
|
|
308
|
+
entries: mystical,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const gear = new Map(Object.values(GEAR_TYPE_TO_KEY).map((key) => [key, []]));
|
|
313
|
+
for (const item of items) {
|
|
314
|
+
const key = GEAR_TYPE_TO_KEY[item.type];
|
|
315
|
+
if (!key) continue;
|
|
316
|
+
gear.get(key).push(itemValue(item, resolve));
|
|
317
|
+
}
|
|
318
|
+
const gearGroups = [];
|
|
319
|
+
for (const [key, entries] of gear) {
|
|
320
|
+
if (entries.length) gearGroups.push({ label: humanizeFieldName(key), entries });
|
|
321
|
+
}
|
|
322
|
+
if (gearGroups.length) {
|
|
323
|
+
sections.push({ id: "equipment", label: "Equipment", layout: "runin", groups: gearGroups });
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return sections;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Armour's box: whatever the note states, then the protection it gives.
|
|
331
|
+
*
|
|
332
|
+
* Protection is shown whole, with an aspect nobody stated rendered `0`. Armour
|
|
333
|
+
* that stops nothing edged is a fact about the armour, and dropping the row
|
|
334
|
+
* would leave a reader to guess whether it was unstated or nil.
|
|
335
|
+
*
|
|
336
|
+
* @param {object} fm - The note's frontmatter.
|
|
337
|
+
* @param {object} ctx - The section context.
|
|
338
|
+
* @returns {object[]} The sections.
|
|
339
|
+
*/
|
|
340
|
+
export function armorSections(fm, ctx) {
|
|
341
|
+
const sections = genericSections(fm, "armorgear", ctx);
|
|
342
|
+
const cells = PROTECTION_FIELDS.map((field) => {
|
|
343
|
+
const { value } = ctx.resolveField(field, fm, { block: ctx.block });
|
|
344
|
+
return {
|
|
345
|
+
label: humanizeFieldName(field.name),
|
|
346
|
+
value: hasValue(value) ? value : 0,
|
|
347
|
+
};
|
|
348
|
+
});
|
|
349
|
+
if (cells.length) {
|
|
350
|
+
sections.push({ id: "protection", label: "Protection", layout: "grid", cells });
|
|
351
|
+
}
|
|
352
|
+
return sections;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* A weapon's box: whatever the note states, then one line per strike mode.
|
|
357
|
+
*
|
|
358
|
+
* A mode that states no attack modifier, no impact or no length is shown with
|
|
359
|
+
* {@link UNSTATED} in that place rather than with the clause missing: the
|
|
360
|
+
* modes are read against each other, and a line that is shorter than its
|
|
361
|
+
* neighbour for no visible reason reads as a fault.
|
|
362
|
+
*
|
|
363
|
+
* @param {object} fm - The note's frontmatter.
|
|
364
|
+
* @param {object} ctx - The section context.
|
|
365
|
+
* @returns {object[]} The sections.
|
|
366
|
+
*/
|
|
367
|
+
export function weaponSections(fm, ctx) {
|
|
368
|
+
const sections = genericSections(fm, "weapongear", ctx);
|
|
369
|
+
const groups = [];
|
|
370
|
+
for (const [key, mode] of strikeModes(systemData(fm, ctx.block).strikeModes)) {
|
|
371
|
+
groups.push({
|
|
372
|
+
label: mode.name || humanizeValue(key),
|
|
373
|
+
entries: [
|
|
374
|
+
{ text: `Attack ${signed(mode.attack?.modifier)}` },
|
|
375
|
+
{ text: `Impact ${impactOf(mode.impactBase)}` },
|
|
376
|
+
{ text: `Length ${hasValue(mode.lengthBase) ? mode.lengthBase : UNSTATED}` },
|
|
377
|
+
],
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
if (groups.length) {
|
|
381
|
+
sections.push({ id: "strikemodes", label: "Strike Modes", layout: "runin", groups });
|
|
382
|
+
}
|
|
383
|
+
return sections;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* A projectile's box: whatever the note states, then what it hits for.
|
|
388
|
+
*
|
|
389
|
+
* Impact is three declared fields — dice, modifier and aspect — and a reader
|
|
390
|
+
* wants the one quantity they compose into. Three rows reading `Die 6`,
|
|
391
|
+
* `Modifier 2`, `Aspect Piercing` say the declaration's structure rather than
|
|
392
|
+
* the projectile's, so the three are withheld and the row they make is added
|
|
393
|
+
* in their place.
|
|
394
|
+
*
|
|
395
|
+
* @param {object} fm - The note's frontmatter.
|
|
396
|
+
* @param {object} ctx - The section context.
|
|
397
|
+
* @returns {object[]} The sections.
|
|
398
|
+
*/
|
|
399
|
+
export function projectileSections(fm, ctx) {
|
|
400
|
+
const sections = genericSections(fm, "projectilegear", ctx);
|
|
401
|
+
const impact = impactOf(systemData(fm, ctx.block).impactBase);
|
|
402
|
+
if (impact !== UNSTATED) {
|
|
403
|
+
const rows = sections[0]?.rows;
|
|
404
|
+
const row = { label: "Impact", kind: "text", value: impact };
|
|
405
|
+
if (rows) rows.push(row);
|
|
406
|
+
else sections.push({ id: "profile", layout: "rows", rows: [row] });
|
|
407
|
+
}
|
|
408
|
+
return sections;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* A weapon's strike modes, whichever of the two shapes the note wrote.
|
|
413
|
+
*
|
|
414
|
+
* Both are live in the corpus and both name the same thing. A **list** carries
|
|
415
|
+
* the mode's identity inside it, as `shortcode`, which is the shape a
|
|
416
|
+
* compendium document holds; a **mapping** carries it as the key. So the
|
|
417
|
+
* fallback name comes from the shortcode in one and from the key in the other,
|
|
418
|
+
* and everything downstream sees one shape.
|
|
419
|
+
*
|
|
420
|
+
* @param {unknown} declared - What the note wrote at `strikeModes`.
|
|
421
|
+
* @returns {[string, object][]} Mode name → the mode.
|
|
422
|
+
*/
|
|
423
|
+
export function strikeModes(declared) {
|
|
424
|
+
if (Array.isArray(declared)) {
|
|
425
|
+
return declared
|
|
426
|
+
.filter(isMapping)
|
|
427
|
+
.map((mode, at) => [String(mode.shortcode ?? at + 1), mode]);
|
|
428
|
+
}
|
|
429
|
+
if (isMapping(declared)) return Object.entries(declared).filter(([, mode]) => isMapping(mode));
|
|
430
|
+
return [];
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/** A modifier with its sign, or {@link UNSTATED}. */
|
|
434
|
+
function signed(value) {
|
|
435
|
+
if (!hasValue(value) && value !== 0) return UNSTATED;
|
|
436
|
+
return Number(value) >= 0 ? `+${value}` : String(value);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* An impact as dice and aspect, or {@link UNSTATED}.
|
|
441
|
+
*
|
|
442
|
+
* **A die of `0` is no die**, which is how the schema says a strike mode rolls
|
|
443
|
+
* nothing — a net envelops and does no damage. Read as a number it would print
|
|
444
|
+
* `1d0`, a roll nobody can make, so it composes to a flat modifier or to
|
|
445
|
+
* nothing at all.
|
|
446
|
+
*
|
|
447
|
+
* **An aspect alone is not an impact.** Without a die or a modifier there is no
|
|
448
|
+
* magnitude, and a line reading `Impact blunt` states the kind of a quantity
|
|
449
|
+
* that was never given. That is {@link UNSTATED}'s whole job.
|
|
450
|
+
*
|
|
451
|
+
* @param {unknown} impact - The authored `impactBase`.
|
|
452
|
+
* @returns {string} The impact, or {@link UNSTATED}.
|
|
453
|
+
*/
|
|
454
|
+
function impactOf(impact) {
|
|
455
|
+
if (!isMapping(impact)) return UNSTATED;
|
|
456
|
+
const die = Number(impact.die ?? 0);
|
|
457
|
+
const modifier = Number(impact.modifier ?? 0);
|
|
458
|
+
const dice = die > 0 ? `${impact.numDice ?? 1}d${die}` : "";
|
|
459
|
+
const roll =
|
|
460
|
+
dice ? `${dice}${modifier ? signed(modifier) : ""}`
|
|
461
|
+
: modifier ? signed(modifier)
|
|
462
|
+
: "";
|
|
463
|
+
if (!roll) return UNSTATED;
|
|
464
|
+
const aspect = hasValue(impact.aspect) ? humanizeValue(impact.aspect) : "";
|
|
465
|
+
return [roll, aspect].filter(Boolean).join(" ");
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* The rows a type's own field declaration yields, as a section.
|
|
470
|
+
*
|
|
471
|
+
* Shared by the three builders above so each adds to what its type states
|
|
472
|
+
* rather than replacing it.
|
|
473
|
+
*
|
|
474
|
+
* @param {object} fm - The note's frontmatter.
|
|
475
|
+
* @param {string} type - The note type.
|
|
476
|
+
* @param {object} ctx - The section context.
|
|
477
|
+
* @returns {object[]} Zero or one section.
|
|
478
|
+
*/
|
|
479
|
+
function genericSections(fm, type, ctx) {
|
|
480
|
+
return systemRowsSection(fm, NOTE_SCHEMAS[type], ctx);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* SoHL's infobox declaration.
|
|
485
|
+
*
|
|
486
|
+
* @type {object}
|
|
487
|
+
*/
|
|
488
|
+
export const SOHL_INFOBOX = defineInfobox({
|
|
489
|
+
system: "sohl",
|
|
490
|
+
title: SOHL_INFOBOX_TITLE,
|
|
491
|
+
fields: NOTE_SCHEMAS,
|
|
492
|
+
presentation: SOHL_FIELD_PRESENTATION,
|
|
493
|
+
sections: {
|
|
494
|
+
being: beingSections,
|
|
495
|
+
armorgear: armorSections,
|
|
496
|
+
weapongear: weaponSections,
|
|
497
|
+
projectilegear: projectileSections,
|
|
498
|
+
},
|
|
499
|
+
});
|
|
@@ -454,6 +454,13 @@ export type RelationshipSpec = {
|
|
|
454
454
|
* The other package's id.
|
|
455
455
|
*/
|
|
456
456
|
id: string;
|
|
457
|
+
/**
|
|
458
|
+
* What the other package's content is
|
|
459
|
+
* called, where that differs from its Foundry
|
|
460
|
+
* id. It is the name a note writes when it
|
|
461
|
+
* addresses a file that package ships.
|
|
462
|
+
*/
|
|
463
|
+
contentPackage?: string | undefined;
|
|
457
464
|
/**
|
|
458
465
|
* `system`, `module`, or `world`.
|
|
459
466
|
*/
|