@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.
@@ -13,13 +13,17 @@
13
13
 
14
14
  /**
15
15
  * A work a people tells, sings or writes: the `literature` lore subType and the
16
- * three optional facts only such a note may state about itself. Whose work it
17
- * is, `data.culture`, is a field every lore note but a culture note may state,
16
+ * optional facts only such a note may state about itself. Whose work it is,
17
+ * `data.culture`, is a field every lore note but a culture note may state,
18
18
  * declared in `culture-choice.mjs`.
19
19
  *
20
+ * `data.language` is shared with the `culture` subType: a work names the
21
+ * tongue it is composed in, and a culture names the tongue its people speak,
22
+ * whose name lists `package-build npc new` draws from.
23
+ *
20
24
  * `DataFieldSpec` declares the keys a type accepts, not the subType that may
21
- * write them, so {@link checkLiteratureNote} scopes these keys to `literature`
22
- * the way the calendar check scopes its own to `calendar`.
25
+ * write them, so {@link checkLiteratureNote} scopes these keys to the subTypes
26
+ * that mean them, the way the calendar check scopes its own to `calendar`.
23
27
  *
24
28
  * @module
25
29
  */
@@ -30,6 +34,9 @@ import { positionOfFrontmatterPath } from "./diagnostics.mjs";
30
34
  /** The `lore` subType whose notes are works of literature. */
31
35
  export const LITERATURE_SUBTYPE = "literature";
32
36
 
37
+ /** The `lore` subTypes that may name a language. */
38
+ export const LANGUAGE_SUBTYPES = Object.freeze([LITERATURE_SUBTYPE, "culture"]);
39
+
33
40
  /**
34
41
  * Require `data.language` to address a `skill` note whose subType is
35
42
  * `language`.
@@ -67,7 +74,27 @@ export function checkLanguageChoice(note, { index } = {}) {
67
74
  }
68
75
 
69
76
  /**
70
- * The fields only a `literature` note may state. Every one is optional.
77
+ * `data.language`: the tongue a work is composed in, or the tongue a culture's
78
+ * people speak. Optional on both.
79
+ *
80
+ * @type {object}
81
+ */
82
+ export const LANGUAGE_FIELD = Object.freeze({
83
+ name: "language",
84
+ shape: "an Address",
85
+ kind: "address",
86
+ ref: "skill",
87
+ accepts: Object.freeze(["skill"]),
88
+ check: checkLanguageChoice,
89
+ describe:
90
+ "The tongue the work is composed in, or on a culture note the tongue its people " +
91
+ "speak, as a language skill note. `package-build npc new` draws a culture's names " +
92
+ "from its language's `data.names`. A `literature` or `culture` note only.",
93
+ });
94
+
95
+ /**
96
+ * The fields only a `literature` note may state, beside the shared
97
+ * {@link LANGUAGE_FIELD}. Every one is optional.
71
98
  *
72
99
  * @type {ReadonlyArray<object>}
73
100
  */
@@ -90,39 +117,47 @@ export const LITERATURE_FIELDS = Object.freeze([
90
117
  "The beings, places, gods and other notes the work concerns, and each " +
91
118
  "event it concerns, named as `place-ironfells#sack`.",
92
119
  },
93
- {
94
- name: "language",
95
- shape: "an Address",
96
- kind: "address",
97
- ref: "skill",
98
- accepts: ["skill"],
99
- check: checkLanguageChoice,
100
- describe: "The tongue the work is composed in, as a language skill note.",
101
- },
102
120
  ]);
103
121
 
104
122
  const LITERATURE_KEYS = Object.freeze(LITERATURE_FIELDS.map((field) => field.name));
105
123
 
106
124
  /**
107
- * Refuse a literature field on a lore note of any other subType.
125
+ * Refuse a literature field on a lore note of any other subType, and
126
+ * `data.language` on a lore note that is neither a work nor a culture.
108
127
  *
109
128
  * @param {object} note
110
- * @returns {object[]} One error per literature key written off `literature`.
129
+ * @returns {object[]} One error per key written off the subTypes that take it.
111
130
  */
112
131
  export function checkLiteratureNote(note) {
113
132
  const type = String(note?.type ?? note?.fm?.type ?? "").toLowerCase();
114
133
  const subType = String(note?.fm?.subType ?? "").toLowerCase();
115
- if (type !== "lore" || subType === LITERATURE_SUBTYPE) return [];
134
+ if (type !== "lore") return [];
116
135
  const data = note?.fm?.data;
117
136
  if (!data || typeof data !== "object" || Array.isArray(data)) return [];
118
- return LITERATURE_KEYS.filter((key) => data[key] !== undefined && data[key] !== null).map(
119
- (key) => ({
120
- file: note.file,
121
- ...positionOfFrontmatterPath(note.raw ?? "", ["data", key], { key: true }),
122
- severity: "error",
123
- message:
124
- `\`data.${key}\` describes a work of literature, and this note's ` +
125
- `\`subType\` is not \`literature\``,
126
- }),
127
- );
137
+ const stated = (key) => data[key] !== undefined && data[key] !== null;
138
+ const finding = (key, message) => ({
139
+ file: note.file,
140
+ ...positionOfFrontmatterPath(note.raw ?? "", ["data", key], { key: true }),
141
+ severity: "error",
142
+ message,
143
+ });
144
+ const findings = [];
145
+ if (subType !== LITERATURE_SUBTYPE)
146
+ for (const key of LITERATURE_KEYS.filter(stated))
147
+ findings.push(
148
+ finding(
149
+ key,
150
+ `\`data.${key}\` describes a work of literature, and this note's ` +
151
+ `\`subType\` is not \`literature\``,
152
+ ),
153
+ );
154
+ if (!LANGUAGE_SUBTYPES.includes(subType) && stated(LANGUAGE_FIELD.name))
155
+ findings.push(
156
+ finding(
157
+ LANGUAGE_FIELD.name,
158
+ "`data.language` names the tongue of a work of literature or of a culture, " +
159
+ "and this note's `subType` is neither `literature` nor `culture`",
160
+ ),
161
+ );
162
+ return findings;
128
163
  }
