@heroiclands/package-build 23.2.2 → 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 +29 -0
- package/bin/content-commands.mjs +293 -2
- package/ci/content-format-reference.mjs +28 -5
- package/docs/api.md +37 -23
- package/docs/authoring/frontmatter.md +2 -0
- package/docs/commands.md +183 -0
- package/docs/content-format.md +6 -0
- package/docs/guides/e2e.md +8 -1
- package/docs/reference/format-details.md +471 -46
- package/docs/reference/note-types.md +124 -107
- package/e2e.mjs +37 -10
- package/engine/appearance-profile.mjs +330 -0
- package/engine/content-format.yaml +8 -0
- package/engine/data-keys.mjs +26 -6
- package/engine/index.mjs +3 -0
- package/engine/infobox.mjs +1 -1
- package/engine/language-names.mjs +634 -0
- package/engine/literature-notes.mjs +62 -27
- package/engine/note-vocabulary.mjs +370 -19
- package/engine/npc-generate.mjs +633 -0
- package/engine/settlement-tables.mjs +777 -0
- package/package.json +1 -1
- package/types/e2e.d.mts +5 -1
- package/types/engine/appearance-profile.d.mts +94 -0
- package/types/engine/data-keys.d.mts +2 -2
- package/types/engine/index.d.mts +1 -0
- package/types/engine/infobox.d.mts +1 -1
- package/types/engine/language-names.d.mts +140 -0
- package/types/engine/literature-notes.d.mts +14 -3
- package/types/engine/note-vocabulary.d.mts +31 -0
- package/types/engine/npc-generate.d.mts +124 -0
- package/types/engine/settlement-tables.d.mts +164 -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";
|
|
@@ -127,6 +133,7 @@ import { checkCitedPopulations, checkPopulation } from "./populations.mjs";
|
|
|
127
133
|
// finding an author meets state one list.
|
|
128
134
|
import { MARKET_CLASSES, checkMarket } from "./market-class.mjs";
|
|
129
135
|
import { positionOfFrontmatterPath } from "./diagnostics.mjs";
|
|
136
|
+
import { checkSettlementTables } from "./settlement-tables.mjs";
|
|
130
137
|
|
|
131
138
|
/**
|
|
132
139
|
* One `data:` key a note type may carry.
|
|
@@ -241,11 +248,166 @@ const NUM = Object.freeze({ shape: "number", kind: "number" });
|
|
|
241
248
|
/** A string, including the enumerated-value fields the format spells as one. */
|
|
242
249
|
const TEXT = Object.freeze({ shape: "string", kind: "string" });
|
|
243
250
|
|
|
251
|
+
/**
|
|
252
|
+
* One word of a closed vocabulary: the value a note writes, the label a reader
|
|
253
|
+
* sees it by, and what it means.
|
|
254
|
+
*
|
|
255
|
+
* @typedef {object} LookWord
|
|
256
|
+
* @property {string} value - The word as a note writes it.
|
|
257
|
+
* @property {string} label - The word as a reader sees it.
|
|
258
|
+
* @property {string} describe - What it means, in one line.
|
|
259
|
+
*/
|
|
260
|
+
|
|
261
|
+
/** A frozen word list from `[value, label, describe]` rows. */
|
|
262
|
+
function lookWords(rows) {
|
|
263
|
+
return Object.freeze(
|
|
264
|
+
rows.map(([value, label, describe]) => Object.freeze({ value, label, describe })),
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* The words a being's look is stated in, keyed by the `data:` field that takes
|
|
270
|
+
* them.
|
|
271
|
+
*
|
|
272
|
+
* Each field is closed to its list: any other value, an empty one included, is
|
|
273
|
+
* a finding at the value naming the words it takes. `appearance.complexion`
|
|
274
|
+
* takes one word or a list of them, and each entry is checked on its own.
|
|
275
|
+
* `appearance.extra_features` is not here: it is free text, a list of the marks
|
|
276
|
+
* no word covers.
|
|
277
|
+
*
|
|
278
|
+
* @type {Readonly<Record<string, readonly LookWord[]>>}
|
|
279
|
+
*/
|
|
280
|
+
export const BEING_LOOK_VOCABULARY = Object.freeze({
|
|
281
|
+
gender: lookWords([
|
|
282
|
+
["female", "Female", "Female."],
|
|
283
|
+
["male", "Male", "Male."],
|
|
284
|
+
["nonbinary", "Nonbinary", "Neither male nor female."],
|
|
285
|
+
["none", "None", "The being has no gender, as against one that is simply unrecorded."],
|
|
286
|
+
["other", "Other", "A gender none of these words fits."],
|
|
287
|
+
]),
|
|
288
|
+
frame: lookWords([
|
|
289
|
+
["scant", "Scant", "Very thin, with little flesh over the bone."],
|
|
290
|
+
["light", "Light", "Slender, narrow through shoulder and hip."],
|
|
291
|
+
["medium", "Medium", "The ordinary build, neither slight nor heavy."],
|
|
292
|
+
["heavy", "Heavy", "Broad and solid, in muscle or in flesh."],
|
|
293
|
+
["massive", "Massive", "Exceptionally large in frame."],
|
|
294
|
+
]),
|
|
295
|
+
"appearance.eye_color": lookWords([
|
|
296
|
+
["amber", "Amber", "Golden or coppery, with no grey or green in it; the honey-toned eye."],
|
|
297
|
+
[
|
|
298
|
+
"black",
|
|
299
|
+
"Black",
|
|
300
|
+
"An iris that cannot be told from the pupil in any light, darker than `dark_brown`.",
|
|
301
|
+
],
|
|
302
|
+
["blue", "Blue", "Unmixed blue, from pale ice to deep sea."],
|
|
303
|
+
["brown", "Brown", "Mid-brown, the iris pattern still legible in ordinary light."],
|
|
304
|
+
["dark_brown", "Dark Brown", "Brown so deep the iris barely separates from the pupil."],
|
|
305
|
+
["grey", "Grey", "Grey or grey-blue, cool and without green."],
|
|
306
|
+
["green", "Green", "Green dominant, with no brown ring worth naming."],
|
|
307
|
+
[
|
|
308
|
+
"hazel",
|
|
309
|
+
"Hazel",
|
|
310
|
+
"Brown and green together, often shifting with the light; the commonest mixed eye.",
|
|
311
|
+
],
|
|
312
|
+
["violet", "Violet", "The pink-violet of an iris carrying no pigment."],
|
|
313
|
+
]),
|
|
314
|
+
"appearance.hair_color": lookWords([
|
|
315
|
+
["auburn", "Auburn", "Red-brown with the red dominant."],
|
|
316
|
+
["black", "Black", "True black, with no brown cast in sunlight."],
|
|
317
|
+
["blonde", "Blonde", "Light yellow, from near-white to wheat."],
|
|
318
|
+
["brown", "Brown", "Mid-brown, neither dark nor reddish."],
|
|
319
|
+
[
|
|
320
|
+
"chestnut",
|
|
321
|
+
"Chestnut",
|
|
322
|
+
"Mid-brown with a red cast, the brown dominant — between brown and auburn.",
|
|
323
|
+
],
|
|
324
|
+
["dark_blonde", "Dark Blonde", "Blonde darkened towards light brown."],
|
|
325
|
+
["dark_brown", "Dark Brown", "Brown approaching black, still brown in sunlight."],
|
|
326
|
+
["grey", "Grey", "Grey throughout, with nothing of the original colour left to read."],
|
|
327
|
+
["greying_black", "Greying Black", "Black still dominant, grey coming in."],
|
|
328
|
+
["greying_brown", "Greying Brown", "Brown still dominant, grey coming in."],
|
|
329
|
+
["red", "Red", "Ginger or copper, the red unmixed with brown."],
|
|
330
|
+
["silver", "Silver", "Grey with a bright metallic sheen rather than a dull cast."],
|
|
331
|
+
[
|
|
332
|
+
"strawberry_blond",
|
|
333
|
+
"Strawberry Blond",
|
|
334
|
+
"Pale red-gold: blonde with a red cast, lighter than red.",
|
|
335
|
+
],
|
|
336
|
+
["white", "White", "Wholly white, whether from age or from carrying no pigment."],
|
|
337
|
+
]),
|
|
338
|
+
"appearance.skin_color": lookWords([
|
|
339
|
+
[
|
|
340
|
+
"pale",
|
|
341
|
+
"Pale",
|
|
342
|
+
"The lightest skin, which burns rather than browns; also skin carrying no pigment.",
|
|
343
|
+
],
|
|
344
|
+
["fair", "Fair", "Light with a pink cast, burning before it browns."],
|
|
345
|
+
["light", "Light", "Light and neutral, browning without burning readily."],
|
|
346
|
+
["medium", "Medium", "Neither light nor dark, with no strong cast either way."],
|
|
347
|
+
["olive", "Olive", "Medium with a green-grey cast."],
|
|
348
|
+
["tawny", "Tawny", "Light brown, warm rather than olive."],
|
|
349
|
+
["golden", "Golden", "Light to medium with a yellow cast."],
|
|
350
|
+
["tanned", "Tanned", "Medium, darkened by sun rather than by birth."],
|
|
351
|
+
["swarthy", "Swarthy", "Dark with an olive or sallow cast, between olive and brown."],
|
|
352
|
+
["brown", "Brown", "Mid-brown."],
|
|
353
|
+
[
|
|
354
|
+
"dark",
|
|
355
|
+
"Dark",
|
|
356
|
+
"Darker than brown with no particular cast; the general word where no finer shade is stated.",
|
|
357
|
+
],
|
|
358
|
+
["dark_brown", "Dark Brown", "Deep brown."],
|
|
359
|
+
["ebony", "Ebony", "The darkest skin, near-black."],
|
|
360
|
+
]),
|
|
361
|
+
"appearance.complexion": lookWords([
|
|
362
|
+
["ashen", "Ashen", "Grey and drained; the skin of serious illness, shock or terror."],
|
|
363
|
+
["blotchy", "Blotchy", "Uneven in colour, patched red or pale."],
|
|
364
|
+
["chapped", "Chapped", "Reddened and cracked by cold and wind."],
|
|
365
|
+
["clear", "Clear", "Free of blemish, mark or broken vessel."],
|
|
366
|
+
["freckled", "Freckled", "Freckled, in patches or across the whole face."],
|
|
367
|
+
["jaundiced", "Jaundiced", "Yellowed, the colour a failing liver gives."],
|
|
368
|
+
["leathery", "Leathery", "Thickened and toughened by years of exposure."],
|
|
369
|
+
["oily", "Oily", "Carrying a greasy sheen."],
|
|
370
|
+
["pasty", "Pasty", "Pale and doughy; the skin of indoor work and poor feeding."],
|
|
371
|
+
["pimpled", "Pimpled", "Presently broken out, with pimples at brow, cheek or jaw."],
|
|
372
|
+
["pockmarked", "Pockmarked", "Pitted by pox, or by deep pimples long since healed."],
|
|
373
|
+
["rough", "Rough", "Coarse in texture, without the wear of outdoor work."],
|
|
374
|
+
["ruddy", "Ruddy", "High colour at cheek and nose, from weather, drink or constitution."],
|
|
375
|
+
["sallow", "Sallow", "Yellowish and bloodless."],
|
|
376
|
+
[
|
|
377
|
+
"scabrous",
|
|
378
|
+
"Scabrous",
|
|
379
|
+
"Rough and scaling, the skin of a lasting complaint such as eczema.",
|
|
380
|
+
],
|
|
381
|
+
[
|
|
382
|
+
"scarred",
|
|
383
|
+
"Scarred",
|
|
384
|
+
"Marked by healed wounds. `extra_features` says where and from what.",
|
|
385
|
+
],
|
|
386
|
+
["smooth", "Smooth", "Even in texture, whatever marks it carries."],
|
|
387
|
+
["sun_kissed", "Sun-Kissed", "Browned by sun on skin not naturally that dark."],
|
|
388
|
+
["sunburnt", "Sunburnt", "Presently burned, red and peeling."],
|
|
389
|
+
["translucent", "Translucent", "So pale the vessels show through."],
|
|
390
|
+
[
|
|
391
|
+
"vitiligo",
|
|
392
|
+
"Vitiligo",
|
|
393
|
+
"Patched with white where the skin has lost its colour, most often at face, hands and mouth. The tone it has lost is still what `skin_color` states.",
|
|
394
|
+
],
|
|
395
|
+
["wan", "Wan", "Pale and drained; the skin of exhaustion or convalescence."],
|
|
396
|
+
["weathered", "Weathered", "Worn and lined by sun and wind; the skin of outdoor work."],
|
|
397
|
+
["wrinkled", "Wrinkled", "Deeply lined by age."],
|
|
398
|
+
]),
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
/** The values of one field of {@link BEING_LOOK_VOCABULARY}. */
|
|
402
|
+
function lookValues(field) {
|
|
403
|
+
return Object.freeze(BEING_LOOK_VOCABULARY[field].map(({ value }) => value));
|
|
404
|
+
}
|
|
405
|
+
|
|
244
406
|
/** The words a being's `gender` takes. */
|
|
245
|
-
const BEING_GENDERS =
|
|
407
|
+
const BEING_GENDERS = lookValues("gender");
|
|
246
408
|
|
|
247
409
|
/** The words a being's `frame` takes, lightest to heaviest. */
|
|
248
|
-
const BEING_FRAMES =
|
|
410
|
+
const BEING_FRAMES = lookValues("frame");
|
|
249
411
|
|
|
250
412
|
/** Words as code spans, joined by commas and a final "or". */
|
|
251
413
|
function wordList(words) {
|
|
@@ -253,6 +415,175 @@ function wordList(words) {
|
|
|
253
415
|
return `${spans.slice(0, -1).join(", ")} or ${spans.at(-1)}`;
|
|
254
416
|
}
|
|
255
417
|
|
|
418
|
+
/**
|
|
419
|
+
* A string field closed to one list of {@link BEING_LOOK_VOCABULARY}, its words
|
|
420
|
+
* named in the reference and their meanings linked.
|
|
421
|
+
*
|
|
422
|
+
* @param {string} name - The field, as declared.
|
|
423
|
+
* @param {string} what - What the field states, without a closing stop.
|
|
424
|
+
* @returns {object} The declaration.
|
|
425
|
+
*/
|
|
426
|
+
function lookField(name, what) {
|
|
427
|
+
const allowed = lookValues(name);
|
|
428
|
+
return {
|
|
429
|
+
name,
|
|
430
|
+
...TEXT,
|
|
431
|
+
allowed,
|
|
432
|
+
describe:
|
|
433
|
+
`${what}: one of ${wordList(allowed)}. ` +
|
|
434
|
+
"See [a being's look](format-details.md#a-beings-look) for what each word means.",
|
|
435
|
+
};
|
|
436
|
+
}
|
|
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
|
+
|
|
256
587
|
/** A list. */
|
|
257
588
|
const LIST = Object.freeze({ shape: "list", kind: "list" });
|
|
258
589
|
|
|
@@ -643,17 +974,23 @@ const CHARGES = Object.freeze([
|
|
|
643
974
|
/**
|
|
644
975
|
* Everything `place` asks of a whole note.
|
|
645
976
|
*
|
|
646
|
-
*
|
|
647
|
-
* inhabited place,
|
|
648
|
-
*
|
|
649
|
-
*
|
|
977
|
+
* Three questions about one subject, and they are independent: who governs
|
|
978
|
+
* this inhabited place, what the body states about itself where the note is a
|
|
979
|
+
* body rather than somewhere within one, and whether a region's settlements
|
|
980
|
+
* table agrees with its holding model and the notes it links. A type declares
|
|
981
|
+
* one whole-note check, so the three are composed here rather than any being
|
|
982
|
+
* folded into another.
|
|
650
983
|
*
|
|
651
984
|
* @param {object} note - The note.
|
|
652
985
|
* @param {object} [opts] - The lint's options, passed through unchanged.
|
|
653
|
-
* @returns {object[]} Findings from
|
|
986
|
+
* @returns {object[]} Findings from all three.
|
|
654
987
|
*/
|
|
655
988
|
function checkPlace(note, opts) {
|
|
656
|
-
return [
|
|
989
|
+
return [
|
|
990
|
+
...checkGovernment(note),
|
|
991
|
+
...checkWorldFacts(note, opts),
|
|
992
|
+
...checkSettlementTables(note, opts),
|
|
993
|
+
];
|
|
657
994
|
}
|
|
658
995
|
|
|
659
996
|
/** Validate a being's authored date using the shared note-date grammar. */
|
|
@@ -1036,7 +1373,8 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1036
1373
|
describe:
|
|
1037
1374
|
`One of ${wordList(BEING_GENDERS)}. ` +
|
|
1038
1375
|
"`none` says the being has no gender; an absent field says its " +
|
|
1039
|
-
"gender is unrecorded."
|
|
1376
|
+
"gender is unrecorded. See [a being's look](format-details.md#a-beings-look) " +
|
|
1377
|
+
"for what each word means.",
|
|
1040
1378
|
},
|
|
1041
1379
|
{
|
|
1042
1380
|
name: "species",
|
|
@@ -1106,22 +1444,31 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1106
1444
|
name: "frame",
|
|
1107
1445
|
...TEXT,
|
|
1108
1446
|
allowed: BEING_FRAMES,
|
|
1109
|
-
describe:
|
|
1447
|
+
describe:
|
|
1448
|
+
`Relative frame — one of ${wordList(BEING_FRAMES)}. ` +
|
|
1449
|
+
"See [a being's look](format-details.md#a-beings-look) for what each word means.",
|
|
1110
1450
|
},
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1451
|
+
lookField("appearance.eye_color", "Eye colour"),
|
|
1452
|
+
lookField("appearance.hair_color", "Hair colour"),
|
|
1453
|
+
lookField("appearance.skin_color", "Skin colour"),
|
|
1114
1454
|
{
|
|
1115
|
-
|
|
1455
|
+
...lookField("appearance.complexion", "The skin's condition"),
|
|
1116
1456
|
...TEXT_OR_LIST,
|
|
1117
1457
|
describe:
|
|
1118
|
-
|
|
1119
|
-
"
|
|
1458
|
+
`The skin's condition: ${wordList(lookValues("appearance.complexion"))}, ` +
|
|
1459
|
+
"one word or a list of several, because a face carries more than one at " +
|
|
1460
|
+
"once. A single word means a list of one. See " +
|
|
1461
|
+
"[a being's look](format-details.md#a-beings-look) for what each word means.",
|
|
1462
|
+
entries: Object.freeze({
|
|
1463
|
+
kind: "string",
|
|
1464
|
+
shape: "a string",
|
|
1465
|
+
allowed: lookValues("appearance.complexion"),
|
|
1466
|
+
}),
|
|
1120
1467
|
},
|
|
1121
1468
|
{
|
|
1122
1469
|
name: "appearance.extra_features",
|
|
1123
1470
|
...TEXT_LIST,
|
|
1124
|
-
describe: "Anything else a stranger would notice.",
|
|
1471
|
+
describe: "Anything else a stranger would notice, in free text.",
|
|
1125
1472
|
},
|
|
1126
1473
|
]),
|
|
1127
1474
|
}),
|
|
@@ -1467,6 +1814,7 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1467
1814
|
accepts: ["skill"],
|
|
1468
1815
|
describe: "The skill this one specialises.",
|
|
1469
1816
|
},
|
|
1817
|
+
NAMES_FIELD,
|
|
1470
1818
|
]),
|
|
1471
1819
|
}),
|
|
1472
1820
|
|
|
@@ -1641,7 +1989,8 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1641
1989
|
// A lore note is prose, and what it *is* about is its subType — with
|
|
1642
1990
|
// two exceptions. A calendar is a division of the year, and a division
|
|
1643
1991
|
// is data; a work of literature states in what tongue it is and what it
|
|
1644
|
-
// 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,
|
|
1645
1994
|
// because `DataFieldSpec` declares the keys a type accepts and not the
|
|
1646
1995
|
// subType that may write them. Any lore note may name the people it
|
|
1647
1996
|
// belongs to; a culture note names only itself.
|
|
@@ -1660,7 +2009,9 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1660
2009
|
"culture, and states the field only as itself.",
|
|
1661
2010
|
},
|
|
1662
2011
|
...LITERATURE_FIELDS,
|
|
2012
|
+
LANGUAGE_FIELD,
|
|
1663
2013
|
EVENTS_FIELD,
|
|
2014
|
+
APPEARANCE_PROFILE_FIELD,
|
|
1664
2015
|
]),
|
|
1665
2016
|
}),
|
|
1666
2017
|
|