@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
|
@@ -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
|
}
|
|
@@ -484,6 +484,17 @@ export function loadForeignIndexes(config, localPackages, bases = PACKAGE_BASE)
|
|
|
484
484
|
// place or affiliation inherits from a parent here without
|
|
485
485
|
// walking that parent's own chain.
|
|
486
486
|
culture: record.data?.culture ?? undefined,
|
|
487
|
+
...(parts.type === "lore" && record.subType === "culture" ?
|
|
488
|
+
{
|
|
489
|
+
data: {
|
|
490
|
+
language: record.data?.language,
|
|
491
|
+
appearanceProfile: record.data?.appearanceProfile,
|
|
492
|
+
},
|
|
493
|
+
}
|
|
494
|
+
: {}),
|
|
495
|
+
...(parts.type === "skill" && record.subType === "language" ?
|
|
496
|
+
{ data: { names: record.data?.names } }
|
|
497
|
+
: {}),
|
|
487
498
|
population: record.data?.population ?? undefined,
|
|
488
499
|
...(Object.hasOwn(record.data ?? {}, "government") ?
|
|
489
500
|
{ government: record.data.government }
|
|
@@ -111,7 +111,14 @@ 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 { checkCultureParents } from "./culture-parents.mjs";
|
|
115
|
+
import {
|
|
116
|
+
checkAppearanceProfile,
|
|
117
|
+
profileLookKeys,
|
|
118
|
+
requiredHeightGenders,
|
|
119
|
+
} from "./appearance-profile.mjs";
|
|
120
|
+
import { LANGUAGE_FIELD, LITERATURE_FIELDS, checkLiteratureNote } from "./literature-notes.mjs";
|
|
121
|
+
import { checkLanguageNames, requiredGivenGenders } from "./language-names.mjs";
|
|
115
122
|
import { SOCIAL_TIES, SOCIAL_TIE_TARGET_TYPES } from "./social-tie-terms.mjs";
|
|
116
123
|
import { parseNoteDate } from "./note-dates.mjs";
|
|
117
124
|
import { reckoningContext } from "./reckoning-markers.mjs";
|
|
@@ -429,6 +436,155 @@ function lookField(name, what) {
|
|
|
429
436
|
};
|
|
430
437
|
}
|
|
431
438
|
|
|
439
|
+
/**
|
|
440
|
+
* A culture's `data.appearanceProfile`: weighted words for each being-look
|
|
441
|
+
* field but `gender`, and height ranges keyed by gender. Its keys and the words
|
|
442
|
+
* each takes are read from {@link BEING_LOOK_VOCABULARY}; what a profile means
|
|
443
|
+
* and how `package-build npc new` draws from it is in `appearance-profile.mjs`.
|
|
444
|
+
*
|
|
445
|
+
* @type {DataFieldSpec}
|
|
446
|
+
*/
|
|
447
|
+
const APPEARANCE_PROFILE_FIELD = Object.freeze({
|
|
448
|
+
name: "appearanceProfile",
|
|
449
|
+
kind: "map",
|
|
450
|
+
shape: "an appearance profile",
|
|
451
|
+
check: (note) => checkAppearanceProfile(note, BEING_LOOK_VOCABULARY),
|
|
452
|
+
describe:
|
|
453
|
+
"What this culture's people look like, for `package-build npc new`: " +
|
|
454
|
+
`${wordList(profileLookKeys(BEING_LOOK_VOCABULARY)).replace(/ or (?!.* or )/, " and ")} ` +
|
|
455
|
+
"each map words of that field's vocabulary to positive weights, and `height` maps a " +
|
|
456
|
+
"gender to a `{min, max}` range, " +
|
|
457
|
+
`${wordList([...requiredHeightGenders()]).replace(" or ", " and ")} required. A culture ` +
|
|
458
|
+
"note only. See [appearance profiles](format-details.md#appearance-profiles).",
|
|
459
|
+
fields: Object.freeze([
|
|
460
|
+
...profileLookKeys(BEING_LOOK_VOCABULARY).map((key) =>
|
|
461
|
+
Object.freeze({
|
|
462
|
+
name: key,
|
|
463
|
+
kind: "map",
|
|
464
|
+
shape: "a map of words to weights",
|
|
465
|
+
required: true,
|
|
466
|
+
describe: `Weighted \`${key}\` words; a word left out is never drawn.`,
|
|
467
|
+
values: Object.freeze({
|
|
468
|
+
kind: "number",
|
|
469
|
+
shape: "a positive weight",
|
|
470
|
+
describe: "The word's weight against the others in its map.",
|
|
471
|
+
}),
|
|
472
|
+
}),
|
|
473
|
+
),
|
|
474
|
+
Object.freeze({
|
|
475
|
+
name: "height",
|
|
476
|
+
kind: "map",
|
|
477
|
+
shape: "a map of genders to height ranges",
|
|
478
|
+
required: true,
|
|
479
|
+
describe:
|
|
480
|
+
"Height ranges keyed by gender; a gender with no range draws from the span " +
|
|
481
|
+
"of every range stated.",
|
|
482
|
+
values: Object.freeze({
|
|
483
|
+
kind: "map",
|
|
484
|
+
shape: "a height range",
|
|
485
|
+
describe: "The shortest and tallest height drawn.",
|
|
486
|
+
fields: Object.freeze([
|
|
487
|
+
Object.freeze({
|
|
488
|
+
name: "min",
|
|
489
|
+
kind: "string",
|
|
490
|
+
required: true,
|
|
491
|
+
shape: "a height in metres or feet and inches",
|
|
492
|
+
describe: "The shortest height drawn.",
|
|
493
|
+
}),
|
|
494
|
+
Object.freeze({
|
|
495
|
+
name: "max",
|
|
496
|
+
kind: "string",
|
|
497
|
+
required: true,
|
|
498
|
+
shape: "a height in metres or feet and inches",
|
|
499
|
+
describe: "The tallest height drawn.",
|
|
500
|
+
}),
|
|
501
|
+
]),
|
|
502
|
+
}),
|
|
503
|
+
}),
|
|
504
|
+
]),
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* A language's `data.names`: the pattern its speakers' names are built by and
|
|
509
|
+
* the lists each part is drawn from. Its keys beyond `pattern` and `given` are
|
|
510
|
+
* named by the language, so the map declares no closed inner keys and
|
|
511
|
+
* {@link checkLanguageNames} checks the whole of it; what the lists mean and
|
|
512
|
+
* how `package-build npc new` draws from them is in `language-names.mjs`.
|
|
513
|
+
*
|
|
514
|
+
* @type {DataFieldSpec}
|
|
515
|
+
*/
|
|
516
|
+
const NAMES_FIELD = Object.freeze({
|
|
517
|
+
name: "names",
|
|
518
|
+
kind: "map",
|
|
519
|
+
shape: "a language's name lists",
|
|
520
|
+
check: (note) => checkLanguageNames(note, BEING_LOOK_VOCABULARY),
|
|
521
|
+
describe:
|
|
522
|
+
"How this language's speakers are named, for `package-build npc new`: `pattern` " +
|
|
523
|
+
'builds a name from placeholders such as "{given} {clan}" and literal text, `given` ' +
|
|
524
|
+
"maps a gender to a list of given names, " +
|
|
525
|
+
`${wordList([...requiredGivenGenders()]).replace(" or ", " and ")} required, and ` +
|
|
526
|
+
"every other key is a list of names a placeholder of the same name draws from. A " +
|
|
527
|
+
"language skill note only. See [name lists](format-details.md#name-lists).",
|
|
528
|
+
fields: Object.freeze([
|
|
529
|
+
Object.freeze({
|
|
530
|
+
name: "pattern",
|
|
531
|
+
kind: "string-list-or-map",
|
|
532
|
+
required: true,
|
|
533
|
+
shape: 'a pattern such as "{given} {clan}", a list of them, or a map of genders to either',
|
|
534
|
+
describe:
|
|
535
|
+
"How a name is built: each `{list}` placeholder is filled from the list of " +
|
|
536
|
+
"that name, `{given.<gender>}` from that gender's given names, and the text " +
|
|
537
|
+
"around them is written as it stands. A list states every form a name may " +
|
|
538
|
+
"take; a map keyed by gender states each gender's forms, and a gender with " +
|
|
539
|
+
"none takes every pattern stated.",
|
|
540
|
+
entries: Object.freeze({
|
|
541
|
+
kind: "string",
|
|
542
|
+
shape: "a pattern",
|
|
543
|
+
describe: "One form a name may take.",
|
|
544
|
+
}),
|
|
545
|
+
values: Object.freeze({
|
|
546
|
+
kind: "string-or-list",
|
|
547
|
+
shape: "a pattern or a list of them",
|
|
548
|
+
describe: "The forms a name of that gender may take.",
|
|
549
|
+
entries: Object.freeze({
|
|
550
|
+
kind: "string",
|
|
551
|
+
shape: "a pattern",
|
|
552
|
+
describe: "One form a name of that gender may take.",
|
|
553
|
+
}),
|
|
554
|
+
}),
|
|
555
|
+
}),
|
|
556
|
+
Object.freeze({
|
|
557
|
+
name: "given",
|
|
558
|
+
kind: "map",
|
|
559
|
+
required: true,
|
|
560
|
+
shape: "a map of genders to lists of names",
|
|
561
|
+
describe:
|
|
562
|
+
"Given names by gender word. A gender with no list draws from every given " +
|
|
563
|
+
"list together.",
|
|
564
|
+
values: Object.freeze({
|
|
565
|
+
kind: "list",
|
|
566
|
+
shape: "a list of names",
|
|
567
|
+
describe: "The given names of one gender.",
|
|
568
|
+
entries: Object.freeze({
|
|
569
|
+
kind: "string",
|
|
570
|
+
shape: "a name",
|
|
571
|
+
describe: "One name, as written.",
|
|
572
|
+
}),
|
|
573
|
+
}),
|
|
574
|
+
}),
|
|
575
|
+
]),
|
|
576
|
+
values: Object.freeze({
|
|
577
|
+
kind: "list",
|
|
578
|
+
shape: "a list of names",
|
|
579
|
+
describe: "A list a placeholder of the same name draws from, such as `clan` or `family`.",
|
|
580
|
+
entries: Object.freeze({
|
|
581
|
+
kind: "string",
|
|
582
|
+
shape: "a name",
|
|
583
|
+
describe: "One name, as written.",
|
|
584
|
+
}),
|
|
585
|
+
}),
|
|
586
|
+
});
|
|
587
|
+
|
|
432
588
|
/** A list. */
|
|
433
589
|
const LIST = Object.freeze({ shape: "list", kind: "list" });
|
|
434
590
|
|
|
@@ -1659,6 +1815,7 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1659
1815
|
accepts: ["skill"],
|
|
1660
1816
|
describe: "The skill this one specialises.",
|
|
1661
1817
|
},
|
|
1818
|
+
NAMES_FIELD,
|
|
1662
1819
|
]),
|
|
1663
1820
|
}),
|
|
1664
1821
|
|
|
@@ -1833,7 +1990,8 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1833
1990
|
// A lore note is prose, and what it *is* about is its subType — with
|
|
1834
1991
|
// two exceptions. A calendar is a division of the year, and a division
|
|
1835
1992
|
// is data; a work of literature states in what tongue it is and what it
|
|
1836
|
-
// concerns
|
|
1993
|
+
// concerns, and a culture what tongue it speaks and what its people look
|
|
1994
|
+
// like. The checks scope each family to the subType that means it,
|
|
1837
1995
|
// because `DataFieldSpec` declares the keys a type accepts and not the
|
|
1838
1996
|
// subType that may write them. Any lore note may name the people it
|
|
1839
1997
|
// belongs to; a culture note names only itself.
|
|
@@ -1844,6 +2002,15 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1844
2002
|
],
|
|
1845
2003
|
data: Object.freeze([
|
|
1846
2004
|
...CALENDAR_FIELDS,
|
|
2005
|
+
{
|
|
2006
|
+
name: "parents",
|
|
2007
|
+
...LINKS,
|
|
2008
|
+
ref: "lore",
|
|
2009
|
+
accepts: ["lore"],
|
|
2010
|
+
check: checkCultureParents,
|
|
2011
|
+
describe:
|
|
2012
|
+
"Culture only: parent culture lore notes, searched in listed depth-first order for omitted language and appearance profile fields.",
|
|
2013
|
+
},
|
|
1847
2014
|
{
|
|
1848
2015
|
...CULTURE_FIELD,
|
|
1849
2016
|
describe:
|
|
@@ -1852,7 +2019,9 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1852
2019
|
"culture, and states the field only as itself.",
|
|
1853
2020
|
},
|
|
1854
2021
|
...LITERATURE_FIELDS,
|
|
2022
|
+
LANGUAGE_FIELD,
|
|
1855
2023
|
EVENTS_FIELD,
|
|
2024
|
+
APPEARANCE_PROFILE_FIELD,
|
|
1856
2025
|
]),
|
|
1857
2026
|
}),
|
|
1858
2027
|
|