@@ -111,7 +111,13 @@ import { checkNoteEvents } from "./note-events.mjs";
111
111
  import { AFFILIATION_STANDINGS } from "../sohl/affiliation-standings.mjs";
112
112
  import { EVENT_ENTRY } from "./note-event-terms.mjs";
113
113
  import { CULTURE_FIELD, checkCultureSubType } from "./culture-choice.mjs";
114
- import { LITERATURE_FIELDS, checkLiteratureNote } from "./literature-notes.mjs";
114
+ import {
115
+ checkAppearanceProfile,
116
+ profileLookKeys,
117
+ requiredHeightGenders,
118
+ } from "./appearance-profile.mjs";
119
+ import { LANGUAGE_FIELD, LITERATURE_FIELDS, checkLiteratureNote } from "./literature-notes.mjs";
120
+ import { checkLanguageNames, requiredGivenGenders } from "./language-names.mjs";
115
121
  import { SOCIAL_TIES, SOCIAL_TIE_TARGET_TYPES } from "./social-tie-terms.mjs";
116
122
  import { parseNoteDate } from "./note-dates.mjs";
117
123
  import { reckoningContext } from "./reckoning-markers.mjs";
@@ -429,6 +435,155 @@ function lookField(name, what) {
429
435
  };
430
436
  }
431
437
 
