@heroiclands/package-build 23.3.0 → 23.3.2
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 +18 -0
- package/bin/content-commands.mjs +308 -2
- package/docs/commands.md +191 -0
- package/docs/content-format.md +9 -0
- package/docs/reference/format-details.md +321 -44
- package/docs/reference/note-types.md +98 -80
- package/engine/appearance-profile.mjs +330 -0
- package/engine/content-format.yaml +3 -0
- package/engine/content-index.mjs +22 -0
- package/engine/content-links.mjs +9 -0
- package/engine/culture-parents.mjs +301 -0
- package/engine/data-keys.mjs +20 -4
- package/engine/frontmatter-lint.mjs +15 -1
- package/engine/language-names.mjs +634 -0
- package/engine/literature-notes.mjs +62 -27
- package/engine/metadata-index.mjs +11 -0
- package/engine/note-vocabulary.mjs +171 -2
- package/engine/npc-generate.mjs +633 -0
- package/package.json +1 -1
- package/types/engine/appearance-profile.d.mts +94 -0
- package/types/engine/culture-parents.d.mts +29 -0
- package/types/engine/data-keys.d.mts +2 -2
- package/types/engine/language-names.d.mts +140 -0
- package/types/engine/literature-notes.d.mts +14 -3
- package/types/engine/npc-generate.d.mts +124 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# @heroiclands/package-build
|
|
2
2
|
|
|
3
|
+
## 23.3.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Cultures can inherit their language and appearance from parent cultures when generating characters.
|
|
8
|
+
|
|
9
|
+
## 23.3.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
**New npcs**
|
|
14
|
+
|
|
15
|
+
- `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.
|
|
16
|
+
- 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.
|
|
17
|
+
- 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`.
|
|
18
|
+
- 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.
|
|
19
|
+
- `package-build lint` checks a profile's words, weights and ranges, and a language's pattern and lists.
|
|
20
|
+
|
|
3
21
|
## 23.3.0
|
|
4
22
|
|
|
5
23
|
### Minor 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,15 @@ 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";
|
|
144
|
+
import { cultureFieldSource, cultureHierarchyProblems } from "../engine/culture-parents.mjs";
|
|
130
145
|
|
|
131
146
|
/**
|
|
132
147
|
* The packs `unpack` extracts.
|
|
@@ -241,7 +256,8 @@ export function registerContentCommands(cli) {
|
|
|
241
256
|
)
|
|
242
257
|
.command(withIndexPreflight(mapCommand()))
|
|
243
258
|
.command(withIndexPreflight(reachabilityCommand()))
|
|
244
|
-
.command(addressesCommand())
|
|
259
|
+
.command(addressesCommand())
|
|
260
|
+
.command(withIndexPreflight(npcCommand()));
|
|
245
261
|
}
|
|
246
262
|
|
|
247
263
|
/** Optional sentence findings and note-level readability reports. */
|
|
@@ -490,6 +506,12 @@ function docsCommand() {
|
|
|
490
506
|
type: "string",
|
|
491
507
|
default: "docs",
|
|
492
508
|
});
|
|
509
|
+
yargs.option("seed", {
|
|
510
|
+
describe:
|
|
511
|
+
"Text that seeds generation beside the spec, to draw a different person " +
|
|
512
|
+
"from the same spec.",
|
|
513
|
+
type: "string",
|
|
514
|
+
});
|
|
493
515
|
yargs.option("out", {
|
|
494
516
|
describe: "Write to this file instead of the configured location.",
|
|
495
517
|
type: "string",
|
|
@@ -1555,6 +1577,12 @@ function contentIndexCommand() {
|
|
|
1555
1577
|
describe: "Content tree to read. Defaults to the configured contentBase.",
|
|
1556
1578
|
type: "string",
|
|
1557
1579
|
});
|
|
1580
|
+
yargs.option("seed", {
|
|
1581
|
+
describe:
|
|
1582
|
+
"Text that seeds generation beside the spec, to draw a different person " +
|
|
1583
|
+
"from the same spec.",
|
|
1584
|
+
type: "string",
|
|
1585
|
+
});
|
|
1558
1586
|
yargs.option("out", {
|
|
1559
1587
|
describe:
|
|
1560
1588
|
"Directory to write into. Defaults to the configured " +
|
|
@@ -2515,3 +2543,281 @@ function packageCommand() {
|
|
|
2515
2543
|
},
|
|
2516
2544
|
};
|
|
2517
2545
|
}
|
|
2546
|
+
|
|
2547
|
+
/**
|
|
2548
|
+
* Read an `npc new` spec from `--spec` or `--specfile`.
|
|
2549
|
+
*
|
|
2550
|
+
* @param {{spec?: string, specfile?: string}} argv
|
|
2551
|
+
* @returns {{spec: unknown, file?: string, text?: string}} The spec as parsed,
|
|
2552
|
+
* and the file and text it was read from where it came from a file.
|
|
2553
|
+
*/
|
|
2554
|
+
function readNpcSpec(argv) {
|
|
2555
|
+
if (argv.spec !== undefined) {
|
|
2556
|
+
try {
|
|
2557
|
+
return { spec: JSON.parse(argv.spec) };
|
|
2558
|
+
} catch (err) {
|
|
2559
|
+
throw new Error(`\`--spec\` is not JSON: ${err.message}`);
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
const file = argv.specfile;
|
|
2563
|
+
const ext = path.extname(file).toLowerCase();
|
|
2564
|
+
if (![".json", ".yaml", ".yml"].includes(ext))
|
|
2565
|
+
throw new Error(`\`--specfile\` ${file} must be a .json, .yaml or .yml file`);
|
|
2566
|
+
let text;
|
|
2567
|
+
try {
|
|
2568
|
+
text = fs.readFileSync(file, "utf8");
|
|
2569
|
+
} catch (err) {
|
|
2570
|
+
throw new Error(`\`--specfile\` ${file} cannot be read: ${err.message}`);
|
|
2571
|
+
}
|
|
2572
|
+
try {
|
|
2573
|
+
return { spec: ext === ".json" ? JSON.parse(text) : YAML.parse(text), file, text };
|
|
2574
|
+
} catch (err) {
|
|
2575
|
+
throw Object.assign(new Error(`${file}: error: ${err.message.split("\n")[0]}`), {
|
|
2576
|
+
located: true,
|
|
2577
|
+
});
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2581
|
+
/**
|
|
2582
|
+
* An error located at a note, or at one key of its frontmatter where the note
|
|
2583
|
+
* states it.
|
|
2584
|
+
*
|
|
2585
|
+
* @param {{file?: string, raw?: string}} note - Where the note is.
|
|
2586
|
+
* @param {Array<string|number>} keyPath - The frontmatter key at fault, or the
|
|
2587
|
+
* map a missing key belongs in.
|
|
2588
|
+
* @param {string} message - What is wrong.
|
|
2589
|
+
* @returns {Error} The error, for `reportFailure` to print as a diagnostic.
|
|
2590
|
+
*/
|
|
2591
|
+
function noteError(note, keyPath, message) {
|
|
2592
|
+
if (!note.file) return new Error(message);
|
|
2593
|
+
return Object.assign(new Error(message), {
|
|
2594
|
+
file: note.file,
|
|
2595
|
+
keyPath,
|
|
2596
|
+
position: positionOfFrontmatterPath(note.raw ?? "", keyPath, { key: true }),
|
|
2597
|
+
});
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2600
|
+
/**
|
|
2601
|
+
* `npc new`: resolve the spec's culture through the content tree, then the
|
|
2602
|
+
* culture's appearance profile, its language and the language's name lists,
|
|
2603
|
+
* and write the note. A missing link at any step is an error naming the note
|
|
2604
|
+
* to fix.
|
|
2605
|
+
*
|
|
2606
|
+
* @param {object} config - The resolved pack configuration.
|
|
2607
|
+
* @param {{spec?: string, specfile?: string, seed?: string, out?: string}} argv
|
|
2608
|
+
*/
|
|
2609
|
+
function newNpc(config, argv) {
|
|
2610
|
+
const { spec, file: specFile, text: specText } = readNpcSpec(argv);
|
|
2611
|
+
const problems = npcSpecProblems(spec);
|
|
2612
|
+
if (problems.length) throw new Error(`npc new: ${problems.join("; ")}`);
|
|
2613
|
+
if (argv.out && fs.existsSync(argv.out))
|
|
2614
|
+
throw new Error(`${argv.out} already exists, and \`npc new\` never overwrites a note`);
|
|
2615
|
+
|
|
2616
|
+
const root = config.paths.content;
|
|
2617
|
+
const corpusProblems = [];
|
|
2618
|
+
const records = indexRecordsFor({
|
|
2619
|
+
contentBase: root,
|
|
2620
|
+
config,
|
|
2621
|
+
skipDirectories: config.skipDirectories,
|
|
2622
|
+
problems: corpusProblems,
|
|
2623
|
+
});
|
|
2624
|
+
const index = buildLinkIndex(root, {
|
|
2625
|
+
config,
|
|
2626
|
+
records,
|
|
2627
|
+
skipDirectories: config.skipDirectories,
|
|
2628
|
+
});
|
|
2629
|
+
const located = (hit) => ({
|
|
2630
|
+
file: hit.file ? path.relative(process.cwd(), hit.file) : undefined,
|
|
2631
|
+
raw: hit.raw,
|
|
2632
|
+
});
|
|
2633
|
+
|
|
2634
|
+
const written = spec.data.culture;
|
|
2635
|
+
const key = cultureKey(written, index);
|
|
2636
|
+
const hit = key ? index.addressHit(key) : undefined;
|
|
2637
|
+
if (!hit)
|
|
2638
|
+
throw new Error(
|
|
2639
|
+
`\`data.culture\` ${JSON.stringify(written)} names no note in this package or ` +
|
|
2640
|
+
"its dependencies",
|
|
2641
|
+
);
|
|
2642
|
+
const culture = hit.fm ?? hit;
|
|
2643
|
+
if (String(culture.type ?? hit.type) !== "lore" || culture.subType !== "culture")
|
|
2644
|
+
throw new Error(
|
|
2645
|
+
`\`data.culture\` ${JSON.stringify(written)} is not a culture: it names a ` +
|
|
2646
|
+
`${culture.type ?? hit.type ?? "note"} of subType ${culture.subType ?? "none"}`,
|
|
2647
|
+
);
|
|
2648
|
+
const hierarchyProblems = cultureHierarchyProblems(records, key);
|
|
2649
|
+
for (const problem of hierarchyProblems) emitDiagnostic(problem);
|
|
2650
|
+
if (hierarchyProblems.some((problem) => problem.severity === "error")) {
|
|
2651
|
+
process.exitCode = 1;
|
|
2652
|
+
return;
|
|
2653
|
+
}
|
|
2654
|
+
const cultureNote = located(hit);
|
|
2655
|
+
const profile = culture.data?.appearanceProfile;
|
|
2656
|
+
if (profile === undefined || profile === null)
|
|
2657
|
+
throw noteError(
|
|
2658
|
+
cultureNote,
|
|
2659
|
+
["data"],
|
|
2660
|
+
`${key} states no \`data.appearanceProfile\`, so \`npc new\` cannot draw its ` +
|
|
2661
|
+
"people's look",
|
|
2662
|
+
);
|
|
2663
|
+
const profileSource = cultureFieldSource(records, key, "appearanceProfile") ?? hit;
|
|
2664
|
+
const profileErrors = profileFindings(
|
|
2665
|
+
{
|
|
2666
|
+
file: located(profileSource).file,
|
|
2667
|
+
raw: profileSource.raw,
|
|
2668
|
+
fm: profileSource.fm ?? profileSource,
|
|
2669
|
+
},
|
|
2670
|
+
BEING_LOOK_VOCABULARY,
|
|
2671
|
+
);
|
|
2672
|
+
if (profileErrors.length) {
|
|
2673
|
+
for (const finding of profileErrors) emitDiagnostic(finding);
|
|
2674
|
+
throw new Error(`${key}'s appearance profile has ${profileErrors.length} problem(s)`);
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
const tongue = culture.data?.language;
|
|
2678
|
+
if (tongue === undefined || tongue === null)
|
|
2679
|
+
throw noteError(
|
|
2680
|
+
cultureNote,
|
|
2681
|
+
["data"],
|
|
2682
|
+
`${key} states no \`data.language\`, so \`npc new\` has no name lists to draw ` +
|
|
2683
|
+
"from; name the language skill note its people speak",
|
|
2684
|
+
);
|
|
2685
|
+
const languageSource = cultureFieldSource(records, key, "language") ?? hit;
|
|
2686
|
+
const languageSourceNote = located(languageSource);
|
|
2687
|
+
const tuple = parseAddress(tongue, {
|
|
2688
|
+
package: index.contentPackage,
|
|
2689
|
+
system: "note",
|
|
2690
|
+
type: "skill",
|
|
2691
|
+
types: index.types,
|
|
2692
|
+
packages: index.packages,
|
|
2693
|
+
});
|
|
2694
|
+
const languageKey = tuple.reason ? undefined : renderAddress(tuple);
|
|
2695
|
+
const languageHit = languageKey ? index.addressHit(languageKey) : undefined;
|
|
2696
|
+
if (!languageHit)
|
|
2697
|
+
throw noteError(
|
|
2698
|
+
languageSourceNote,
|
|
2699
|
+
["data", "language"],
|
|
2700
|
+
`${key}'s \`data.language\` names no note in this package or its dependencies`,
|
|
2701
|
+
);
|
|
2702
|
+
const language = languageHit.fm ?? languageHit;
|
|
2703
|
+
if (String(language.type ?? languageHit.type) !== "skill" || language.subType !== "language")
|
|
2704
|
+
throw noteError(
|
|
2705
|
+
languageSourceNote,
|
|
2706
|
+
["data", "language"],
|
|
2707
|
+
`${key}'s \`data.language\` ${languageKey} is not a language: it names a ` +
|
|
2708
|
+
`${language.type ?? languageHit.type ?? "note"} of subType ` +
|
|
2709
|
+
`${language.subType ?? "none"}`,
|
|
2710
|
+
);
|
|
2711
|
+
const languageNote = located(languageHit);
|
|
2712
|
+
const names = language.data?.names;
|
|
2713
|
+
if (names === undefined || names === null)
|
|
2714
|
+
throw noteError(
|
|
2715
|
+
languageNote,
|
|
2716
|
+
["data"],
|
|
2717
|
+
`${languageKey} states no \`data.names\`, so \`npc new\` has no name lists to ` +
|
|
2718
|
+
`draw ${key}'s names from`,
|
|
2719
|
+
);
|
|
2720
|
+
const namesErrors = namesFindings(
|
|
2721
|
+
{ file: languageNote.file, raw: languageHit.raw, fm: language },
|
|
2722
|
+
BEING_LOOK_VOCABULARY,
|
|
2723
|
+
);
|
|
2724
|
+
if (namesErrors.length) {
|
|
2725
|
+
for (const finding of namesErrors) emitDiagnostic(finding);
|
|
2726
|
+
throw new Error(`${languageKey}'s name lists have ${namesErrors.length} problem(s)`);
|
|
2727
|
+
}
|
|
2728
|
+
|
|
2729
|
+
const { taken, ...defaults } = npcDefaults(index, key);
|
|
2730
|
+
if (typeof spec.shortcode === "string" && taken.has(spec.shortcode))
|
|
2731
|
+
throw new Error(`\`shortcode: ${spec.shortcode}\` already names a being in this package`);
|
|
2732
|
+
let text;
|
|
2733
|
+
try {
|
|
2734
|
+
({ text } = generateNpc(spec, {
|
|
2735
|
+
profile,
|
|
2736
|
+
names,
|
|
2737
|
+
language: languageKey,
|
|
2738
|
+
seed: argv.seed,
|
|
2739
|
+
present: worldInvariants(index).present,
|
|
2740
|
+
dateContext: reckoningContext(index),
|
|
2741
|
+
defaults,
|
|
2742
|
+
taken,
|
|
2743
|
+
}));
|
|
2744
|
+
} catch (err) {
|
|
2745
|
+
// A name that does not fit is the spec's fault, so a spec file
|
|
2746
|
+
// locates it at the name it states.
|
|
2747
|
+
if (specFile && Array.isArray(err?.keyPath))
|
|
2748
|
+
Object.assign(err, {
|
|
2749
|
+
file: specFile,
|
|
2750
|
+
position: positionOfYamlPath(specText ?? "", err.keyPath),
|
|
2751
|
+
});
|
|
2752
|
+
throw err;
|
|
2753
|
+
}
|
|
2754
|
+
if (!argv.out) {
|
|
2755
|
+
process.stdout.write(text);
|
|
2756
|
+
return;
|
|
2757
|
+
}
|
|
2758
|
+
fs.mkdirSync(path.dirname(argv.out), { recursive: true });
|
|
2759
|
+
try {
|
|
2760
|
+
fs.writeFileSync(argv.out, text, { flag: "wx" });
|
|
2761
|
+
} catch (err) {
|
|
2762
|
+
if (err?.code === "EEXIST")
|
|
2763
|
+
throw new Error(`${argv.out} already exists, and \`npc new\` never overwrites a note`);
|
|
2764
|
+
throw err;
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
|
|
2768
|
+
/**
|
|
2769
|
+
* `package-build npc new` — a complete NPC being note from a spec, its
|
|
2770
|
+
* appearance drawn from its culture's profile.
|
|
2771
|
+
*
|
|
2772
|
+
* @returns {object} The yargs command module.
|
|
2773
|
+
*/
|
|
2774
|
+
function npcCommand() {
|
|
2775
|
+
return {
|
|
2776
|
+
command: "npc <action>",
|
|
2777
|
+
describe:
|
|
2778
|
+
"Write a new NPC being note from a spec, its culture's appearance profile and " +
|
|
2779
|
+
"its language's name lists",
|
|
2780
|
+
builder: (yargs) => {
|
|
2781
|
+
yargs.positional("action", {
|
|
2782
|
+
describe: "The action to perform.",
|
|
2783
|
+
type: "string",
|
|
2784
|
+
choices: ["new"],
|
|
2785
|
+
});
|
|
2786
|
+
yargs.option("spec", {
|
|
2787
|
+
describe:
|
|
2788
|
+
"The spec as a JSON string: frontmatter fields, with `data.culture` and " +
|
|
2789
|
+
"`data.gender` required.",
|
|
2790
|
+
type: "string",
|
|
2791
|
+
});
|
|
2792
|
+
yargs.option("specfile", {
|
|
2793
|
+
describe: "The spec as a .json, .yaml or .yml file.",
|
|
2794
|
+
type: "string",
|
|
2795
|
+
});
|
|
2796
|
+
yargs.option("seed", {
|
|
2797
|
+
describe:
|
|
2798
|
+
"Text that seeds generation beside the spec, to draw a different person " +
|
|
2799
|
+
"from the same spec.",
|
|
2800
|
+
type: "string",
|
|
2801
|
+
});
|
|
2802
|
+
yargs.option("out", {
|
|
2803
|
+
describe:
|
|
2804
|
+
"Write the note to this new file instead of standard output. An " +
|
|
2805
|
+
"existing file is never overwritten.",
|
|
2806
|
+
type: "string",
|
|
2807
|
+
});
|
|
2808
|
+
yargs.check((parsed) => {
|
|
2809
|
+
if ((parsed.spec === undefined) === (parsed.specfile === undefined))
|
|
2810
|
+
throw new Error("`npc new` takes exactly one of `--spec` and `--specfile`");
|
|
2811
|
+
return true;
|
|
2812
|
+
});
|
|
2813
|
+
},
|
|
2814
|
+
handler: async (argv) => {
|
|
2815
|
+
try {
|
|
2816
|
+
newNpc(loadPackConfig(), argv);
|
|
2817
|
+
} catch (err) {
|
|
2818
|
+
reportFailure(err);
|
|
2819
|
+
process.exitCode = 1;
|
|
2820
|
+
}
|
|
2821
|
+
},
|
|
2822
|
+
};
|
|
2823
|
+
}
|
package/docs/commands.md
CHANGED
|
@@ -2766,3 +2766,194 @@ $ package-build addresses diff --from build/dist/module.zip
|
|
|
2766
2766
|
|
|
2767
2767
|
`package-build package <action> [pack] [entry]`, `package-build deps fetch`,
|
|
2768
2768
|
[Diagnostics](diagnostics.md).
|
|
2769
|
+
|
|
2770
|
+
### `package-build npc new`
|
|
2771
|
+
|
|
2772
|
+
**NAME**
|
|
2773
|
+
|
|
2774
|
+
Write a new NPC being note from a spec, its look drawn from its culture's
|
|
2775
|
+
appearance profile and its name from its culture's language.
|
|
2776
|
+
|
|
2777
|
+
**SYNOPSIS**
|
|
2778
|
+
|
|
2779
|
+
```
|
|
2780
|
+
package-build npc new (--spec <json> | --specfile <path>) [--seed <text>] [--out <path>]
|
|
2781
|
+
```
|
|
2782
|
+
|
|
2783
|
+
**DESCRIPTION**
|
|
2784
|
+
|
|
2785
|
+
Writes one `type: being`, `subType: npc` note: the full `data` block, an
|
|
2786
|
+
`# Appearance {#appearance}` section and a `# Dossier {#dossier}` section, with
|
|
2787
|
+
no system block. The note goes to standard output, or to a new file with
|
|
2788
|
+
`--out`.
|
|
2789
|
+
|
|
2790
|
+
The spec is a possibly incomplete set of frontmatter fields, given as a JSON
|
|
2791
|
+
string with `--spec` or as a `.json`, `.yaml` or `.yml` file with
|
|
2792
|
+
`--specfile`; exactly one of the two is required. `data.culture` and
|
|
2793
|
+
`data.gender` are required in it. Every field the spec gives is written
|
|
2794
|
+
exactly as given, at every depth, and every field it leaves out is generated:
|
|
2795
|
+
|
|
2796
|
+
| Field | Generated value |
|
|
2797
|
+
| ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
|
2798
|
+
| `name.full` | Drawn from the culture's language's name lists by one of its patterns for `data.gender`, the given name from the list for that gender. |
|
|
2799
|
+
| `shortcode` | The name transliterated to lowercase letters and digits and cut to twelve characters; a counter replaces its tail where a being holds it. |
|
|
2800
|
+
| `type`, `subType`, `tags` | `being`, `npc`, `[generated, draft]`. |
|
|
2801
|
+
| `data.icon`, `data.species`, `data.packFolder` | The commonest value among the package's npcs of the same culture, else among all its npcs; left out where none states one. |
|
|
2802
|
+
| `data.archetypes` | `[commoner]`, the archetype of a person who fits no more specific one. |
|
|
2803
|
+
| `data.templatePriority`, `data.occupation`, `data.died` | `null`. |
|
|
2804
|
+
| `data.lore`, `data.homes` | `[]`. |
|
|
2805
|
+
| `data.affiliations` | `{}`. |
|
|
2806
|
+
| `data.born` | An adult age of 18 to 60 years before the package's present, as a `<year>.<day>` date; `unknown` where the package states no present. |
|
|
2807
|
+
| `data.frame`, `data.appearance.*` | Weighted draws from the culture's appearance profile; `extra_features` is `[]`. |
|
|
2808
|
+
| `data.height` | From the profile's range for the gender, in feet and inches. |
|
|
2809
|
+
| `data.weight` | From the height and the frame's body-mass band, in pounds. A spec giving height and weight but no frame gets the frame they imply. |
|
|
2810
|
+
|
|
2811
|
+
Generation is seeded from the spec itself, through a canonical serialisation
|
|
2812
|
+
that sorts every map's keys: the same spec always writes the same note, whether
|
|
2813
|
+
or not it states a name, and the order the spec writes its fields in changes
|
|
2814
|
+
nothing. `--seed` adds text to that seed, so the same spec with a different
|
|
2815
|
+
`--seed` writes a different person; two people drawn from a spec that states
|
|
2816
|
+
only the culture and the gender differ only by their seeds.
|
|
2817
|
+
|
|
2818
|
+
`data.culture` resolves as any culture reference does, through this package's
|
|
2819
|
+
content tree and its dependencies, and must name a `lore` note of
|
|
2820
|
+
`subType: culture`. The culture supplies two things, and the command refuses a
|
|
2821
|
+
culture missing either, with an error at the culture note:
|
|
2822
|
+
|
|
2823
|
+
- **Its look**, from `data.appearanceProfile`: the words and ranges every
|
|
2824
|
+
appearance field is drawn from. See
|
|
2825
|
+
[appearance profiles](reference/format-details.md#appearance-profiles).
|
|
2826
|
+
- **Its names**, from the language skill note its `data.language` names, whose
|
|
2827
|
+
`data.names` holds the pattern and the lists. A language with no
|
|
2828
|
+
`data.names` is an error at the language note. See
|
|
2829
|
+
[name lists](reference/format-details.md#name-lists).
|
|
2830
|
+
|
|
2831
|
+
The content-index JSONL resolves a culture's `data.language` and
|
|
2832
|
+
`data.appearanceProfile` before this command reads them. A child culture may
|
|
2833
|
+
inherit each omitted field from its `data.parents`, while a stated value keeps
|
|
2834
|
+
precedence. The command consumes the resolved values and keeps the selected
|
|
2835
|
+
child culture on the generated note. A parent cycle or conflicting inherited
|
|
2836
|
+
sources are errors; complete fields on the selected culture do not excuse a
|
|
2837
|
+
cycle. See [culture parents](reference/format-details.md#culture-parents-and-inherited-fields).
|
|
2838
|
+
|
|
2839
|
+
A profile or name lists with problems are refused with a finding at each.
|
|
2840
|
+
|
|
2841
|
+
A spec that states `name.full` keeps it, and the name must fit the language's
|
|
2842
|
+
lists: one of the patterns for `data.gender`, with every placeholder filled by
|
|
2843
|
+
a name of its list, the given name from the list for `data.gender`. A name that does not fit is an
|
|
2844
|
+
error naming the part that fails and the language it was checked against; from
|
|
2845
|
+
a `--specfile`, the error is located at the name in the file. Where the
|
|
2846
|
+
gender has no given list of its own, the given name is drawn from, and
|
|
2847
|
+
checked against, every given list together.
|
|
2848
|
+
|
|
2849
|
+
Two top-level spec keys are section bodies rather than frontmatter.
|
|
2850
|
+
`appearance` is the Appearance section's markdown, written verbatim with its
|
|
2851
|
+
newlines and spacing; without it, the section is a paragraph written from the
|
|
2852
|
+
data. `dossier` is the Dossier section's markdown, likewise verbatim; without
|
|
2853
|
+
it, the section is its heading alone.
|
|
2854
|
+
|
|
2855
|
+
**OPTIONS**
|
|
2856
|
+
|
|
2857
|
+
| Positional | Type | Default | Description |
|
|
2858
|
+
| ---------- | -------------------- | ------- | ---------------------- |
|
|
2859
|
+
| `action` | string, one of `new` | — | The action to perform. |
|
|
2860
|
+
|
|
2861
|
+
| Option | Type | Default | Description |
|
|
2862
|
+
| ------------ | ------ | --------------- | -------------------------------------------------------------------------------- |
|
|
2863
|
+
| `--spec` | string | — | The spec as a JSON string. Exactly one of `--spec` and `--specfile` is required. |
|
|
2864
|
+
| `--specfile` | string | — | The spec as a `.json`, `.yaml` or `.yml` file. |
|
|
2865
|
+
| `--seed` | string | — | Text that seeds generation beside the spec, to draw a different person. |
|
|
2866
|
+
| `--out` | string | standard output | Write the note to this new file. An existing file is never overwritten. |
|
|
2867
|
+
|
|
2868
|
+
**EXIT STATUS**
|
|
2869
|
+
|
|
2870
|
+
1 if neither or both of `--spec` and `--specfile` are given, if the spec
|
|
2871
|
+
cannot be read or lacks `data.culture` or `data.gender`, if `data.culture`
|
|
2872
|
+
names no culture note, if the culture states no appearance profile or no
|
|
2873
|
+
language, if its language states no name lists, if the profile or the lists
|
|
2874
|
+
have problems, if the spec's `name.full` does not fit the lists, if the spec's
|
|
2875
|
+
`shortcode` already names a being, or if `--out` names a file that exists.
|
|
2876
|
+
Otherwise 0.
|
|
2877
|
+
|
|
2878
|
+
**EXAMPLES**
|
|
2879
|
+
|
|
2880
|
+
A spec, `spec.yaml`, stating the culture, the gender, two archetypes, an
|
|
2881
|
+
occupation and the dossier, and no name:
|
|
2882
|
+
|
|
2883
|
+
```yaml
|
|
2884
|
+
data:
|
|
2885
|
+
culture: khelathi
|
|
2886
|
+
gender: male
|
|
2887
|
+
archetypes: [warrior, mariner]
|
|
2888
|
+
occupation: River Fleet Officer
|
|
2889
|
+
dossier: |
|
|
2890
|
+
He holds a commission in the river fleet, and his service is a matter of
|
|
2891
|
+
record rather than of reputation.
|
|
2892
|
+
```
|
|
2893
|
+
|
|
2894
|
+
Run against a package whose world states `present: "720.300"`, whose
|
|
2895
|
+
`khelathi` culture carries the profile in
|
|
2896
|
+
[appearance profiles](reference/format-details.md#appearance-profiles) and
|
|
2897
|
+
names `language: skill-khelathilng`, and whose language carries the lists in
|
|
2898
|
+
[name lists](reference/format-details.md#name-lists):
|
|
2899
|
+
|
|
2900
|
+
```
|
|
2901
|
+
$ package-build npc new --specfile spec.yaml
|
|
2902
|
+
---
|
|
2903
|
+
shortcode: khelemureziq
|
|
2904
|
+
name: {full: Khelemûr ez Iqu}
|
|
2905
|
+
type: being
|
|
2906
|
+
subType: npc
|
|
2907
|
+
tags: [generated, draft]
|
|
2908
|
+
data:
|
|
2909
|
+
templatePriority: null
|
|
2910
|
+
archetypes: [warrior, mariner]
|
|
2911
|
+
occupation: River Fleet Officer
|
|
2912
|
+
lore: []
|
|
2913
|
+
culture: khelathi
|
|
2914
|
+
homes: []
|
|
2915
|
+
affiliations: {}
|
|
2916
|
+
gender: male
|
|
2917
|
+
born: "699.134"
|
|
2918
|
+
died: null
|
|
2919
|
+
height: 5' 11"
|
|
2920
|
+
weight: 167 lbs
|
|
2921
|
+
frame: medium
|
|
2922
|
+
appearance:
|
|
2923
|
+
eye_color: brown
|
|
2924
|
+
hair_color: black
|
|
2925
|
+
skin_color: brown
|
|
2926
|
+
complexion: clear
|
|
2927
|
+
extra_features: []
|
|
2928
|
+
---
|
|
2929
|
+
|
|
2930
|
+
# Appearance {#appearance}
|
|
2931
|
+
|
|
2932
|
+
Khelemûr ez Iqu is a 21-year-old man who stands 5'11" tall with a medium build. He has brown skin, clear complexion, black hair, and brown eyes.
|
|
2933
|
+
|
|
2934
|
+
# Dossier {#dossier}
|
|
2935
|
+
|
|
2936
|
+
He holds a commission in the river fleet, and his service is a matter of
|
|
2937
|
+
record rather than of reputation.
|
|
2938
|
+
```
|
|
2939
|
+
|
|
2940
|
+
A spec file, `bad.yaml`, stating a name whose second part is in no list:
|
|
2941
|
+
|
|
2942
|
+
```
|
|
2943
|
+
$ package-build npc new --specfile bad.yaml
|
|
2944
|
+
bad.yaml:2:9: error: name.full "Aguri ez Lut" does not fit the names of demo-note-skill-khelathilng: "Lut", the {place} part, is not in the place list
|
|
2945
|
+
```
|
|
2946
|
+
|
|
2947
|
+
A spec inline, for a culture that names no language:
|
|
2948
|
+
|
|
2949
|
+
```
|
|
2950
|
+
$ package-build npc new --spec '{"data":{"culture":"plainfolk","gender":"female"}}'
|
|
2951
|
+
assets/content/Cultures/Plainfolk.md:6:1: error: demo-note-lore-plainfolk states no `data.language`, so `npc new` has no name lists to draw from; name the language skill note its people speak
|
|
2952
|
+
```
|
|
2953
|
+
|
|
2954
|
+
**SEE ALSO**
|
|
2955
|
+
|
|
2956
|
+
`package-build lint [root]`,
|
|
2957
|
+
[Appearance profiles](reference/format-details.md#appearance-profiles),
|
|
2958
|
+
[Name lists](reference/format-details.md#name-lists),
|
|
2959
|
+
[A being's look](reference/format-details.md#a-beings-look).
|
package/docs/content-format.md
CHANGED
|
@@ -94,6 +94,15 @@ A region may list its settlements in a table under `## Settlements` — name, ma
|
|
|
94
94
|
|
|
95
95
|
A being states its gender, frame and appearance in words from one closed vocabulary: `data.gender`, `data.frame`, and `data.appearance.eye_color`, `hair_color`, `skin_color` and `complexion` each take only the words listed for them, and any other value is an error at the value. `data.appearance.extra_features` stays free text. See [a being's look](reference/format-details.md#a-beings-look).
|
|
96
96
|
|
|
97
|
+
A culture note may state what its people look like in `data.appearanceProfile`: weighted words for frame, eyes, hair, skin and complexion, and height ranges by gender, all from the same vocabulary. A culture names its tongue in `data.language`, and that language skill note may state `data.names`: the patterns its speakers' names are built by and the lists each part is drawn from. `package-build npc new` writes a complete npc note from a spec of a culture and a gender, drawing the name from those lists and everything else the spec leaves out from the profile. See [appearance profiles](reference/format-details.md#appearance-profiles), [name lists](reference/format-details.md#name-lists) and [the command](commands.md#package-build-npc-new).
|
|
98
|
+
|
|
99
|
+
A culture may name its larger cultures in `data.parents`. Each entry names a
|
|
100
|
+
culture lore note. Omitted `data.language` and `data.appearanceProfile` fields
|
|
101
|
+
inherit independently through those parents; the content-index JSONL contains
|
|
102
|
+
the resolved values, which `npc new` reads directly. A stated field wins over
|
|
103
|
+
its inherited value. See [culture parents](reference/format-details.md#culture-parents-and-inherited-fields)
|
|
104
|
+
for a complete example, parent ordering and findings.
|
|
105
|
+
|
|
97
106
|
Places, affiliations, lore and beings may name their culture in `data.culture`, and a `doc` of `subType: settingguide` must. A place or affiliation that names none inherits the culture of its parents. See [culture in the index](reference/format-details.md#culture-in-the-index).
|
|
98
107
|
|
|
99
108
|
Blockquote alerts use `[!NOTE]`, `[!TIP]`, `[!IMPORTANT]`, `[!WARNING]`, or `[!]`, with optional attributes after the marker. They replace the former info and warn div fences; secret blocks remain supported. See [alerts](authoring/links-and-markup.md#alerts).
|