@heroiclands/package-build 22.19.0 → 22.20.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 +7 -0
- package/ci/content-format-reference.mjs +31 -1
- package/docs/api.md +1 -0
- package/docs/authoring/frontmatter.md +3 -1
- package/docs/reference/format-details.md +43 -29
- package/engine/base-compiler.mjs +2 -2
- package/engine/frontmatter-lint.mjs +102 -13
- package/engine/infobox.mjs +3 -0
- package/engine/note-frontmatter.mjs +6 -0
- package/engine/note-vocabulary.mjs +19 -0
- package/engine/retired-fields.mjs +7 -18
- package/package.json +1 -1
- package/types/engine/note-frontmatter.d.mts +4 -0
- package/types/engine/note-vocabulary.d.mts +18 -0
- package/types/engine/retired-fields.d.mts +5 -11
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# @heroiclands/package-build
|
|
2
2
|
|
|
3
|
+
## 22.20.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Content checks require characters and NPCs to name an archetype and reject unrecognized archetypes; creatures may omit them.
|
|
8
|
+
- Names with unsupported fields now fail validation, and aliases appear beneath the name in infoboxes across published formats.
|
|
9
|
+
|
|
3
10
|
## 22.19.0
|
|
4
11
|
|
|
5
12
|
### Minor Changes
|
|
@@ -7,7 +7,11 @@ import path from "node:path";
|
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { formatGenerated } from "../engine/format-generated.mjs";
|
|
9
9
|
import { loadContentFormat } from "../engine/content-format.mjs";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
BEING_ARCHETYPES,
|
|
12
|
+
NOTE_VOCABULARY,
|
|
13
|
+
SHARED_DATA_FIELDS,
|
|
14
|
+
} from "../engine/note-vocabulary.mjs";
|
|
11
15
|
|
|
12
16
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
13
17
|
const output = path.join(root, "docs/reference/note-types.md");
|
|
@@ -104,3 +108,29 @@ if (process.argv.includes("--check")) {
|
|
|
104
108
|
} else {
|
|
105
109
|
process.stdout.write(result);
|
|
106
110
|
}
|
|
111
|
+
|
|
112
|
+
const detailsPath = path.join(root, "docs/reference/format-details.md");
|
|
113
|
+
const details = fs.readFileSync(detailsPath, "utf8");
|
|
114
|
+
if (!/<!-- archetypes:start -->[\s\S]*?<!-- archetypes:end -->/.test(details)) {
|
|
115
|
+
throw new Error("The archetype reference requires its generation markers");
|
|
116
|
+
}
|
|
117
|
+
const archetypes = Object.entries(BEING_ARCHETYPES)
|
|
118
|
+
.map(([name, meaning]) => `- ${name}: ${meaning}`)
|
|
119
|
+
.join("\n");
|
|
120
|
+
const updatedDetails = await formatGenerated(
|
|
121
|
+
details.replace(
|
|
122
|
+
/<!-- archetypes:start -->[\s\S]*?<!-- archetypes:end -->/,
|
|
123
|
+
`<!-- archetypes:start -->\n${archetypes}\n<!-- archetypes:end -->`,
|
|
124
|
+
),
|
|
125
|
+
detailsPath,
|
|
126
|
+
);
|
|
127
|
+
if (process.argv.includes("--check")) {
|
|
128
|
+
if (details !== updatedDetails) {
|
|
129
|
+
console.error(
|
|
130
|
+
"docs/reference/format-details.md: error: run node ci/content-format-reference.mjs to refresh the archetype reference",
|
|
131
|
+
);
|
|
132
|
+
process.exitCode = 1;
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
fs.writeFileSync(detailsPath, updatedDetails);
|
|
136
|
+
}
|
package/docs/api.md
CHANGED
|
@@ -353,6 +353,7 @@ An addressed content note accepts exactly these top-level keys, in this order: `
|
|
|
353
353
|
| ------------------------- | -------------------------------------------- | ---------------------------------------- | -------------------------------------------------------------------------------------------- |
|
|
354
354
|
| `DRAFT_TAG` | `const DRAFT_TAG` | — | naming the declared tag that marks a note as unfinished |
|
|
355
355
|
| `GM_TAG` | `const GM_TAG` | — | naming the declared tag that marks a GM-only note |
|
|
356
|
+
| `BEING_ARCHETYPES` | `const BEING_ARCHETYPES` | `Readonly<Record<string, string>>` | looking up the case-sensitive being archetypes and their meanings |
|
|
356
357
|
| `NOTE_VOCABULARY` | `const NOTE_VOCABULARY` | — | looking up, per content type, its `data:` field list and its closed `subType` values |
|
|
357
358
|
| `SHARED_DATA_FIELDS` | `const SHARED_DATA_FIELDS` | `readonly DataFieldSpec[]` | reading the `data:` keys every type accepts, whatever it is — the art slots legal everywhere |
|
|
358
359
|
| `DECLARED_TAGS` | `const DECLARED_TAGS` | — | looking up the tags a note type may declare, grouped |
|
|
@@ -21,7 +21,9 @@ data: {}
|
|
|
21
21
|
---
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
`name
|
|
24
|
+
`name` is a map with a required, nonempty `full` display name. Every note may also use `aliases` for an ordered list of other names. The note infobox lists aliases directly beneath the full name. For a `being` with `subType: character`, `given` and `clan` are optional name components. The first alias of a character or NPC is its nickname. Omit optional keys when they do not apply; `given`, `clan`, and alias entries must be nonempty strings when present. No other keys are accepted under `name`.
|
|
25
|
+
|
|
26
|
+
`description` is the short page summary. `tags` hold draft state and descriptive labels. The note type's classification belongs in `subType` where it is declared. `data` holds facts about the subject shared by systems. A YAML comment is the place for an author-only note: commented values remain valid YAML but do not enter the content index or generated documents.
|
|
25
27
|
|
|
26
28
|
Use `tags: [gm]` for a note intended only for the GM. It is absent from the public website and book. Foundry includes it only when its document routes to a pack with `private: true`. A note with `gm` that also creates a prose JournalEntry needs a private JournalEntry pack. Links from untagged notes to GM notes are errors; GM notes may link to one another.
|
|
27
29
|
|
|
@@ -117,7 +117,8 @@ Item link.
|
|
|
117
117
|
order** — the very table each `### type:` section below states. There is no
|
|
118
118
|
second list, so a key added to a type appears in the book, on the website and
|
|
119
119
|
in a compendium journal with no further edit. A `Name` row stands first on
|
|
120
|
-
every note, because every note has one.
|
|
120
|
+
every note, because every note has one. An `Aliases` row follows it when the
|
|
121
|
+
note names any aliases, preserving their authored order.
|
|
121
122
|
|
|
122
123
|
A few keys carry no row, and the reason in each case is one of exactly two —
|
|
123
124
|
the key is **machinery**, steering a build or an interface rather than
|
|
@@ -333,6 +334,13 @@ sohl: {}
|
|
|
333
334
|
Any other top-level key is an error. To keep an author-only note beside the
|
|
334
335
|
content, comment out its YAML lines with `#`. Comments remain valid YAML text
|
|
335
336
|
and do not become data in the JSONL index, generated pages, or Foundry documents.
|
|
337
|
+
|
|
338
|
+
`name` is a map with a required nonempty `full` string when present. It may
|
|
339
|
+
also contain `aliases`, an ordered list of nonempty strings. A being with
|
|
340
|
+
`subType: character` may additionally contain nonempty `given` and `clan`
|
|
341
|
+
strings. Other keys under `name` are errors. For a character or NPC, the first
|
|
342
|
+
alias is its nickname. A being's homes belong in `data.homes`.
|
|
343
|
+
|
|
336
344
|
There is no custom site metadata. `description` supplies the page's short
|
|
337
345
|
description; `tags` carries publication and classification tags.
|
|
338
346
|
|
|
@@ -2432,6 +2440,8 @@ For JournalEntries, the following rules apply:
|
|
|
2432
2440
|
|
|
2433
2441
|
**Archetypes** describe characters in broad terms and help match them to an adventure. The list is:
|
|
2434
2442
|
|
|
2443
|
+
<!-- archetypes:start -->
|
|
2444
|
+
|
|
2435
2445
|
- warrior: Can hold a line and win a fight.
|
|
2436
2446
|
- skirmisher: Fights light — ambush, missile, mobility.
|
|
2437
2447
|
- infiltrator: Gets in unseen — locks, stealth, disguise.
|
|
@@ -2448,6 +2458,10 @@ For JournalEntries, the following rules apply:
|
|
|
2448
2458
|
- entertainer: Performs for an audience through acting, music, comedy, or similar arts.
|
|
2449
2459
|
- guildsperson: Has professional training, standing, or connections that open doors.
|
|
2450
2460
|
|
|
2461
|
+
<!-- archetypes:end -->
|
|
2462
|
+
|
|
2463
|
+
`character` and `npc` beings require at least one archetype. `creature` beings may omit archetypes or write an empty array. Values use the exact lowercase spellings above.
|
|
2464
|
+
|
|
2451
2465
|
Note that archetypes are descriptive, not proscriptive, and a character may be described by multiple archetypes at once.
|
|
2452
2466
|
`commoner` stands alone: a being with another fitting archetype does not also carry `commoner`.
|
|
2453
2467
|
Guild membership alone does not require `guildsperson`; the being's training, standing, or connections must matter to the role.
|
|
@@ -2462,34 +2476,34 @@ An Item can mark its readable description with `# ... {#description}`. That head
|
|
|
2462
2476
|
|
|
2463
2477
|
Generates a living (or undead, or spirit) being.
|
|
2464
2478
|
|
|
2465
|
-
| `data` property | Values | Description
|
|
2466
|
-
| --------------------------- | ---------------------------------------------- |
|
|
2467
|
-
| `tokenIcon` | `Address` | What a token on the canvas wears — an `icon` address; defaults to `icon`
|
|
2468
|
-
| `templatePriority` | `number` | Template priority, _null_ = not a template
|
|
2469
|
-
| `archetypes` | `Archetype[]` | What sort of character this is. **Always an array** —
|
|
2470
|
-
| `occupation` | `string` | Name of the character's occupation
|
|
2471
|
-
| `social` | `Map<string, unknown>` | The being's social profile
|
|
2472
|
-
| `stations` | `Address[]` | Name of the stations the character belongs to
|
|
2473
|
-
| `lore` | `Address[]` | Other lore concerning this being, such as social standing or law
|
|
2474
|
-
| `culture` | `Address` | Primary culture, naming a lore note with `subType: culture`
|
|
2475
|
-
| `homes` | `Address[]` | Place the being calls home
|
|
2476
|
-
| `affiliations` | `Address[]` | Affilliations (e.g., arcane/divine traditions, polities, etc)
|
|
2477
|
-
| `socialTies` | `Map<Address, Tie>` | Defining support and opposition, directed from this being to others
|
|
2478
|
-
| `gender` | `male \| female \| other` | Gender of the character
|
|
2479
|
-
| `species` | `Address` | Being's species (lore)
|
|
2480
|
-
| `born` | `date \| unknown` | When the being was born; absent, it was never born
|
|
2481
|
-
| `calendar` | `Address` | Calendar note used to print the being's dates
|
|
2482
|
-
| `died` | `date \| unknown` | When the being died; absent, it is alive
|
|
2483
|
-
| `age` | `34 \| ~34` | Age in years, stated only to override what `born` says; `~` marks an estimate
|
|
2484
|
-
| `ageYears` | `number` | Written by the compiler beside an `age` estimate — the `~` stripped; a note never authors this
|
|
2485
|
-
| `height` | `1.91m \| 6' 3"` | Height in metres or feet and inches
|
|
2486
|
-
| `weight` | `85kg \| 187 lbs` | Body weight in kilograms or pounds
|
|
2487
|
-
| `frame` | `scant \| light \| medium \| large \| massive` | Relative frame size
|
|
2488
|
-
| `appearance.eye_color` | `string` | Eye color
|
|
2489
|
-
| `appearance.hair_color` | `string` | Hair color
|
|
2490
|
-
| `appearance.skin_color` | `string` | Skin color
|
|
2491
|
-
| `appearance.complexion` | `string` | Complexion
|
|
2492
|
-
| `appearance.extra_features` | `string[]` | Extra features
|
|
2479
|
+
| `data` property | Values | Description |
|
|
2480
|
+
| --------------------------- | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
2481
|
+
| `tokenIcon` | `Address` | What a token on the canvas wears — an `icon` address; defaults to `icon` |
|
|
2482
|
+
| `templatePriority` | `number` | Template priority, _null_ = not a template |
|
|
2483
|
+
| `archetypes` | `Archetype[]` | What sort of character this is. **Always an array** — character and npc require at least one; creature may omit it or write `[]`; `null` is an error. |
|
|
2484
|
+
| `occupation` | `string` | Name of the character's occupation |
|
|
2485
|
+
| `social` | `Map<string, unknown>` | The being's social profile |
|
|
2486
|
+
| `stations` | `Address[]` | Name of the stations the character belongs to |
|
|
2487
|
+
| `lore` | `Address[]` | Other lore concerning this being, such as social standing or law |
|
|
2488
|
+
| `culture` | `Address` | Primary culture, naming a lore note with `subType: culture` |
|
|
2489
|
+
| `homes` | `Address[]` | Place the being calls home |
|
|
2490
|
+
| `affiliations` | `Address[]` | Affilliations (e.g., arcane/divine traditions, polities, etc) |
|
|
2491
|
+
| `socialTies` | `Map<Address, Tie>` | Defining support and opposition, directed from this being to others |
|
|
2492
|
+
| `gender` | `male \| female \| other` | Gender of the character |
|
|
2493
|
+
| `species` | `Address` | Being's species (lore) |
|
|
2494
|
+
| `born` | `date \| unknown` | When the being was born; absent, it was never born |
|
|
2495
|
+
| `calendar` | `Address` | Calendar note used to print the being's dates |
|
|
2496
|
+
| `died` | `date \| unknown` | When the being died; absent, it is alive |
|
|
2497
|
+
| `age` | `34 \| ~34` | Age in years, stated only to override what `born` says; `~` marks an estimate |
|
|
2498
|
+
| `ageYears` | `number` | Written by the compiler beside an `age` estimate — the `~` stripped; a note never authors this |
|
|
2499
|
+
| `height` | `1.91m \| 6' 3"` | Height in metres or feet and inches |
|
|
2500
|
+
| `weight` | `85kg \| 187 lbs` | Body weight in kilograms or pounds |
|
|
2501
|
+
| `frame` | `scant \| light \| medium \| large \| massive` | Relative frame size |
|
|
2502
|
+
| `appearance.eye_color` | `string` | Eye color |
|
|
2503
|
+
| `appearance.hair_color` | `string` | Hair color |
|
|
2504
|
+
| `appearance.skin_color` | `string` | Skin color |
|
|
2505
|
+
| `appearance.complexion` | `string` | Complexion |
|
|
2506
|
+
| `appearance.extra_features` | `string[]` | Extra features |
|
|
2493
2507
|
|
|
2494
2508
|
Author `data.height` as a string of decimal metres (`1.91m` or `1.91 m`) or
|
|
2495
2509
|
whole feet with optional inches (`6'`, `6'3"`, or `6' 3"`). Inches must be
|
package/engine/base-compiler.mjs
CHANGED
|
@@ -1133,8 +1133,8 @@ export class BasePackCompiler {
|
|
|
1133
1133
|
// - `aliases:`: it fed the alias index, which the bare
|
|
1134
1134
|
// `[[Alias]]` form was looked up in; the form is retired, so the
|
|
1135
1135
|
// list has no reader left. The nested `name.aliases` is a
|
|
1136
|
-
// different field
|
|
1137
|
-
//
|
|
1136
|
+
// different field: the infobox displays it, and the note name
|
|
1137
|
+
// check validates it.
|
|
1138
1138
|
//
|
|
1139
1139
|
// Both are reported and counted — never skipped, which is how a
|
|
1140
1140
|
// tree naming a package nothing answers to used to compile zero
|
|
@@ -55,7 +55,12 @@
|
|
|
55
55
|
*/
|
|
56
56
|
|
|
57
57
|
import { isAddressTuple } from "./address.mjs";
|
|
58
|
-
import {
|
|
58
|
+
import {
|
|
59
|
+
authoredNoteKeys,
|
|
60
|
+
CHARACTER_NAME_KEYS,
|
|
61
|
+
NOTE_NAME_KEYS,
|
|
62
|
+
NOTE_TOP_LEVEL_KEY_SET,
|
|
63
|
+
} from "./note-frontmatter.mjs";
|
|
59
64
|
import { AddressEntries } from "./address-values.mjs";
|
|
60
65
|
import { addressPositions } from "./note-addresses.mjs";
|
|
61
66
|
import { authoredFields, readsLegacyKey } from "./field-spec.mjs";
|
|
@@ -78,6 +83,7 @@ import { isAddressSegment } from "./address-charset.mjs";
|
|
|
78
83
|
// reads is exactly the disagreement to avoid.
|
|
79
84
|
import { DEFAULT_PARENT } from "./folder-notes.mjs";
|
|
80
85
|
import {
|
|
86
|
+
BEING_ARCHETYPES,
|
|
81
87
|
dataFields,
|
|
82
88
|
declaredTags,
|
|
83
89
|
subTypeCharsetMessage,
|
|
@@ -1173,18 +1179,101 @@ export function lintNote(
|
|
|
1173
1179
|
const type = String(fm.type ?? "");
|
|
1174
1180
|
const raw = () => note.raw ?? "";
|
|
1175
1181
|
const at = (key, literal) => positionInFrontmatter(raw(), key, literal ?? undefined);
|
|
1176
|
-
if (
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1182
|
+
if (type === "being") {
|
|
1183
|
+
const archetypes = fm.data?.archetypes;
|
|
1184
|
+
if (
|
|
1185
|
+
["character", "npc"].includes(fm.subType) &&
|
|
1186
|
+
(archetypes == null || (Array.isArray(archetypes) && archetypes.length === 0))
|
|
1187
|
+
) {
|
|
1188
|
+
findings.push({
|
|
1189
|
+
file: note.file,
|
|
1190
|
+
...positionOfFrontmatterPath(
|
|
1191
|
+
raw(),
|
|
1192
|
+
archetypes == null ? ["subType"] : ["data", "archetypes"],
|
|
1193
|
+
),
|
|
1194
|
+
severity: "error",
|
|
1195
|
+
message: "A character or npc requires at least one `data.archetypes` value",
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
if (Array.isArray(archetypes)) {
|
|
1199
|
+
archetypes.forEach((value, i) => {
|
|
1200
|
+
if (!Object.hasOwn(BEING_ARCHETYPES, value)) {
|
|
1201
|
+
findings.push({
|
|
1202
|
+
file: note.file,
|
|
1203
|
+
...positionOfFrontmatterPath(raw(), ["data", "archetypes", i]),
|
|
1204
|
+
severity: "error",
|
|
1205
|
+
message: `Unknown archetype \`${String(value)}\`; use a case-sensitive archetype from the reference`,
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
});
|
|
1209
|
+
const commoner = archetypes.indexOf("commoner");
|
|
1210
|
+
if (commoner !== -1 && archetypes.length > 1) {
|
|
1211
|
+
findings.push({
|
|
1212
|
+
file: note.file,
|
|
1213
|
+
...positionOfFrontmatterPath(raw(), ["data", "archetypes", commoner]),
|
|
1214
|
+
severity: "error",
|
|
1215
|
+
message: "`data.archetypes` may contain `commoner` only by itself",
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
if (Object.hasOwn(fm, "name")) {
|
|
1221
|
+
const name = fm.name;
|
|
1222
|
+
const nameAt = (path) => positionOfFrontmatterPath(raw(), ["name", ...path], { key: true });
|
|
1223
|
+
if (!name || typeof name !== "object" || Array.isArray(name)) {
|
|
1224
|
+
findings.push({
|
|
1225
|
+
file: note.file,
|
|
1226
|
+
...positionOfFrontmatterPath(raw(), ["name"], { key: true }),
|
|
1227
|
+
severity: "error",
|
|
1228
|
+
message: "`name` must be a map with a nonempty `full` string",
|
|
1229
|
+
});
|
|
1230
|
+
} else {
|
|
1231
|
+
const allowed = new Set(NOTE_NAME_KEYS);
|
|
1232
|
+
if (type === "being" && fm.subType === "character") {
|
|
1233
|
+
for (const key of CHARACTER_NAME_KEYS) allowed.add(key);
|
|
1234
|
+
}
|
|
1235
|
+
for (const key of Object.keys(name)) {
|
|
1236
|
+
if (!allowed.has(key)) {
|
|
1237
|
+
findings.push({
|
|
1238
|
+
file: note.file,
|
|
1239
|
+
...nameAt([key]),
|
|
1240
|
+
severity: "error",
|
|
1241
|
+
message: `\`name.${key}\` is not valid for this note`,
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
if (typeof name.full !== "string" || !name.full.trim()) {
|
|
1246
|
+
findings.push({
|
|
1247
|
+
file: note.file,
|
|
1248
|
+
...(Object.hasOwn(name, "full") ? nameAt(["full"]) : nameAt([])),
|
|
1249
|
+
severity: "error",
|
|
1250
|
+
message: "`name.full` must be a nonempty string",
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
if (Object.hasOwn(name, "aliases")) {
|
|
1254
|
+
if (
|
|
1255
|
+
!Array.isArray(name.aliases) ||
|
|
1256
|
+
name.aliases.some((alias) => typeof alias !== "string" || !alias.trim())
|
|
1257
|
+
) {
|
|
1258
|
+
findings.push({
|
|
1259
|
+
file: note.file,
|
|
1260
|
+
...nameAt(["aliases"]),
|
|
1261
|
+
severity: "error",
|
|
1262
|
+
message: "`name.aliases` must be a list of nonempty strings",
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
for (const key of CHARACTER_NAME_KEYS) {
|
|
1267
|
+
if (!Object.hasOwn(name, key) || !allowed.has(key)) continue;
|
|
1268
|
+
if (typeof name[key] === "string" && name[key].trim()) continue;
|
|
1269
|
+
findings.push({
|
|
1270
|
+
file: note.file,
|
|
1271
|
+
...nameAt([key]),
|
|
1272
|
+
severity: "error",
|
|
1273
|
+
message: `\`name.${key}\` must be a nonempty string`,
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1188
1277
|
}
|
|
1189
1278
|
/**
|
|
1190
1279
|
* The in-block keys this note's own type claims for something other than
|
package/engine/infobox.mjs
CHANGED
|
@@ -706,6 +706,9 @@ function noteBox(
|
|
|
706
706
|
const shown = new Set();
|
|
707
707
|
const name = fm?.name?.full ?? fm?.title;
|
|
708
708
|
if (hasValue(name)) rows.push({ label: "Name", kind: "text", value: String(name) });
|
|
709
|
+
if (Array.isArray(fm?.name?.aliases) && fm.name.aliases.length) {
|
|
710
|
+
rows.push({ label: "Aliases", kind: "list", value: fm.name.aliases });
|
|
711
|
+
}
|
|
709
712
|
if (fm?.type === "being" && Object.hasOwn(BEING_SUBTYPE_LABELS, fm.subType)) {
|
|
710
713
|
rows.push({ label: "Type", kind: "text", value: BEING_SUBTYPE_LABELS[fm.subType] });
|
|
711
714
|
}
|
|
@@ -24,6 +24,12 @@ export const NOTE_TOP_LEVEL_KEYS = Object.freeze([
|
|
|
24
24
|
/** Membership test for the top-level vocabulary. */
|
|
25
25
|
export const NOTE_TOP_LEVEL_KEY_SET = new Set(NOTE_TOP_LEVEL_KEYS);
|
|
26
26
|
|
|
27
|
+
/** The name properties every note accepts, in display order. */
|
|
28
|
+
export const NOTE_NAME_KEYS = Object.freeze(["full", "aliases"]);
|
|
29
|
+
|
|
30
|
+
/** Additional name components accepted by an individual character. */
|
|
31
|
+
export const CHARACTER_NAME_KEYS = Object.freeze(["given", "clan"]);
|
|
32
|
+
|
|
27
33
|
/** Return authored keys from an opening YAML frontmatter map. */
|
|
28
34
|
export function authoredNoteKeys(markdown) {
|
|
29
35
|
const source = markdown.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/)?.[1];
|
|
@@ -666,6 +666,25 @@ export function isGmNote(fm) {
|
|
|
666
666
|
return hasTag(fm, GM_TAG);
|
|
667
667
|
}
|
|
668
668
|
|
|
669
|
+
/** The case-sensitive archetypes and their author-facing meanings. */
|
|
670
|
+
export const BEING_ARCHETYPES = Object.freeze({
|
|
671
|
+
warrior: "Can hold a line and win a fight.",
|
|
672
|
+
skirmisher: "Fights light — ambush, missile, mobility.",
|
|
673
|
+
infiltrator: "Gets in unseen — locks, stealth, disguise.",
|
|
674
|
+
mage: "Commands arcane practice.",
|
|
675
|
+
cleric: "Commands religious practice and standing.",
|
|
676
|
+
healer: "Treats wounds and illness.",
|
|
677
|
+
scholar: "Reads, researches, and knows things.",
|
|
678
|
+
courtier: "Navigates rank, negotiation, and intrigue.",
|
|
679
|
+
woodsman: "Travels and survives wild country.",
|
|
680
|
+
mariner: "Handles boats and blue water.",
|
|
681
|
+
artisan: "Builds, repairs, and appraises craft work.",
|
|
682
|
+
trader: "Moves goods, values them, and knows markets.",
|
|
683
|
+
commoner: "Fits no more specific archetype; handles ordinary work and daily life.",
|
|
684
|
+
entertainer: "Performs for an audience through acting, music, comedy, or similar arts.",
|
|
685
|
+
guildsperson: "Has professional training, standing, or connections that open doors.",
|
|
686
|
+
});
|
|
687
|
+
|
|
669
688
|
/**
|
|
670
689
|
* Every note type this toolchain compiles, and the closed vocabulary it
|
|
671
690
|
* declares.
|
|
@@ -48,13 +48,8 @@
|
|
|
48
48
|
* nothing checks unrecognized top-level keys, so left in place it would be
|
|
49
49
|
* silently ignored rather than reported.
|
|
50
50
|
*
|
|
51
|
-
* **`name.aliases`
|
|
52
|
-
*
|
|
53
|
-
* field here that is neither retired nor read. Nothing consults it: no index,
|
|
54
|
-
* no resolver, no lint rule, no emitter. A note carrying one compiles,
|
|
55
|
-
* resolves and emits exactly as if it were absent, and `tests/name-aliases-
|
|
56
|
-
* reserved.test.ts` pins that equivalence so a future reader cannot be added
|
|
57
|
-
* by accident.
|
|
51
|
+
* **`name.aliases` lists alternate names.** The note infobox shows them in
|
|
52
|
+
* authored order. They do not create addresses or change link resolution.
|
|
58
53
|
*
|
|
59
54
|
* **What `traits:` did.** It held a being's own description — gender,
|
|
60
55
|
* species, age, birth date, height, weight, frame and `appearance.*` — at the
|
|
@@ -199,10 +194,8 @@ export function assertNoDraftField(fm, { file, absPath } = {}) {
|
|
|
199
194
|
* unambiguous folded in every note's `name.full` and so dictated what a note
|
|
200
195
|
* could be named. The form is retired, so the list has no reader.
|
|
201
196
|
*
|
|
202
|
-
* **`name.aliases` is a different field
|
|
203
|
-
*
|
|
204
|
-
* and deliberately unmentioned by this message, which would otherwise tell an
|
|
205
|
-
* author to delete a field they are allowed to write. See
|
|
197
|
+
* **`name.aliases` is a different field.** It lists alternate names in the
|
|
198
|
+
* infobox and is deliberately unmentioned by this message. See
|
|
206
199
|
* {@link assertNoAliasesField}.
|
|
207
200
|
*
|
|
208
201
|
* @param {string} [file] - The note's path, named in the message. Omit it where
|
|
@@ -227,13 +220,9 @@ export function aliasesRetiredMessage(file) {
|
|
|
227
220
|
* it reads as "this note claims no other names", a statement about a namespace
|
|
228
221
|
* that no longer exists.
|
|
229
222
|
*
|
|
230
|
-
* **The nested `name.aliases` is deliberately not refused.**
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
* does not exist yet. So it is neither refused nor read — no index consults it,
|
|
234
|
-
* no rule validates its contents, nothing derives from it, and nothing emits
|
|
235
|
-
* it. It rides in the note as inert data, and a note carrying one compiles,
|
|
236
|
-
* resolves and emits exactly as if it were absent.
|
|
223
|
+
* **The nested `name.aliases` is deliberately not refused.** It is the note's
|
|
224
|
+
* ordered alternate names and appears in the note infobox. It does not create
|
|
225
|
+
* addresses or change how authored links resolve.
|
|
237
226
|
*
|
|
238
227
|
* That distinction is the reason this checks `Object.hasOwn(fm, "aliases")`
|
|
239
228
|
* rather than resolving a dotted key: the top-level field is the whole subject,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroiclands/package-build",
|
|
3
|
-
"version": "22.
|
|
3
|
+
"version": "22.20.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",
|
|
@@ -4,3 +4,7 @@ export function authoredNoteKeys(markdown: any): string[];
|
|
|
4
4
|
export const NOTE_TOP_LEVEL_KEYS: readonly string[];
|
|
5
5
|
/** Membership test for the top-level vocabulary. */
|
|
6
6
|
export const NOTE_TOP_LEVEL_KEY_SET: Set<string>;
|
|
7
|
+
/** The name properties every note accepts, in display order. */
|
|
8
|
+
export const NOTE_NAME_KEYS: readonly string[];
|
|
9
|
+
/** Additional name components accepted by an individual character. */
|
|
10
|
+
export const CHARACTER_NAME_KEYS: readonly string[];
|
|
@@ -244,6 +244,24 @@ export const DECLARED_TAGS: Readonly<{
|
|
|
244
244
|
tags: readonly string[];
|
|
245
245
|
}>;
|
|
246
246
|
}>;
|
|
247
|
+
/** The case-sensitive archetypes and their author-facing meanings. */
|
|
248
|
+
export const BEING_ARCHETYPES: Readonly<{
|
|
249
|
+
warrior: "Can hold a line and win a fight.";
|
|
250
|
+
skirmisher: "Fights light — ambush, missile, mobility.";
|
|
251
|
+
infiltrator: "Gets in unseen — locks, stealth, disguise.";
|
|
252
|
+
mage: "Commands arcane practice.";
|
|
253
|
+
cleric: "Commands religious practice and standing.";
|
|
254
|
+
healer: "Treats wounds and illness.";
|
|
255
|
+
scholar: "Reads, researches, and knows things.";
|
|
256
|
+
courtier: "Navigates rank, negotiation, and intrigue.";
|
|
257
|
+
woodsman: "Travels and survives wild country.";
|
|
258
|
+
mariner: "Handles boats and blue water.";
|
|
259
|
+
artisan: "Builds, repairs, and appraises craft work.";
|
|
260
|
+
trader: "Moves goods, values them, and knows markets.";
|
|
261
|
+
commoner: "Fits no more specific archetype; handles ordinary work and daily life.";
|
|
262
|
+
entertainer: "Performs for an audience through acting, music, comedy, or similar arts.";
|
|
263
|
+
guildsperson: "Has professional training, standing, or connections that open doors.";
|
|
264
|
+
}>;
|
|
247
265
|
/**
|
|
248
266
|
* Every note type this toolchain compiles, and the closed vocabulary it
|
|
249
267
|
* declares.
|
|
@@ -49,10 +49,8 @@ export function assertNoDraftField(fm: object | null | undefined, { file, absPat
|
|
|
49
49
|
* unambiguous folded in every note's `name.full` and so dictated what a note
|
|
50
50
|
* could be named. The form is retired, so the list has no reader.
|
|
51
51
|
*
|
|
52
|
-
* **`name.aliases` is a different field
|
|
53
|
-
*
|
|
54
|
-
* and deliberately unmentioned by this message, which would otherwise tell an
|
|
55
|
-
* author to delete a field they are allowed to write. See
|
|
52
|
+
* **`name.aliases` is a different field.** It lists alternate names in the
|
|
53
|
+
* infobox and is deliberately unmentioned by this message. See
|
|
56
54
|
* {@link assertNoAliasesField}.
|
|
57
55
|
*
|
|
58
56
|
* @param {string} [file] - The note's path, named in the message. Omit it where
|
|
@@ -68,13 +66,9 @@ export function aliasesRetiredMessage(file?: string): string;
|
|
|
68
66
|
* it reads as "this note claims no other names", a statement about a namespace
|
|
69
67
|
* that no longer exists.
|
|
70
68
|
*
|
|
71
|
-
* **The nested `name.aliases` is deliberately not refused.**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* does not exist yet. So it is neither refused nor read — no index consults it,
|
|
75
|
-
* no rule validates its contents, nothing derives from it, and nothing emits
|
|
76
|
-
* it. It rides in the note as inert data, and a note carrying one compiles,
|
|
77
|
-
* resolves and emits exactly as if it were absent.
|
|
69
|
+
* **The nested `name.aliases` is deliberately not refused.** It is the note's
|
|
70
|
+
* ordered alternate names and appears in the note infobox. It does not create
|
|
71
|
+
* addresses or change how authored links resolve.
|
|
78
72
|
*
|
|
79
73
|
* That distinction is the reason this checks `Object.hasOwn(fm, "aliases")`
|
|
80
74
|
* rather than resolving a dotted key: the top-level field is the whole subject,
|