438
+ /**
439
+ * A culture's `data.appearanceProfile`: weighted words for each being-look
440
+ * field but `gender`, and height ranges keyed by gender. Its keys and the words
441
+ * each takes are read from {@link BEING_LOOK_VOCABULARY}; what a profile means
442
+ * and how `package-build npc new` draws from it is in `appearance-profile.mjs`.
443
+ *
444
+ * @type {DataFieldSpec}
445
+ */
446
+ const APPEARANCE_PROFILE_FIELD = Object.freeze({
447
+ name: "appearanceProfile",
448
+ kind: "map",
449
+ shape: "an appearance profile",
450
+ check: (note) => checkAppearanceProfile(note, BEING_LOOK_VOCABULARY),
451
+ describe:
452
+ "What this culture's people look like, for `package-build npc new`: " +
453
+ `${wordList(profileLookKeys(BEING_LOOK_VOCABULARY)).replace(/ or (?!.* or )/, " and ")} ` +
454
+ "each map words of that field's vocabulary to positive weights, and `height` maps a " +
455
+ "gender to a `{min, max}` range, " +
456
+ `${wordList([...requiredHeightGenders()]).replace(" or ", " and ")} required. A culture ` +
457
+ "note only. See [appearance profiles](format-details.md#appearance-profiles).",
458
+ fields: Object.freeze([
459
+ ...profileLookKeys(BEING_LOOK_VOCABULARY).map((key) =>
460
+ Object.freeze({
461
+ name: key,
462
+ kind: "map",
463
+ shape: "a map of words to weights",
464
+ required: true,
465
+ describe: `Weighted \`${key}\` words; a word left out is never drawn.`,
466
+ values: Object.freeze({
467
+ kind: "number",
468
+ shape: "a positive weight",
469
+ describe: "The word's weight against the others in its map.",
470
+ }),
471
+ }),
472
+ ),
473
+ Object.freeze({
474
+ name: "height",
475
+ kind: "map",
476
+ shape: "a map of genders to height ranges",
477
+ required: true,
478
+ describe:
479
+ "Height ranges keyed by gender; a gender with no range draws from the span " +
480
+ "of every range stated.",
481
+ values: Object.freeze({
482
+ kind: "map",
483
+ shape: "a height range",
484
+ describe: "The shortest and tallest height drawn.",
485
+ fields: Object.freeze([
486
+ Object.freeze({
487
+ name: "min",
488
+ kind: "string",
489
+ required: true,
490
+ shape: "a height in metres or feet and inches",
491
+ describe: "The shortest height drawn.",
492
+ }),
493
+ Object.freeze({
494
+ name: "max",
495
+ kind: "string",
496
+ required: true,
497
+ shape: "a height in metres or feet and inches",
498
+ describe: "The tallest height drawn.",
499
+ }),
500
+ ]),
501
+ }),
502
+ }),
503
+ ]),
504
+ });
505
+
506
+ /**
507
+ * A language's `data.names`: the pattern its speakers' names are built by and
508
+ * the lists each part is drawn from. Its keys beyond `pattern` and `given` are
509
+ * named by the language, so the map declares no closed inner keys and
510
+ * {@link checkLanguageNames} checks the whole of it; what the lists mean and
511
+ * how `package-build npc new` draws from them is in `language-names.mjs`.
512
+ *
513
+ * @type {DataFieldSpec}
514
+ */
515
+ const NAMES_FIELD = Object.freeze({
516
+ name: "names",
517
+ kind: "map",
518
+ shape: "a language's name lists",
519
+ check: (note) => checkLanguageNames(note, BEING_LOOK_VOCABULARY),
520
+ describe:
521
+ "How this language's speakers are named, for `package-build npc new`: `pattern` " +
522
+ 'builds a name from placeholders such as "{given} {clan}" and literal text, `given` ' +
523
+ "maps a gender to a list of given names, " +
524
+ `${wordList([...requiredGivenGenders()]).replace(" or ", " and ")} required, and ` +
525
+ "every other key is a list of names a placeholder of the same name draws from. A " +
526
+ "language skill note only. See [name lists](format-details.md#name-lists).",
527
+ fields: Object.freeze([
528
+ Object.freeze({
529
+ name: "pattern",
530
+ kind: "string-list-or-map",
531
+ required: true,
532
+ shape: 'a pattern such as "{given} {clan}", a list of them, or a map of genders to either',
533
+ describe:
534
+ "How a name is built: each `{list}` placeholder is filled from the list of " +
535
+ "that name, `{given.<gender>}` from that gender's given names, and the text " +
536
+ "around them is written as it stands. A list states every form a name may " +
537
+ "take; a map keyed by gender states each gender's forms, and a gender with " +
538
+ "none takes every pattern stated.",
539
+ entries: Object.freeze({
540
+ kind: "string",
541
+ shape: "a pattern",
542
+ describe: "One form a name may take.",
543
+ }),
544
+ values: Object.freeze({
545
+ kind: "string-or-list",
546
+ shape: "a pattern or a list of them",
547
+ describe: "The forms a name of that gender may take.",
548
+ entries: Object.freeze({
549
+ kind: "string",
550
+ shape: "a pattern",
551
+ describe: "One form a name of that gender may take.",
552
+ }),
553
+ }),
554
+ }),
555
+ Object.freeze({
556
+ name: "given",
557
+ kind: "map",
558
+ required: true,
559
+ shape: "a map of genders to lists of names",
560
+ describe:
561
+ "Given names by gender word. A gender with no list draws from every given " +
562
+ "list together.",
563
+ values: Object.freeze({
564
+ kind: "list",
565
+ shape: "a list of names",
566
+ describe: "The given names of one gender.",
567
+ entries: Object.freeze({
568
+ kind: "string",
569
+ shape: "a name",
570
+ describe: "One name, as written.",
571
+ }),
572
+ }),
573
+ }),
574
+ ]),
575
+ values: Object.freeze({
576
+ kind: "list",
577
+ shape: "a list of names",
578
+ describe: "A list a placeholder of the same name draws from, such as `clan` or `family`.",
579
+ entries: Object.freeze({
580
+ kind: "string",
581
+ shape: "a name",
582
+ describe: "One name, as written.",
583
+ }),
584
+ }),
585
+ });
586
+
432
587
  /** A list. */
433
588
  const LIST = Object.freeze({ shape: "list", kind: "list" });
434
589
 
@@ -1659,6 +1814,7 @@ export const NOTE_VOCABULARY = Object.freeze({
1659
1814
  accepts: ["skill"],
1660
1815
  describe: "The skill this one specialises.",
1661
1816
  },
1817
+ NAMES_FIELD,
1662
1818
  ]),
1663
1819
  }),
1664
1820
 
@@ -1833,7 +1989,8 @@ export const NOTE_VOCABULARY = Object.freeze({
1833
1989
  // A lore note is prose, and what it *is* about is its subType — with
1834
1990
  // two exceptions. A calendar is a division of the year, and a division
1835
1991
  // is data; a work of literature states in what tongue it is and what it
1836
- // concerns. The checks scope each family to the subType that means it,
1992
+ // concerns, and a culture what tongue it speaks and what its people look
1993
+ // like. The checks scope each family to the subType that means it,
1837
1994
  // because `DataFieldSpec` declares the keys a type accepts and not the
1838
1995
  // subType that may write them. Any lore note may name the people it
1839
1996
  // belongs to; a culture note names only itself.
@@ -1852,7 +2009,9 @@ export const NOTE_VOCABULARY = Object.freeze({
1852
2009
  "culture, and states the field only as itself.",
1853
2010
  },
1854
2011
  ...LITERATURE_FIELDS,
2012
+ LANGUAGE_FIELD,
1855
2013
  EVENTS_FIELD,
2014
+ APPEARANCE_PROFILE_FIELD,
1856
2015
  ]),
1857
2016
  }),
1858
2017