@heroiclands/package-build 23.3.0 → 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 CHANGED
@@ -1,5 +1,17 @@
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
+
3
15
  ## 23.3.0
4
16
 
5
17
  ### Minor Changes
@@ -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 { emitDiagnostic, positionOfLiteral } from "../engine/diagnostics.mjs";
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
+ }
package/docs/commands.md CHANGED
@@ -2766,3 +2766,186 @@ $ 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
+ A profile or name lists with problems are refused with a finding at each.
2832
+
2833
+ A spec that states `name.full` keeps it, and the name must fit the language's
2834
+ lists: one of the patterns for `data.gender`, with every placeholder filled by
2835
+ a name of its list, the given name from the list for `data.gender`. A name that does not fit is an
2836
+ error naming the part that fails and the language it was checked against; from
2837
+ a `--specfile`, the error is located at the name in the file. Where the
2838
+ gender has no given list of its own, the given name is drawn from, and
2839
+ checked against, every given list together.
2840
+
2841
+ Two top-level spec keys are section bodies rather than frontmatter.
2842
+ `appearance` is the Appearance section's markdown, written verbatim with its
2843
+ newlines and spacing; without it, the section is a paragraph written from the
2844
+ data. `dossier` is the Dossier section's markdown, likewise verbatim; without
2845
+ it, the section is its heading alone.
2846
+
2847
+ **OPTIONS**
2848
+
2849
+ | Positional | Type | Default | Description |
2850
+ | ---------- | -------------------- | ------- | ---------------------- |
2851
+ | `action` | string, one of `new` | — | The action to perform. |
2852
+
2853
+ | Option | Type | Default | Description |
2854
+ | ------------ | ------ | --------------- | -------------------------------------------------------------------------------- |
2855
+ | `--spec` | string | — | The spec as a JSON string. Exactly one of `--spec` and `--specfile` is required. |
2856
+ | `--specfile` | string | — | The spec as a `.json`, `.yaml` or `.yml` file. |
2857
+ | `--seed` | string | — | Text that seeds generation beside the spec, to draw a different person. |
2858
+ | `--out` | string | standard output | Write the note to this new file. An existing file is never overwritten. |
2859
+
2860
+ **EXIT STATUS**
2861
+
2862
+ 1 if neither or both of `--spec` and `--specfile` are given, if the spec
2863
+ cannot be read or lacks `data.culture` or `data.gender`, if `data.culture`
2864
+ names no culture note, if the culture states no appearance profile or no
2865
+ language, if its language states no name lists, if the profile or the lists
2866
+ have problems, if the spec's `name.full` does not fit the lists, if the spec's
2867
+ `shortcode` already names a being, or if `--out` names a file that exists.
2868
+ Otherwise 0.
2869
+
2870
+ **EXAMPLES**
2871
+
2872
+ A spec, `spec.yaml`, stating the culture, the gender, two archetypes, an
2873
+ occupation and the dossier, and no name:
2874
+
2875
+ ```yaml
2876
+ data:
2877
+ culture: khelathi
2878
+ gender: male
2879
+ archetypes: [warrior, mariner]
2880
+ occupation: River Fleet Officer
2881
+ dossier: |
2882
+ He holds a commission in the river fleet, and his service is a matter of
2883
+ record rather than of reputation.
2884
+ ```
2885
+
2886
+ Run against a package whose world states `present: "720.300"`, whose
2887
+ `khelathi` culture carries the profile in
2888
+ [appearance profiles](reference/format-details.md#appearance-profiles) and
2889
+ names `language: skill-khelathilng`, and whose language carries the lists in
2890
+ [name lists](reference/format-details.md#name-lists):
2891
+
2892
+ ```
2893
+ $ package-build npc new --specfile spec.yaml
2894
+ ---
2895
+ shortcode: khelemureziq
2896
+ name: {full: Khelemûr ez Iqu}
2897
+ type: being
2898
+ subType: npc
2899
+ tags: [generated, draft]
2900
+ data:
2901
+ templatePriority: null
2902
+ archetypes: [warrior, mariner]
2903
+ occupation: River Fleet Officer
2904
+ lore: []
2905
+ culture: khelathi
2906
+ homes: []
2907
+ affiliations: {}
2908
+ gender: male
2909
+ born: "699.134"
2910
+ died: null
2911
+ height: 5' 11"
2912
+ weight: 167 lbs
2913
+ frame: medium
2914
+ appearance:
2915
+ eye_color: brown
2916
+ hair_color: black
2917
+ skin_color: brown
2918
+ complexion: clear
2919
+ extra_features: []
2920
+ ---
2921
+
2922
+ # Appearance {#appearance}
2923
+
2924
+ 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.
2925
+
2926
+ # Dossier {#dossier}
2927
+
2928
+ He holds a commission in the river fleet, and his service is a matter of
2929
+ record rather than of reputation.
2930
+ ```
2931
+
2932
+ A spec file, `bad.yaml`, stating a name whose second part is in no list:
2933
+
2934
+ ```
2935
+ $ package-build npc new --specfile bad.yaml
2936
+ 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
2937
+ ```
2938
+
2939
+ A spec inline, for a culture that names no language:
2940
+
2941
+ ```
2942
+ $ package-build npc new --spec '{"data":{"culture":"plainfolk","gender":"female"}}'
2943
+ 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
2944
+ ```
2945
+
2946
+ **SEE ALSO**
2947
+
2948
+ `package-build lint [root]`,
2949
+ [Appearance profiles](reference/format-details.md#appearance-profiles),
2950
+ [Name lists](reference/format-details.md#name-lists),
2951
+ [A being's look](reference/format-details.md#a-beings-look).
@@ -94,6 +94,8 @@ 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
+
97
99
  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
100
 
99
101
  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).