@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 +12 -0
- package/bin/content-commands.mjs +293 -2
- package/docs/commands.md +183 -0
- package/docs/content-format.md +2 -0
- package/docs/reference/format-details.md +211 -39
- package/docs/reference/note-types.md +97 -80
- package/engine/appearance-profile.mjs +330 -0
- package/engine/content-format.yaml +2 -0
- package/engine/data-keys.mjs +20 -4
- package/engine/language-names.mjs +634 -0
- package/engine/literature-notes.mjs +62 -27
- package/engine/note-vocabulary.mjs +161 -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/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
|
@@ -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
|
-
*
|
|
17
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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"
|
|
134
|
+
if (type !== "lore") return [];
|
|
116
135
|
const data = note?.fm?.data;
|
|
117
136
|
if (!data || typeof data !== "object" || Array.isArray(data)) return [];
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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 {
|
|
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
|
|
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
|
|