@heroiclands/package-build 23.2.2 → 23.3.1
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 +29 -0
- package/bin/content-commands.mjs +293 -2
- package/ci/content-format-reference.mjs +28 -5
- package/docs/api.md +37 -23
- package/docs/authoring/frontmatter.md +2 -0
- package/docs/commands.md +183 -0
- package/docs/content-format.md +6 -0
- package/docs/guides/e2e.md +8 -1
- package/docs/reference/format-details.md +471 -46
- package/docs/reference/note-types.md +124 -107
- package/e2e.mjs +37 -10
- package/engine/appearance-profile.mjs +330 -0
- package/engine/content-format.yaml +8 -0
- package/engine/data-keys.mjs +26 -6
- package/engine/index.mjs +3 -0
- package/engine/infobox.mjs +1 -1
- package/engine/language-names.mjs +634 -0
- package/engine/literature-notes.mjs +62 -27
- package/engine/note-vocabulary.mjs +370 -19
- package/engine/npc-generate.mjs +633 -0
- package/engine/settlement-tables.mjs +777 -0
- package/package.json +1 -1
- package/types/e2e.d.mts +5 -1
- package/types/engine/appearance-profile.d.mts +94 -0
- package/types/engine/data-keys.d.mts +2 -2
- package/types/engine/index.d.mts +1 -0
- package/types/engine/infobox.d.mts +1 -1
- package/types/engine/language-names.d.mts +140 -0
- package/types/engine/literature-notes.d.mts +14 -3
- package/types/engine/note-vocabulary.d.mts +31 -0
- package/types/engine/npc-generate.d.mts +124 -0
- package/types/engine/settlement-tables.d.mts +164 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# @heroiclands/package-build
|
|
2
2
|
|
|
3
|
+
## 23.3.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
**New npcs**
|
|
8
|
+
|
|
9
|
+
- `package-build npc new` writes a complete npc note from a culture and a gender, filling every other field, an Appearance paragraph and a name; the same spec always gives the same person, and `--seed` draws another.
|
|
10
|
+
- Give any field yourself, inline or from a JSON or YAML file, including the name and the Appearance and Dossier text, and it is written exactly as you gave it.
|
|
11
|
+
- A culture describes its people in `data.appearanceProfile`, with weighted eye, hair and skin colours, complexions, frames and height ranges, and names the tongue they speak in `data.language`.
|
|
12
|
+
- A language skill note lists its speakers' names in `data.names`: one or several patterns such as `{given} {clan}` or `{given} {given.male}sen`, by gender where forms differ, and the lists they draw on; a name you give must fit one.
|
|
13
|
+
- `package-build lint` checks a profile's words, weights and ranges, and a language's pattern and lists.
|
|
14
|
+
|
|
15
|
+
## 23.3.0
|
|
16
|
+
|
|
17
|
+
### Minor Changes
|
|
18
|
+
|
|
19
|
+
**Being notes**
|
|
20
|
+
|
|
21
|
+
- A being's eye, hair and skin colour and its complexion now take words from a fixed list, alongside gender and frame, and any other value fails `package-build lint` naming the accepted words.
|
|
22
|
+
- Variant spellings and empty values are refused too: write `grey` rather than `gray`, and leave a field out when it is unrecorded.
|
|
23
|
+
- A complexion takes one word or a list of them; distinguishing marks stay free text in `extra_features`.
|
|
24
|
+
- The format reference lists every word with what it means.
|
|
25
|
+
|
|
26
|
+
### Patch Changes
|
|
27
|
+
|
|
28
|
+
A region can list its settlements in a table under `## Settlements` — name, market, people and holder, one row each — tagged with how its countryside is held: `held`, `endowed`, `registered`, `kin` or `polis`. `package-build lint` checks the table's columns, its market classes and figures, that a holder suits the region's model, and that every linked row agrees with the settlement's own note.
|
|
29
|
+
|
|
30
|
+
`e2e` now says when the Foundry licence is unsigned, and how to sign it, instead of timing out waiting for a world.
|
|
31
|
+
|
|
3
32
|
## 23.2.2
|
|
4
33
|
|
|
5
34
|
### Patch Changes
|
package/bin/content-commands.mjs
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
* npx package-build map [--tree] [--from <shortcode>] [--chart <shortcode>] [--travel] [--out <dir>]
|
|
32
32
|
* npx package-build reachability <dir> [file] [--index <shortcode>]
|
|
33
33
|
* npx package-build addresses diff --from <zip|dir> [--strict]
|
|
34
|
+
* npx package-build npc new (--spec <json> | --specfile <path>) [--seed <text>] [--out <path>]
|
|
34
35
|
*
|
|
35
36
|
* In a consuming repository, wrapped as npm scripts — SoHL spells them:
|
|
36
37
|
* npm run build:compiledb // → … package compile (all packs)
|
|
@@ -112,7 +113,12 @@ import { auditLinks, buildLinkIndex, walkReachability } from "../engine/content-
|
|
|
112
113
|
import { prepareTreeSqlTables } from "../engine/sql-tables.mjs";
|
|
113
114
|
// The one place a link finding is worded, shared with both builds.
|
|
114
115
|
import { linkFindingMessage } from "../engine/wikilink-syntax.mjs";
|
|
115
|
-
import {
|
|
116
|
+
import {
|
|
117
|
+
emitDiagnostic,
|
|
118
|
+
positionOfFrontmatterPath,
|
|
119
|
+
positionOfLiteral,
|
|
120
|
+
positionOfYamlPath,
|
|
121
|
+
} from "../engine/diagnostics.mjs";
|
|
116
122
|
import { reportFindings } from "./report.mjs";
|
|
117
123
|
import {
|
|
118
124
|
readItemAddresses,
|
|
@@ -127,6 +133,14 @@ import { CHART_HORIZON_DAYS } from "../engine/map-layout.mjs";
|
|
|
127
133
|
import { GRAPHVIZ_ENGINES } from "../engine/map-graphviz.mjs";
|
|
128
134
|
import { loadMapWorld } from "../engine/map-places.mjs";
|
|
129
135
|
import { encodeAddresses } from "../engine/address-values.mjs";
|
|
136
|
+
import YAML from "yaml";
|
|
137
|
+
import { worldInvariants } from "../engine/calendar-notes.mjs";
|
|
138
|
+
import { reckoningContext } from "../engine/reckoning-markers.mjs";
|
|
139
|
+
import { BEING_LOOK_VOCABULARY } from "../engine/note-vocabulary.mjs";
|
|
140
|
+
import { profileFindings } from "../engine/appearance-profile.mjs";
|
|
141
|
+
import { namesFindings } from "../engine/language-names.mjs";
|
|
142
|
+
import { parseAddress, renderAddress } from "../engine/address.mjs";
|
|
143
|
+
import { cultureKey, generateNpc, npcDefaults, npcSpecProblems } from "../engine/npc-generate.mjs";
|
|
130
144
|
|
|
131
145
|
/**
|
|
132
146
|
* The packs `unpack` extracts.
|
|
@@ -241,7 +255,8 @@ export function registerContentCommands(cli) {
|
|
|
241
255
|
)
|
|
242
256
|
.command(withIndexPreflight(mapCommand()))
|
|
243
257
|
.command(withIndexPreflight(reachabilityCommand()))
|
|
244
|
-
.command(addressesCommand())
|
|
258
|
+
.command(addressesCommand())
|
|
259
|
+
.command(withIndexPreflight(npcCommand()));
|
|
245
260
|
}
|
|
246
261
|
|
|
247
262
|
/** Optional sentence findings and note-level readability reports. */
|
|
@@ -490,6 +505,12 @@ function docsCommand() {
|
|
|
490
505
|
type: "string",
|
|
491
506
|
default: "docs",
|
|
492
507
|
});
|
|
508
|
+
yargs.option("seed", {
|
|
509
|
+
describe:
|
|
510
|
+
"Text that seeds generation beside the spec, to draw a different person " +
|
|
511
|
+
"from the same spec.",
|
|
512
|
+
type: "string",
|
|
513
|
+
});
|
|
493
514
|
yargs.option("out", {
|
|
494
515
|
describe: "Write to this file instead of the configured location.",
|
|
495
516
|
type: "string",
|
|
@@ -1555,6 +1576,12 @@ function contentIndexCommand() {
|
|
|
1555
1576
|
describe: "Content tree to read. Defaults to the configured contentBase.",
|
|
1556
1577
|
type: "string",
|
|
1557
1578
|
});
|
|
1579
|
+
yargs.option("seed", {
|
|
1580
|
+
describe:
|
|
1581
|
+
"Text that seeds generation beside the spec, to draw a different person " +
|
|
1582
|
+
"from the same spec.",
|
|
1583
|
+
type: "string",
|
|
1584
|
+
});
|
|
1558
1585
|
yargs.option("out", {
|
|
1559
1586
|
describe:
|
|
1560
1587
|
"Directory to write into. Defaults to the configured " +
|
|
@@ -2515,3 +2542,267 @@ function packageCommand() {
|
|
|
2515
2542
|
},
|
|
2516
2543
|
};
|
|
2517
2544
|
}
|
|
2545
|
+
|
|
2546
|
+
/**
|
|
2547
|
+
* Read an `npc new` spec from `--spec` or `--specfile`.
|
|
2548
|
+
*
|
|
2549
|
+
* @param {{spec?: string, specfile?: string}} argv
|
|
2550
|
+
* @returns {{spec: unknown, file?: string, text?: string}} The spec as parsed,
|
|
2551
|
+
* and the file and text it was read from where it came from a file.
|
|
2552
|
+
*/
|
|
2553
|
+
function readNpcSpec(argv) {
|
|
2554
|
+
if (argv.spec !== undefined) {
|
|
2555
|
+
try {
|
|
2556
|
+
return { spec: JSON.parse(argv.spec) };
|
|
2557
|
+
} catch (err) {
|
|
2558
|
+
throw new Error(`\`--spec\` is not JSON: ${err.message}`);
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
const file = argv.specfile;
|
|
2562
|
+
const ext = path.extname(file).toLowerCase();
|
|
2563
|
+
if (![".json", ".yaml", ".yml"].includes(ext))
|
|
2564
|
+
throw new Error(`\`--specfile\` ${file} must be a .json, .yaml or .yml file`);
|
|
2565
|
+
let text;
|
|
2566
|
+
try {
|
|
2567
|
+
text = fs.readFileSync(file, "utf8");
|
|
2568
|
+
} catch (err) {
|
|
2569
|
+
throw new Error(`\`--specfile\` ${file} cannot be read: ${err.message}`);
|
|
2570
|
+
}
|
|
2571
|
+
try {
|
|
2572
|
+
return { spec: ext === ".json" ? JSON.parse(text) : YAML.parse(text), file, text };
|
|
2573
|
+
} catch (err) {
|
|
2574
|
+
throw Object.assign(new Error(`${file}: error: ${err.message.split("\n")[0]}`), {
|
|
2575
|
+
located: true,
|
|
2576
|
+
});
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
|
|
2580
|
+
/**
|
|
2581
|
+
* An error located at a note, or at one key of its frontmatter where the note
|
|
2582
|
+
* states it.
|
|
2583
|
+
*
|
|
2584
|
+
* @param {{file?: string, raw?: string}} note - Where the note is.
|
|
2585
|
+
* @param {Array<string|number>} keyPath - The frontmatter key at fault, or the
|
|
2586
|
+
* map a missing key belongs in.
|
|
2587
|
+
* @param {string} message - What is wrong.
|
|
2588
|
+
* @returns {Error} The error, for `reportFailure` to print as a diagnostic.
|
|
2589
|
+
*/
|
|
2590
|
+
function noteError(note, keyPath, message) {
|
|
2591
|
+
if (!note.file) return new Error(message);
|
|
2592
|
+
return Object.assign(new Error(message), {
|
|
2593
|
+
file: note.file,
|
|
2594
|
+
keyPath,
|
|
2595
|
+
position: positionOfFrontmatterPath(note.raw ?? "", keyPath, { key: true }),
|
|
2596
|
+
});
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
/**
|
|
2600
|
+
* `npc new`: resolve the spec's culture through the content tree, then the
|
|
2601
|
+
* culture's appearance profile, its language and the language's name lists,
|
|
2602
|
+
* and write the note. A missing link at any step is an error naming the note
|
|
2603
|
+
* to fix.
|
|
2604
|
+
*
|
|
2605
|
+
* @param {object} config - The resolved pack configuration.
|
|
2606
|
+
* @param {{spec?: string, specfile?: string, seed?: string, out?: string}} argv
|
|
2607
|
+
*/
|
|
2608
|
+
function newNpc(config, argv) {
|
|
2609
|
+
const { spec, file: specFile, text: specText } = readNpcSpec(argv);
|
|
2610
|
+
const problems = npcSpecProblems(spec);
|
|
2611
|
+
if (problems.length) throw new Error(`npc new: ${problems.join("; ")}`);
|
|
2612
|
+
if (argv.out && fs.existsSync(argv.out))
|
|
2613
|
+
throw new Error(`${argv.out} already exists, and \`npc new\` never overwrites a note`);
|
|
2614
|
+
|
|
2615
|
+
const root = config.paths.content;
|
|
2616
|
+
const records = indexRecordsFor({
|
|
2617
|
+
contentBase: root,
|
|
2618
|
+
config,
|
|
2619
|
+
skipDirectories: config.skipDirectories,
|
|
2620
|
+
problems: [],
|
|
2621
|
+
});
|
|
2622
|
+
const index = buildLinkIndex(root, {
|
|
2623
|
+
config,
|
|
2624
|
+
records,
|
|
2625
|
+
skipDirectories: config.skipDirectories,
|
|
2626
|
+
});
|
|
2627
|
+
const located = (hit) => ({
|
|
2628
|
+
file: hit.file ? path.relative(process.cwd(), hit.file) : undefined,
|
|
2629
|
+
raw: hit.raw,
|
|
2630
|
+
});
|
|
2631
|
+
|
|
2632
|
+
const written = spec.data.culture;
|
|
2633
|
+
const key = cultureKey(written, index);
|
|
2634
|
+
const hit = key ? index.addressHit(key) : undefined;
|
|
2635
|
+
if (!hit)
|
|
2636
|
+
throw new Error(
|
|
2637
|
+
`\`data.culture\` ${JSON.stringify(written)} names no note in this package or ` +
|
|
2638
|
+
"its dependencies",
|
|
2639
|
+
);
|
|
2640
|
+
const culture = hit.fm ?? hit;
|
|
2641
|
+
if (String(culture.type ?? hit.type) !== "lore" || culture.subType !== "culture")
|
|
2642
|
+
throw new Error(
|
|
2643
|
+
`\`data.culture\` ${JSON.stringify(written)} is not a culture: it names a ` +
|
|
2644
|
+
`${culture.type ?? hit.type ?? "note"} of subType ${culture.subType ?? "none"}`,
|
|
2645
|
+
);
|
|
2646
|
+
const cultureNote = located(hit);
|
|
2647
|
+
const profile = culture.data?.appearanceProfile;
|
|
2648
|
+
if (profile === undefined || profile === null)
|
|
2649
|
+
throw noteError(
|
|
2650
|
+
cultureNote,
|
|
2651
|
+
["data"],
|
|
2652
|
+
`${key} states no \`data.appearanceProfile\`, so \`npc new\` cannot draw its ` +
|
|
2653
|
+
"people's look",
|
|
2654
|
+
);
|
|
2655
|
+
const profileErrors = profileFindings(
|
|
2656
|
+
{ file: cultureNote.file, raw: hit.raw, fm: culture },
|
|
2657
|
+
BEING_LOOK_VOCABULARY,
|
|
2658
|
+
);
|
|
2659
|
+
if (profileErrors.length) {
|
|
2660
|
+
for (const finding of profileErrors) emitDiagnostic(finding);
|
|
2661
|
+
throw new Error(`${key}'s appearance profile has ${profileErrors.length} problem(s)`);
|
|
2662
|
+
}
|
|
2663
|
+
|
|
2664
|
+
const tongue = culture.data?.language;
|
|
2665
|
+
if (tongue === undefined || tongue === null)
|
|
2666
|
+
throw noteError(
|
|
2667
|
+
cultureNote,
|
|
2668
|
+
["data"],
|
|
2669
|
+
`${key} states no \`data.language\`, so \`npc new\` has no name lists to draw ` +
|
|
2670
|
+
"from; name the language skill note its people speak",
|
|
2671
|
+
);
|
|
2672
|
+
const tuple = parseAddress(tongue, {
|
|
2673
|
+
package: index.contentPackage,
|
|
2674
|
+
system: "note",
|
|
2675
|
+
type: "skill",
|
|
2676
|
+
types: index.types,
|
|
2677
|
+
packages: index.packages,
|
|
2678
|
+
});
|
|
2679
|
+
const languageKey = tuple.reason ? undefined : renderAddress(tuple);
|
|
2680
|
+
const languageHit = languageKey ? index.addressHit(languageKey) : undefined;
|
|
2681
|
+
if (!languageHit)
|
|
2682
|
+
throw noteError(
|
|
2683
|
+
cultureNote,
|
|
2684
|
+
["data", "language"],
|
|
2685
|
+
`${key}'s \`data.language\` names no note in this package or its dependencies`,
|
|
2686
|
+
);
|
|
2687
|
+
const language = languageHit.fm ?? languageHit;
|
|
2688
|
+
if (String(language.type ?? languageHit.type) !== "skill" || language.subType !== "language")
|
|
2689
|
+
throw noteError(
|
|
2690
|
+
cultureNote,
|
|
2691
|
+
["data", "language"],
|
|
2692
|
+
`${key}'s \`data.language\` ${languageKey} is not a language: it names a ` +
|
|
2693
|
+
`${language.type ?? languageHit.type ?? "note"} of subType ` +
|
|
2694
|
+
`${language.subType ?? "none"}`,
|
|
2695
|
+
);
|
|
2696
|
+
const languageNote = located(languageHit);
|
|
2697
|
+
const names = language.data?.names;
|
|
2698
|
+
if (names === undefined || names === null)
|
|
2699
|
+
throw noteError(
|
|
2700
|
+
languageNote,
|
|
2701
|
+
["data"],
|
|
2702
|
+
`${languageKey} states no \`data.names\`, so \`npc new\` has no name lists to ` +
|
|
2703
|
+
`draw ${key}'s names from`,
|
|
2704
|
+
);
|
|
2705
|
+
const namesErrors = namesFindings(
|
|
2706
|
+
{ file: languageNote.file, raw: languageHit.raw, fm: language },
|
|
2707
|
+
BEING_LOOK_VOCABULARY,
|
|
2708
|
+
);
|
|
2709
|
+
if (namesErrors.length) {
|
|
2710
|
+
for (const finding of namesErrors) emitDiagnostic(finding);
|
|
2711
|
+
throw new Error(`${languageKey}'s name lists have ${namesErrors.length} problem(s)`);
|
|
2712
|
+
}
|
|
2713
|
+
|
|
2714
|
+
const { taken, ...defaults } = npcDefaults(index, key);
|
|
2715
|
+
if (typeof spec.shortcode === "string" && taken.has(spec.shortcode))
|
|
2716
|
+
throw new Error(`\`shortcode: ${spec.shortcode}\` already names a being in this package`);
|
|
2717
|
+
let text;
|
|
2718
|
+
try {
|
|
2719
|
+
({ text } = generateNpc(spec, {
|
|
2720
|
+
profile,
|
|
2721
|
+
names,
|
|
2722
|
+
language: languageKey,
|
|
2723
|
+
seed: argv.seed,
|
|
2724
|
+
present: worldInvariants(index).present,
|
|
2725
|
+
dateContext: reckoningContext(index),
|
|
2726
|
+
defaults,
|
|
2727
|
+
taken,
|
|
2728
|
+
}));
|
|
2729
|
+
} catch (err) {
|
|
2730
|
+
// A name that does not fit is the spec's fault, so a spec file
|
|
2731
|
+
// locates it at the name it states.
|
|
2732
|
+
if (specFile && Array.isArray(err?.keyPath))
|
|
2733
|
+
Object.assign(err, {
|
|
2734
|
+
file: specFile,
|
|
2735
|
+
position: positionOfYamlPath(specText ?? "", err.keyPath),
|
|
2736
|
+
});
|
|
2737
|
+
throw err;
|
|
2738
|
+
}
|
|
2739
|
+
if (!argv.out) {
|
|
2740
|
+
process.stdout.write(text);
|
|
2741
|
+
return;
|
|
2742
|
+
}
|
|
2743
|
+
fs.mkdirSync(path.dirname(argv.out), { recursive: true });
|
|
2744
|
+
try {
|
|
2745
|
+
fs.writeFileSync(argv.out, text, { flag: "wx" });
|
|
2746
|
+
} catch (err) {
|
|
2747
|
+
if (err?.code === "EEXIST")
|
|
2748
|
+
throw new Error(`${argv.out} already exists, and \`npc new\` never overwrites a note`);
|
|
2749
|
+
throw err;
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
|
|
2753
|
+
/**
|
|
2754
|
+
* `package-build npc new` — a complete NPC being note from a spec, its
|
|
2755
|
+
* appearance drawn from its culture's profile.
|
|
2756
|
+
*
|
|
2757
|
+
* @returns {object} The yargs command module.
|
|
2758
|
+
*/
|
|
2759
|
+
function npcCommand() {
|
|
2760
|
+
return {
|
|
2761
|
+
command: "npc <action>",
|
|
2762
|
+
describe:
|
|
2763
|
+
"Write a new NPC being note from a spec, its culture's appearance profile and " +
|
|
2764
|
+
"its language's name lists",
|
|
2765
|
+
builder: (yargs) => {
|
|
2766
|
+
yargs.positional("action", {
|
|
2767
|
+
describe: "The action to perform.",
|
|
2768
|
+
type: "string",
|
|
2769
|
+
choices: ["new"],
|
|
2770
|
+
});
|
|
2771
|
+
yargs.option("spec", {
|
|
2772
|
+
describe:
|
|
2773
|
+
"The spec as a JSON string: frontmatter fields, with `data.culture` and " +
|
|
2774
|
+
"`data.gender` required.",
|
|
2775
|
+
type: "string",
|
|
2776
|
+
});
|
|
2777
|
+
yargs.option("specfile", {
|
|
2778
|
+
describe: "The spec as a .json, .yaml or .yml file.",
|
|
2779
|
+
type: "string",
|
|
2780
|
+
});
|
|
2781
|
+
yargs.option("seed", {
|
|
2782
|
+
describe:
|
|
2783
|
+
"Text that seeds generation beside the spec, to draw a different person " +
|
|
2784
|
+
"from the same spec.",
|
|
2785
|
+
type: "string",
|
|
2786
|
+
});
|
|
2787
|
+
yargs.option("out", {
|
|
2788
|
+
describe:
|
|
2789
|
+
"Write the note to this new file instead of standard output. An " +
|
|
2790
|
+
"existing file is never overwritten.",
|
|
2791
|
+
type: "string",
|
|
2792
|
+
});
|
|
2793
|
+
yargs.check((parsed) => {
|
|
2794
|
+
if ((parsed.spec === undefined) === (parsed.specfile === undefined))
|
|
2795
|
+
throw new Error("`npc new` takes exactly one of `--spec` and `--specfile`");
|
|
2796
|
+
return true;
|
|
2797
|
+
});
|
|
2798
|
+
},
|
|
2799
|
+
handler: async (argv) => {
|
|
2800
|
+
try {
|
|
2801
|
+
newNpc(loadPackConfig(), argv);
|
|
2802
|
+
} catch (err) {
|
|
2803
|
+
reportFailure(err);
|
|
2804
|
+
process.exitCode = 1;
|
|
2805
|
+
}
|
|
2806
|
+
},
|
|
2807
|
+
};
|
|
2808
|
+
}
|
|
@@ -18,6 +18,7 @@ import { formatGenerated } from "../engine/format-generated.mjs";
|
|
|
18
18
|
import { loadContentFormat } from "../engine/content-format.mjs";
|
|
19
19
|
import {
|
|
20
20
|
BEING_ARCHETYPES,
|
|
21
|
+
BEING_LOOK_VOCABULARY,
|
|
21
22
|
NOTE_TOP_LEVEL_FIELDS,
|
|
22
23
|
NOTE_VOCABULARY,
|
|
23
24
|
SHARED_DATA_FIELDS,
|
|
@@ -236,20 +237,42 @@ const details = fs.readFileSync(detailsPath, "utf8");
|
|
|
236
237
|
if (!/<!-- archetypes:start -->[\s\S]*?<!-- archetypes:end -->/.test(details)) {
|
|
237
238
|
throw new Error("The archetype reference requires its generation markers");
|
|
238
239
|
}
|
|
240
|
+
if (!/<!-- being-look:start -->[\s\S]*?<!-- being-look:end -->/.test(details)) {
|
|
241
|
+
throw new Error("The being-look reference requires its generation markers");
|
|
242
|
+
}
|
|
239
243
|
const archetypes = Object.entries(BEING_ARCHETYPES)
|
|
240
244
|
.map(([name, meaning]) => `- ${name}: ${meaning}`)
|
|
241
245
|
.join("\n");
|
|
246
|
+
// One table per field of a being's look, every word with its label and meaning,
|
|
247
|
+
// written from the declaration the lint closes each field to.
|
|
248
|
+
const beingLook = Object.entries(BEING_LOOK_VOCABULARY)
|
|
249
|
+
.flatMap(([field, words]) => [
|
|
250
|
+
`**\`data.${field}\`**`,
|
|
251
|
+
"",
|
|
252
|
+
"| Word | Label | Meaning |",
|
|
253
|
+
"| --- | --- | --- |",
|
|
254
|
+
...words.map(
|
|
255
|
+
({ value, label, describe }) => `| \`${value}\` | ${cell(label)} | ${cell(describe)} |`,
|
|
256
|
+
),
|
|
257
|
+
"",
|
|
258
|
+
])
|
|
259
|
+
.join("\n");
|
|
242
260
|
const updatedDetails = await formatGenerated(
|
|
243
|
-
details
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
261
|
+
details
|
|
262
|
+
.replace(
|
|
263
|
+
/<!-- archetypes:start -->[\s\S]*?<!-- archetypes:end -->/,
|
|
264
|
+
`<!-- archetypes:start -->\n${archetypes}\n<!-- archetypes:end -->`,
|
|
265
|
+
)
|
|
266
|
+
.replace(
|
|
267
|
+
/<!-- being-look:start -->[\s\S]*?<!-- being-look:end -->/,
|
|
268
|
+
`<!-- being-look:start -->\n\n${beingLook}\n<!-- being-look:end -->`,
|
|
269
|
+
),
|
|
247
270
|
detailsPath,
|
|
248
271
|
);
|
|
249
272
|
if (process.argv.includes("--check")) {
|
|
250
273
|
if (details !== updatedDetails) {
|
|
251
274
|
console.error(
|
|
252
|
-
"docs/reference/format-details.md: error: run node ci/content-format-reference.mjs to refresh the archetype
|
|
275
|
+
"docs/reference/format-details.md: error: run node ci/content-format-reference.mjs to refresh the archetype and being-look references",
|
|
253
276
|
);
|
|
254
277
|
process.exitCode = 1;
|
|
255
278
|
}
|
package/docs/api.md
CHANGED
|
@@ -323,29 +323,30 @@ The note types the engine itself declares — vocabulary that is a fact about th
|
|
|
323
323
|
|
|
324
324
|
An addressed content note accepts exactly these top-level keys, in this order: `shortcode`, `name`, `type`, `subType`, `description`, `tags`, `data`, `dnd5e`, `hm3`, `sohl`. A key outside this list is an error at its own line. The system blocks are one per system the address vocabulary recognizes, sorted by name after the fixed keys, so a further system is a change to `SYSTEM_IDS` alone. No dnd5e-specific document types are declared. Within that shape, the closed vocabularies are the `data:` container and each type's `subType`.
|
|
325
325
|
|
|
326
|
-
| Export | Signature | Returns
|
|
327
|
-
| ------------------------- | -------------------------------------------- |
|
|
328
|
-
| `DRAFT_TAG` | `const DRAFT_TAG` | —
|
|
329
|
-
| `GM_TAG` | `const GM_TAG` | —
|
|
330
|
-
| `BEING_ARCHETYPES` | `const BEING_ARCHETYPES` | `Readonly<Record<string, string>>`
|
|
331
|
-
| `
|
|
332
|
-
| `
|
|
333
|
-
| `
|
|
334
|
-
| `
|
|
335
|
-
| `
|
|
336
|
-
| `
|
|
337
|
-
| `
|
|
338
|
-
| `
|
|
339
|
-
| `
|
|
340
|
-
| `
|
|
341
|
-
| `
|
|
342
|
-
| `
|
|
343
|
-
| `
|
|
344
|
-
| `
|
|
345
|
-
| `
|
|
346
|
-
| `
|
|
347
|
-
| `
|
|
348
|
-
| `
|
|
326
|
+
| Export | Signature | Returns | Use it when |
|
|
327
|
+
| ------------------------- | -------------------------------------------- | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
|
328
|
+
| `DRAFT_TAG` | `const DRAFT_TAG` | — | naming the declared tag that marks a note as unfinished |
|
|
329
|
+
| `GM_TAG` | `const GM_TAG` | — | naming the declared tag that marks a GM-only note |
|
|
330
|
+
| `BEING_ARCHETYPES` | `const BEING_ARCHETYPES` | `Readonly<Record<string, string>>` | looking up the case-sensitive being archetypes and their meanings |
|
|
331
|
+
| `BEING_LOOK_VOCABULARY` | `const BEING_LOOK_VOCABULARY` | `Readonly<Record<string, readonly LookWord[]>>` | reading the closed words a being's gender, frame and appearance take, each with its label and meaning, keyed by `data:` field |
|
|
332
|
+
| `NOTE_VOCABULARY` | `const NOTE_VOCABULARY` | — | looking up, per content type, its `data:` field list and its closed `subType` values |
|
|
333
|
+
| `NOTE_TOP_LEVEL_FIELDS` | `const NOTE_TOP_LEVEL_FIELDS` | `readonly TopLevelFieldSpec[]` | reading the top-level keys a note accepts, in formatted order, with what each one means |
|
|
334
|
+
| `NOTE_TOP_LEVEL_KEYS` | `const NOTE_TOP_LEVEL_KEYS` | `readonly string[]` | reading just the key names, in the order the formatter writes them |
|
|
335
|
+
| `NOTE_TOP_LEVEL_KEY_SET` | `const NOTE_TOP_LEVEL_KEY_SET` | `ReadonlySet<string>` | testing whether a top-level key is one the note format declares |
|
|
336
|
+
| `SHARED_DATA_FIELDS` | `const SHARED_DATA_FIELDS` | `readonly DataFieldSpec[]` | reading the `data:` keys every type accepts, whatever it is — the art slots legal everywhere |
|
|
337
|
+
| `EVENTS_FIELD` | `const EVENTS_FIELD` | `DataFieldSpec` | reading the one `events` declaration that `lore`, `place` and `affiliation` share, with its check |
|
|
338
|
+
| `DECLARED_TAGS` | `const DECLARED_TAGS` | — | looking up the tags a note type may declare, grouped |
|
|
339
|
+
| `declaredTags` | `declaredTags(type, groups)` | `readonly string[]` | reading the declared tags a note of a type may carry, flattened |
|
|
340
|
+
| `applicableTagGroups` | `applicableTagGroups(type, groups)` | `object[]` | reading the declared tag groups that apply to a note type |
|
|
341
|
+
| `exclusiveTagGroups` | `exclusiveTagGroups(type, groups)` | `Array<{slot, tags}>` | reading single-valued tag slots declared for a note type |
|
|
342
|
+
| `hasTag` | `hasTag(fm, tag)` | `boolean` | checking whether a note carries a given tag, whatever scalar-or-list form it was authored in |
|
|
343
|
+
| `isDraftNote` | `isDraftNote(fm)` | `boolean` | checking whether a note is tagged as an unfinished draft |
|
|
344
|
+
| `isGmNote` | `isGmNote(fm)` | `boolean` | checking whether a note is restricted to GM-facing output |
|
|
345
|
+
| `subTypeCharsetMessage` | `subTypeCharsetMessage(value)` | `string` | building the message for a `subType` outside the address charset |
|
|
346
|
+
| `typeCharsetMessage` | `typeCharsetMessage(type)` | `string` | building the message for a `type` outside the address charset |
|
|
347
|
+
| `assertVocabularyCharset` | `assertVocabularyCharset(vocabulary, where)` | throws | refusing a vocabulary declaration whose type or subType breaks the address charset |
|
|
348
|
+
| `dataFields` | `dataFields(type, vocabulary)` | `readonly DataFieldSpec[] \| undefined` | looking up the `data:` keys a note type may carry |
|
|
349
|
+
| `subTypes` | `subTypes(type, vocabulary)` | `readonly string[] \| null \| undefined` | looking up the closed `subType` values a note type declares |
|
|
349
350
|
|
|
350
351
|
### `engine.infobox`
|
|
351
352
|
|
|
@@ -507,6 +508,19 @@ What trade a settlement supports (`data.market`): the six-step scale, stated onc
|
|
|
507
508
|
| `marketClass` | `marketClass(value)` | `object\|undefined` | the class a value names, for rendering what a number means |
|
|
508
509
|
| `checkMarket` | `checkMarket(note)` | `object[]` | linting a place's `market` — a value off the scale, located at the value that states it |
|
|
509
510
|
|
|
511
|
+
### `engine.settlementTables`
|
|
512
|
+
|
|
513
|
+
A region's settlements table — a markdown table under `## Settlements` on a `place` note, one row per settlement with its market, its people and its holder — held to its declared columns, to the holding model the region's tags name, and to the notes its rows link. `package-build lint` runs it as part of the `place` check; a note with no such table is not read.
|
|
514
|
+
|
|
515
|
+
| Export | Signature | Returns | Use it when |
|
|
516
|
+
| ------------------------ | ---------------------------------------- | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
|
|
517
|
+
| `HOLDING_MODELS` | `const HOLDING_MODELS` | `readonly object[]` | the holding models in order, each its `tag`, the `holder` its Held by cell names, and whether a `person` may hold |
|
|
518
|
+
| `SETTLEMENT_COLUMNS` | `const SETTLEMENT_COLUMNS` | `readonly object[]` | the table's columns in header order, each its `name` and whether it is `required` |
|
|
519
|
+
| `SETTLEMENTS_HEADING` | `const SETTLEMENTS_HEADING` | `string` | the level-two heading text a settlements table stands under |
|
|
520
|
+
| `SETTLEMENT_TABLE_RULES` | `const SETTLEMENT_TABLE_RULES` | `readonly object[]` | the rules in order, each its `name` — the prefix its findings carry — and what it `describe`s |
|
|
521
|
+
| `findSettlementTables` | `findSettlementTables(raw)` | `{ heading: number, tables: object[] }` | reading a note's settlements tables, each with its header, its rows' cells and their 1-based lines and columns |
|
|
522
|
+
| `checkSettlementTables` | `checkSettlementTables(note, { index })` | `object[]` | linting a place note's settlements tables against the corpus the index holds |
|
|
523
|
+
|
|
510
524
|
### `engine.mapPlaces`
|
|
511
525
|
|
|
512
526
|
The places a map is drawn from — this package's index records and every fetched dependency's entries, read through one shape — and what their `parents` say: the world, the continents, each place's continent, and everything the containment tree can get wrong, reported as findings located at the entry that states it.
|
|
@@ -35,6 +35,8 @@ The [note-type reference](../reference/note-types.md) lists every field accepted
|
|
|
35
35
|
|
|
36
36
|
A being's `data.archetypes` names the roles it can fill in an adventure. Choose every specific role that fits, such as `entertainer` for a performer or `guildsperson` for someone whose professional training or connections matter. Use `[commoner]` when no specific archetype fits. `commoner` cannot share the list with another value. The [archetype reference](../reference/format-details.md#actors) defines the available roles.
|
|
37
37
|
|
|
38
|
+
A being's look is stated in a closed vocabulary. `data.gender` and `data.frame` take one word each, as do `data.appearance.eye_color`, `hair_color` and `skin_color`; `data.appearance.complexion` takes one word or a list of them. A value outside a field's list — a variant spelling such as `gray`, a word from another field's list, or an empty string — is an error naming the words the field takes. Leave a field out when the fact is unrecorded. `data.appearance.extra_features` is a free-text list for the marks no word covers. The [look reference](../reference/format-details.md#a-beings-look) gives every word and what it means.
|
|
39
|
+
|
|
38
40
|
A `sohl:`, `hm3:` or `dnd5e:` block holds values for that Foundry system. Its `system:` child contains values sent to the system's DataModel. The system block can override shared routing and art where the declared field permits it. `data.pack` and `data.packFolder` are shared routes; a system-specific route belongs in that system block. The [detailed mapping reference](../reference/format-details.md#mappings-every-type-shares) and [system-block explanation](../reference/format-details.md#frontmatter-has-three-regions) cover precedence and emitted fields.
|
|
39
41
|
|
|
40
42
|
One body describes the subject for both systems. A being's `{#appearance}` and `{#dossier}` sections feed SoHL Actor `system.appearance` and `system.dossier`, and HM3 Actor `system.description` and `system.biography`. An Item's `{#description}` page becomes a shared JournalEntryPage. Its resolved `@UUID[...]` pointer is stored in SoHL Item `system.docHtml` and HM3 Item `system.description`. An Item with no `{#description}` anchor uses its first page. Other H1 pages remain linkable.
|