@heroiclands/package-build 23.2.2 → 23.3.0
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 +17 -0
- package/ci/content-format-reference.mjs +28 -5
- package/docs/api.md +37 -23
- package/docs/authoring/frontmatter.md +2 -0
- package/docs/content-format.md +4 -0
- package/docs/guides/e2e.md +8 -1
- package/docs/reference/format-details.md +260 -7
- package/docs/reference/note-types.md +27 -27
- package/e2e.mjs +37 -10
- package/engine/content-format.yaml +6 -0
- package/engine/data-keys.mjs +6 -2
- package/engine/index.mjs +3 -0
- package/engine/infobox.mjs +1 -1
- package/engine/note-vocabulary.mjs +209 -17
- package/engine/settlement-tables.mjs +777 -0
- package/package.json +1 -1
- package/types/e2e.d.mts +5 -1
- package/types/engine/index.d.mts +1 -0
- package/types/engine/infobox.d.mts +1 -1
- package/types/engine/note-vocabulary.d.mts +31 -0
- package/types/engine/settlement-tables.d.mts +164 -0
|
@@ -53,33 +53,33 @@ These fields are accepted by every note type. A field's value can still be irrel
|
|
|
53
53
|
|
|
54
54
|
**Subtypes:** `npc`, `character`, `creature`.
|
|
55
55
|
|
|
56
|
-
| Field | Shape | Meaning
|
|
57
|
-
| -------------------------------- | ------------------------------- |
|
|
58
|
-
| `data.icon` | an Address | The document's profile art — an `icon` address, resolved into `img`.
|
|
59
|
-
| `data.tokenIcon` | an Address | What a token on the canvas wears — an `icon` address; unset, it follows `icon`.
|
|
60
|
-
| `data.templatePriority` | number | Template priority; unset means the note is not a template.
|
|
61
|
-
| `data.archetypes` | list of strings | Archetypal behaviours the being fits.
|
|
62
|
-
| `data.occupation` | string | What the being does for a living.
|
|
63
|
-
| `data.lore` | list of Addresses | Lore concerning this being — the law it lives under, the customs it is subject to, the traditions it was raised in.
|
|
64
|
-
| `data.culture` | an Address | The being's primary culture, as a culture lore note.
|
|
65
|
-
| `data.homes` | list of Addresses | Places the being calls home.
|
|
66
|
-
| `data.affiliations` | a standing map keyed by Address | Bodies the being belongs to, keyed by Address, each entry holding the standing it holds there — `rank`, required, a level on that body's own ladder, and `office`, optional, a post that body names.
|
|
67
|
-
| `data.socialTies` | a map keyed by Address | Defining ties directed from this being to others: `patron` (Supports the subject from greater power, wealth, or status.); `friend` (Supports the subject out of goodwill.); `dependent` (Relies on the subject's support or protection.); `acquaintance` (Knows the subject without a strong disposition.); `rival` (Opposes the subject without implacable hostility.); `nemesis` (Opposes the subject personally and implacably.)
|
|
68
|
-
| `data.gender` | string | One of `
|
|
69
|
-
| `data.species` | an Address | The being's species, as a lore note.
|
|
70
|
-
| `data.born` | string | When the being was born — a date, or `unknown` where the birth is unrecorded. Absent, the being was never born.
|
|
71
|
-
| `data.calendar` | an Address | Calendar that displays this being's dates.
|
|
72
|
-
| `data.died` | string | When the being died — a date, or `unknown` where the death is unrecorded. Absent, the being is alive.
|
|
73
|
-
| `data.age` | string | Age in years, stated only to override what `born` says — `34`, or `~34` for an estimate. Unstated beside a dated `born` it is computed; unstated beside an unknown or absent `born` the age is unknown.
|
|
74
|
-
| `data.ageYears` | number | Written by the compiler beside an `age` estimate — the `~` stripped, the magnitude alone. A note never authors this.
|
|
75
|
-
| `data.height` | metres or feet and inches | Height in metres or feet and inches.
|
|
76
|
-
| `data.weight` | kilograms or pounds | Body weight in kilograms or pounds.
|
|
77
|
-
| `data.frame` | string | Relative frame — one of `scant`, `light`, `medium`, `heavy` or `massive`.
|
|
78
|
-
| `data.appearance.eye_color` | string | Eye colour.
|
|
79
|
-
| `data.appearance.hair_color` | string | Hair colour.
|
|
80
|
-
| `data.appearance.skin_color` | string | Skin colour.
|
|
81
|
-
| `data.appearance.complexion` | string or list | The skin's condition
|
|
82
|
-
| `data.appearance.extra_features` | list of strings | Anything else a stranger would notice.
|
|
56
|
+
| Field | Shape | Meaning |
|
|
57
|
+
| -------------------------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
58
|
+
| `data.icon` | an Address | The document's profile art — an `icon` address, resolved into `img`. |
|
|
59
|
+
| `data.tokenIcon` | an Address | What a token on the canvas wears — an `icon` address; unset, it follows `icon`. |
|
|
60
|
+
| `data.templatePriority` | number | Template priority; unset means the note is not a template. |
|
|
61
|
+
| `data.archetypes` | list of strings | Archetypal behaviours the being fits. |
|
|
62
|
+
| `data.occupation` | string | What the being does for a living. |
|
|
63
|
+
| `data.lore` | list of Addresses | Lore concerning this being — the law it lives under, the customs it is subject to, the traditions it was raised in. |
|
|
64
|
+
| `data.culture` | an Address | The being's primary culture, as a culture lore note. |
|
|
65
|
+
| `data.homes` | list of Addresses | Places the being calls home. |
|
|
66
|
+
| `data.affiliations` | a standing map keyed by Address | Bodies the being belongs to, keyed by Address, each entry holding the standing it holds there — `rank`, required, a level on that body's own ladder, and `office`, optional, a post that body names. |
|
|
67
|
+
| `data.socialTies` | a map keyed by Address | Defining ties directed from this being to others: `patron` (Supports the subject from greater power, wealth, or status.); `friend` (Supports the subject out of goodwill.); `dependent` (Relies on the subject's support or protection.); `acquaintance` (Knows the subject without a strong disposition.); `rival` (Opposes the subject without implacable hostility.); `nemesis` (Opposes the subject personally and implacably.) |
|
|
68
|
+
| `data.gender` | string | One of `female`, `male`, `nonbinary`, `none` or `other`. `none` says the being has no gender; an absent field says its gender is unrecorded. See [a being's look](format-details.md#a-beings-look) for what each word means. |
|
|
69
|
+
| `data.species` | an Address | The being's species, as a lore note. |
|
|
70
|
+
| `data.born` | string | When the being was born — a date, or `unknown` where the birth is unrecorded. Absent, the being was never born. |
|
|
71
|
+
| `data.calendar` | an Address | Calendar that displays this being's dates. |
|
|
72
|
+
| `data.died` | string | When the being died — a date, or `unknown` where the death is unrecorded. Absent, the being is alive. |
|
|
73
|
+
| `data.age` | string | Age in years, stated only to override what `born` says — `34`, or `~34` for an estimate. Unstated beside a dated `born` it is computed; unstated beside an unknown or absent `born` the age is unknown. |
|
|
74
|
+
| `data.ageYears` | number | Written by the compiler beside an `age` estimate — the `~` stripped, the magnitude alone. A note never authors this. |
|
|
75
|
+
| `data.height` | metres or feet and inches | Height in metres or feet and inches. |
|
|
76
|
+
| `data.weight` | kilograms or pounds | Body weight in kilograms or pounds. |
|
|
77
|
+
| `data.frame` | string | Relative frame — one of `scant`, `light`, `medium`, `heavy` or `massive`. See [a being's look](format-details.md#a-beings-look) for what each word means. |
|
|
78
|
+
| `data.appearance.eye_color` | string | Eye colour: one of `amber`, `black`, `blue`, `brown`, `dark_brown`, `grey`, `green`, `hazel` or `violet`. See [a being's look](format-details.md#a-beings-look) for what each word means. |
|
|
79
|
+
| `data.appearance.hair_color` | string | Hair colour: one of `auburn`, `black`, `blonde`, `brown`, `chestnut`, `dark_blonde`, `dark_brown`, `grey`, `greying_black`, `greying_brown`, `red`, `silver`, `strawberry_blond` or `white`. See [a being's look](format-details.md#a-beings-look) for what each word means. |
|
|
80
|
+
| `data.appearance.skin_color` | string | Skin colour: one of `pale`, `fair`, `light`, `medium`, `olive`, `tawny`, `golden`, `tanned`, `swarthy`, `brown`, `dark`, `dark_brown` or `ebony`. See [a being's look](format-details.md#a-beings-look) for what each word means. |
|
|
81
|
+
| `data.appearance.complexion` | string or list | The skin's condition: `ashen`, `blotchy`, `chapped`, `clear`, `freckled`, `jaundiced`, `leathery`, `oily`, `pasty`, `pimpled`, `pockmarked`, `rough`, `ruddy`, `sallow`, `scabrous`, `scarred`, `smooth`, `sun_kissed`, `sunburnt`, `translucent`, `vitiligo`, `wan`, `weathered` or `wrinkled`, one word or a list of several, because a face carries more than one at once. A single word means a list of one. See [a being's look](format-details.md#a-beings-look) for what each word means. |
|
|
82
|
+
| `data.appearance.extra_features` | list of strings | Anything else a stranger would notice, in free text. |
|
|
83
83
|
|
|
84
84
|
**Inner keys** — a key a field's value does not declare is an error at its own line and column.
|
|
85
85
|
|
package/e2e.mjs
CHANGED
|
@@ -636,26 +636,48 @@ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
636
636
|
* @param {string} opts.url - The container's base URL.
|
|
637
637
|
* @param {string} opts.container - The container name, for its log.
|
|
638
638
|
* @param {string} opts.stage - The stage, named in a licence failure.
|
|
639
|
+
* @param {string} [opts.dataRoot] - The stage's data root, named when its licence is unsigned.
|
|
640
|
+
* @param {(container: string) => string} [opts.readLog] - Reads the container's log since it started.
|
|
639
641
|
* @param {number} [opts.timeoutMs] - How long to wait.
|
|
640
642
|
* @param {(message: string) => void} [opts.log] - Progress reporting.
|
|
641
643
|
* @returns {Promise<void>} Resolves once the world is active.
|
|
642
644
|
* @throws {Error} On a licence failure or a timeout.
|
|
643
645
|
*/
|
|
644
|
-
export async function waitForWorld({
|
|
646
|
+
export async function waitForWorld({
|
|
647
|
+
url,
|
|
648
|
+
container,
|
|
649
|
+
stage,
|
|
650
|
+
dataRoot,
|
|
651
|
+
readLog = captureContainerLog,
|
|
652
|
+
timeoutMs = 180_000,
|
|
653
|
+
log = () => {},
|
|
654
|
+
}) {
|
|
645
655
|
const join = `${url}/join`;
|
|
646
656
|
const deadline = Date.now() + timeoutMs;
|
|
647
657
|
log(`Waiting for the world at ${join} …`);
|
|
648
658
|
while (Date.now() < deadline) {
|
|
649
|
-
const logs =
|
|
659
|
+
const logs = readLog(container);
|
|
660
|
+
if (/software license requires signature/i.test(logs)) {
|
|
661
|
+
throw new Error(
|
|
662
|
+
"The Foundry licence is not signed, so Foundry serves its licence " +
|
|
663
|
+
"page and no world activates. Each installation fetches its own " +
|
|
664
|
+
"signature with the owner's Foundry account, once per data root: " +
|
|
665
|
+
`run \`package-build container ${stage} start\` with no world ` +
|
|
666
|
+
`(FOUNDRYVTT_${stage.toUpperCase()}_WORLD unset), open the stage in a ` +
|
|
667
|
+
"browser and accept the licence agreement. The signature then " +
|
|
668
|
+
`persists in Config/license.json${dataRoot ? ` under ${dataRoot}` : " in the data root"}. ` +
|
|
669
|
+
"The image applies the key but never signs it.",
|
|
670
|
+
);
|
|
671
|
+
}
|
|
650
672
|
if (/license verification failed/i.test(logs)) {
|
|
651
673
|
throw new Error(
|
|
652
674
|
"Foundry licence verification failed. The end-to-end container " +
|
|
653
|
-
"needs its own
|
|
654
|
-
`FOUNDRYVTT_${stage.toUpperCase()}_LICENSE_KEY ` +
|
|
655
|
-
"
|
|
656
|
-
"
|
|
657
|
-
"
|
|
658
|
-
"
|
|
675
|
+
"needs its own signed licence, set with " +
|
|
676
|
+
`FOUNDRYVTT_${stage.toUpperCase()}_LICENSE_KEY and signed by ` +
|
|
677
|
+
"accepting the licence agreement in Foundry's own page once " +
|
|
678
|
+
"per data root. Another installation's license.json does not " +
|
|
679
|
+
"verify, and a licence shared with a running container is " +
|
|
680
|
+
"single-seat and will fail too.",
|
|
659
681
|
);
|
|
660
682
|
}
|
|
661
683
|
try {
|
|
@@ -677,12 +699,15 @@ export async function waitForWorld({ url, container, stage, timeoutMs = 180_000,
|
|
|
677
699
|
|
|
678
700
|
/**
|
|
679
701
|
* @param {string} container - Container name.
|
|
680
|
-
* @returns {string}
|
|
702
|
+
* @returns {string} Its log since it last started, or `""`.
|
|
681
703
|
*/
|
|
682
704
|
function captureContainerLog(container) {
|
|
683
|
-
const
|
|
705
|
+
const started = spawnSync("docker", ["inspect", "-f", "{{.State.StartedAt}}", container], {
|
|
684
706
|
encoding: "utf8",
|
|
685
707
|
});
|
|
708
|
+
const since = (started.stdout ?? "").trim();
|
|
709
|
+
const args = since ? ["logs", "--since", since, container] : ["logs", container];
|
|
710
|
+
const result = spawnSync("docker", args, { encoding: "utf8" });
|
|
686
711
|
return `${result.stdout ?? ""}${result.stderr ?? ""}`;
|
|
687
712
|
}
|
|
688
713
|
|
|
@@ -1098,6 +1123,7 @@ export async function e2eRun({
|
|
|
1098
1123
|
url: container.url,
|
|
1099
1124
|
container: container.name,
|
|
1100
1125
|
stage,
|
|
1126
|
+
dataRoot: resolveDataRoot(stage, { env }),
|
|
1101
1127
|
log,
|
|
1102
1128
|
});
|
|
1103
1129
|
return runSuite({
|
|
@@ -1175,6 +1201,7 @@ export async function e2eFast({ config, argv = [], env = process.env, log = () =
|
|
|
1175
1201
|
url: container.url,
|
|
1176
1202
|
container: container.name,
|
|
1177
1203
|
stage,
|
|
1204
|
+
dataRoot: resolveDataRoot(stage, { env }),
|
|
1178
1205
|
log,
|
|
1179
1206
|
});
|
|
1180
1207
|
|
package/engine/data-keys.mjs
CHANGED
|
@@ -289,9 +289,13 @@ export function checkDataKeys(note, fields) {
|
|
|
289
289
|
walk(spec, value, path);
|
|
290
290
|
};
|
|
291
291
|
|
|
292
|
-
/**
|
|
292
|
+
/**
|
|
293
|
+
* Whether a present value is in the spec's closed set, if it has one; else a
|
|
294
|
+
* finding. A list is not itself a word: its entries are checked against
|
|
295
|
+
* their own declaration as the walk reaches them.
|
|
296
|
+
*/
|
|
293
297
|
const inAllowed = (spec, value, path) => {
|
|
294
|
-
if (!spec.allowed || spec.allowed.includes(value)) return true;
|
|
298
|
+
if (!spec.allowed || Array.isArray(value) || spec.allowed.includes(value)) return true;
|
|
295
299
|
const guess = typeof value === "string" ? nearest(value, spec.allowed) : undefined;
|
|
296
300
|
findings.push(
|
|
297
301
|
finding(
|
package/engine/index.mjs
CHANGED
|
@@ -103,6 +103,9 @@ export * as populations from "./populations.mjs";
|
|
|
103
103
|
/** What trade a settlement supports: the six-step market scale, and the check that holds a value to it. */
|
|
104
104
|
export * as marketClass from "./market-class.mjs";
|
|
105
105
|
|
|
106
|
+
/** A region's settlements table: the holding models, the columns, and the check that holds a table to them. */
|
|
107
|
+
export * as settlementTables from "./settlement-tables.mjs";
|
|
108
|
+
|
|
106
109
|
/** The places a map is drawn from, read from the content index, and what `parents` says about them. */
|
|
107
110
|
export * as mapPlaces from "./map-places.mjs";
|
|
108
111
|
|
package/engine/infobox.mjs
CHANGED
|
@@ -491,7 +491,7 @@ export function humanizeFieldName(name) {
|
|
|
491
491
|
}
|
|
492
492
|
|
|
493
493
|
/**
|
|
494
|
-
* An enumerated value as a reader sees it: `
|
|
494
|
+
* An enumerated value as a reader sees it: `greying_brown` → `greying brown`.
|
|
495
495
|
*
|
|
496
496
|
* @param {unknown} value - The authored value.
|
|
497
497
|
* @returns {string} The text.
|
|
@@ -127,6 +127,7 @@ import { checkCitedPopulations, checkPopulation } from "./populations.mjs";
|
|
|
127
127
|
// finding an author meets state one list.
|
|
128
128
|
import { MARKET_CLASSES, checkMarket } from "./market-class.mjs";
|
|
129
129
|
import { positionOfFrontmatterPath } from "./diagnostics.mjs";
|
|
130
|
+
import { checkSettlementTables } from "./settlement-tables.mjs";
|
|
130
131
|
|
|
131
132
|
/**
|
|
132
133
|
* One `data:` key a note type may carry.
|
|
@@ -241,11 +242,166 @@ const NUM = Object.freeze({ shape: "number", kind: "number" });
|
|
|
241
242
|
/** A string, including the enumerated-value fields the format spells as one. */
|
|
242
243
|
const TEXT = Object.freeze({ shape: "string", kind: "string" });
|
|
243
244
|
|
|
245
|
+
/**
|
|
246
|
+
* One word of a closed vocabulary: the value a note writes, the label a reader
|
|
247
|
+
* sees it by, and what it means.
|
|
248
|
+
*
|
|
249
|
+
* @typedef {object} LookWord
|
|
250
|
+
* @property {string} value - The word as a note writes it.
|
|
251
|
+
* @property {string} label - The word as a reader sees it.
|
|
252
|
+
* @property {string} describe - What it means, in one line.
|
|
253
|
+
*/
|
|
254
|
+
|
|
255
|
+
/** A frozen word list from `[value, label, describe]` rows. */
|
|
256
|
+
function lookWords(rows) {
|
|
257
|
+
return Object.freeze(
|
|
258
|
+
rows.map(([value, label, describe]) => Object.freeze({ value, label, describe })),
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* The words a being's look is stated in, keyed by the `data:` field that takes
|
|
264
|
+
* them.
|
|
265
|
+
*
|
|
266
|
+
* Each field is closed to its list: any other value, an empty one included, is
|
|
267
|
+
* a finding at the value naming the words it takes. `appearance.complexion`
|
|
268
|
+
* takes one word or a list of them, and each entry is checked on its own.
|
|
269
|
+
* `appearance.extra_features` is not here: it is free text, a list of the marks
|
|
270
|
+
* no word covers.
|
|
271
|
+
*
|
|
272
|
+
* @type {Readonly<Record<string, readonly LookWord[]>>}
|
|
273
|
+
*/
|
|
274
|
+
export const BEING_LOOK_VOCABULARY = Object.freeze({
|
|
275
|
+
gender: lookWords([
|
|
276
|
+
["female", "Female", "Female."],
|
|
277
|
+
["male", "Male", "Male."],
|
|
278
|
+
["nonbinary", "Nonbinary", "Neither male nor female."],
|
|
279
|
+
["none", "None", "The being has no gender, as against one that is simply unrecorded."],
|
|
280
|
+
["other", "Other", "A gender none of these words fits."],
|
|
281
|
+
]),
|
|
282
|
+
frame: lookWords([
|
|
283
|
+
["scant", "Scant", "Very thin, with little flesh over the bone."],
|
|
284
|
+
["light", "Light", "Slender, narrow through shoulder and hip."],
|
|
285
|
+
["medium", "Medium", "The ordinary build, neither slight nor heavy."],
|
|
286
|
+
["heavy", "Heavy", "Broad and solid, in muscle or in flesh."],
|
|
287
|
+
["massive", "Massive", "Exceptionally large in frame."],
|
|
288
|
+
]),
|
|
289
|
+
"appearance.eye_color": lookWords([
|
|
290
|
+
["amber", "Amber", "Golden or coppery, with no grey or green in it; the honey-toned eye."],
|
|
291
|
+
[
|
|
292
|
+
"black",
|
|
293
|
+
"Black",
|
|
294
|
+
"An iris that cannot be told from the pupil in any light, darker than `dark_brown`.",
|
|
295
|
+
],
|
|
296
|
+
["blue", "Blue", "Unmixed blue, from pale ice to deep sea."],
|
|
297
|
+
["brown", "Brown", "Mid-brown, the iris pattern still legible in ordinary light."],
|
|
298
|
+
["dark_brown", "Dark Brown", "Brown so deep the iris barely separates from the pupil."],
|
|
299
|
+
["grey", "Grey", "Grey or grey-blue, cool and without green."],
|
|
300
|
+
["green", "Green", "Green dominant, with no brown ring worth naming."],
|
|
301
|
+
[
|
|
302
|
+
"hazel",
|
|
303
|
+
"Hazel",
|
|
304
|
+
"Brown and green together, often shifting with the light; the commonest mixed eye.",
|
|
305
|
+
],
|
|
306
|
+
["violet", "Violet", "The pink-violet of an iris carrying no pigment."],
|
|
307
|
+
]),
|
|
308
|
+
"appearance.hair_color": lookWords([
|
|
309
|
+
["auburn", "Auburn", "Red-brown with the red dominant."],
|
|
310
|
+
["black", "Black", "True black, with no brown cast in sunlight."],
|
|
311
|
+
["blonde", "Blonde", "Light yellow, from near-white to wheat."],
|
|
312
|
+
["brown", "Brown", "Mid-brown, neither dark nor reddish."],
|
|
313
|
+
[
|
|
314
|
+
"chestnut",
|
|
315
|
+
"Chestnut",
|
|
316
|
+
"Mid-brown with a red cast, the brown dominant — between brown and auburn.",
|
|
317
|
+
],
|
|
318
|
+
["dark_blonde", "Dark Blonde", "Blonde darkened towards light brown."],
|
|
319
|
+
["dark_brown", "Dark Brown", "Brown approaching black, still brown in sunlight."],
|
|
320
|
+
["grey", "Grey", "Grey throughout, with nothing of the original colour left to read."],
|
|
321
|
+
["greying_black", "Greying Black", "Black still dominant, grey coming in."],
|
|
322
|
+
["greying_brown", "Greying Brown", "Brown still dominant, grey coming in."],
|
|
323
|
+
["red", "Red", "Ginger or copper, the red unmixed with brown."],
|
|
324
|
+
["silver", "Silver", "Grey with a bright metallic sheen rather than a dull cast."],
|
|
325
|
+
[
|
|
326
|
+
"strawberry_blond",
|
|
327
|
+
"Strawberry Blond",
|
|
328
|
+
"Pale red-gold: blonde with a red cast, lighter than red.",
|
|
329
|
+
],
|
|
330
|
+
["white", "White", "Wholly white, whether from age or from carrying no pigment."],
|
|
331
|
+
]),
|
|
332
|
+
"appearance.skin_color": lookWords([
|
|
333
|
+
[
|
|
334
|
+
"pale",
|
|
335
|
+
"Pale",
|
|
336
|
+
"The lightest skin, which burns rather than browns; also skin carrying no pigment.",
|
|
337
|
+
],
|
|
338
|
+
["fair", "Fair", "Light with a pink cast, burning before it browns."],
|
|
339
|
+
["light", "Light", "Light and neutral, browning without burning readily."],
|
|
340
|
+
["medium", "Medium", "Neither light nor dark, with no strong cast either way."],
|
|
341
|
+
["olive", "Olive", "Medium with a green-grey cast."],
|
|
342
|
+
["tawny", "Tawny", "Light brown, warm rather than olive."],
|
|
343
|
+
["golden", "Golden", "Light to medium with a yellow cast."],
|
|
344
|
+
["tanned", "Tanned", "Medium, darkened by sun rather than by birth."],
|
|
345
|
+
["swarthy", "Swarthy", "Dark with an olive or sallow cast, between olive and brown."],
|
|
346
|
+
["brown", "Brown", "Mid-brown."],
|
|
347
|
+
[
|
|
348
|
+
"dark",
|
|
349
|
+
"Dark",
|
|
350
|
+
"Darker than brown with no particular cast; the general word where no finer shade is stated.",
|
|
351
|
+
],
|
|
352
|
+
["dark_brown", "Dark Brown", "Deep brown."],
|
|
353
|
+
["ebony", "Ebony", "The darkest skin, near-black."],
|
|
354
|
+
]),
|
|
355
|
+
"appearance.complexion": lookWords([
|
|
356
|
+
["ashen", "Ashen", "Grey and drained; the skin of serious illness, shock or terror."],
|
|
357
|
+
["blotchy", "Blotchy", "Uneven in colour, patched red or pale."],
|
|
358
|
+
["chapped", "Chapped", "Reddened and cracked by cold and wind."],
|
|
359
|
+
["clear", "Clear", "Free of blemish, mark or broken vessel."],
|
|
360
|
+
["freckled", "Freckled", "Freckled, in patches or across the whole face."],
|
|
361
|
+
["jaundiced", "Jaundiced", "Yellowed, the colour a failing liver gives."],
|
|
362
|
+
["leathery", "Leathery", "Thickened and toughened by years of exposure."],
|
|
363
|
+
["oily", "Oily", "Carrying a greasy sheen."],
|
|
364
|
+
["pasty", "Pasty", "Pale and doughy; the skin of indoor work and poor feeding."],
|
|
365
|
+
["pimpled", "Pimpled", "Presently broken out, with pimples at brow, cheek or jaw."],
|
|
366
|
+
["pockmarked", "Pockmarked", "Pitted by pox, or by deep pimples long since healed."],
|
|
367
|
+
["rough", "Rough", "Coarse in texture, without the wear of outdoor work."],
|
|
368
|
+
["ruddy", "Ruddy", "High colour at cheek and nose, from weather, drink or constitution."],
|
|
369
|
+
["sallow", "Sallow", "Yellowish and bloodless."],
|
|
370
|
+
[
|
|
371
|
+
"scabrous",
|
|
372
|
+
"Scabrous",
|
|
373
|
+
"Rough and scaling, the skin of a lasting complaint such as eczema.",
|
|
374
|
+
],
|
|
375
|
+
[
|
|
376
|
+
"scarred",
|
|
377
|
+
"Scarred",
|
|
378
|
+
"Marked by healed wounds. `extra_features` says where and from what.",
|
|
379
|
+
],
|
|
380
|
+
["smooth", "Smooth", "Even in texture, whatever marks it carries."],
|
|
381
|
+
["sun_kissed", "Sun-Kissed", "Browned by sun on skin not naturally that dark."],
|
|
382
|
+
["sunburnt", "Sunburnt", "Presently burned, red and peeling."],
|
|
383
|
+
["translucent", "Translucent", "So pale the vessels show through."],
|
|
384
|
+
[
|
|
385
|
+
"vitiligo",
|
|
386
|
+
"Vitiligo",
|
|
387
|
+
"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.",
|
|
388
|
+
],
|
|
389
|
+
["wan", "Wan", "Pale and drained; the skin of exhaustion or convalescence."],
|
|
390
|
+
["weathered", "Weathered", "Worn and lined by sun and wind; the skin of outdoor work."],
|
|
391
|
+
["wrinkled", "Wrinkled", "Deeply lined by age."],
|
|
392
|
+
]),
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
/** The values of one field of {@link BEING_LOOK_VOCABULARY}. */
|
|
396
|
+
function lookValues(field) {
|
|
397
|
+
return Object.freeze(BEING_LOOK_VOCABULARY[field].map(({ value }) => value));
|
|
398
|
+
}
|
|
399
|
+
|
|
244
400
|
/** The words a being's `gender` takes. */
|
|
245
|
-
const BEING_GENDERS =
|
|
401
|
+
const BEING_GENDERS = lookValues("gender");
|
|
246
402
|
|
|
247
403
|
/** The words a being's `frame` takes, lightest to heaviest. */
|
|
248
|
-
const BEING_FRAMES =
|
|
404
|
+
const BEING_FRAMES = lookValues("frame");
|
|
249
405
|
|
|
250
406
|
/** Words as code spans, joined by commas and a final "or". */
|
|
251
407
|
function wordList(words) {
|
|
@@ -253,6 +409,26 @@ function wordList(words) {
|
|
|
253
409
|
return `${spans.slice(0, -1).join(", ")} or ${spans.at(-1)}`;
|
|
254
410
|
}
|
|
255
411
|
|
|
412
|
+
/**
|
|
413
|
+
* A string field closed to one list of {@link BEING_LOOK_VOCABULARY}, its words
|
|
414
|
+
* named in the reference and their meanings linked.
|
|
415
|
+
*
|
|
416
|
+
* @param {string} name - The field, as declared.
|
|
417
|
+
* @param {string} what - What the field states, without a closing stop.
|
|
418
|
+
* @returns {object} The declaration.
|
|
419
|
+
*/
|
|
420
|
+
function lookField(name, what) {
|
|
421
|
+
const allowed = lookValues(name);
|
|
422
|
+
return {
|
|
423
|
+
name,
|
|
424
|
+
...TEXT,
|
|
425
|
+
allowed,
|
|
426
|
+
describe:
|
|
427
|
+
`${what}: one of ${wordList(allowed)}. ` +
|
|
428
|
+
"See [a being's look](format-details.md#a-beings-look) for what each word means.",
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
256
432
|
/** A list. */
|
|
257
433
|
const LIST = Object.freeze({ shape: "list", kind: "list" });
|
|
258
434
|
|
|
@@ -643,17 +819,23 @@ const CHARGES = Object.freeze([
|
|
|
643
819
|
/**
|
|
644
820
|
* Everything `place` asks of a whole note.
|
|
645
821
|
*
|
|
646
|
-
*
|
|
647
|
-
* inhabited place,
|
|
648
|
-
*
|
|
649
|
-
*
|
|
822
|
+
* Three questions about one subject, and they are independent: who governs
|
|
823
|
+
* this inhabited place, what the body states about itself where the note is a
|
|
824
|
+
* body rather than somewhere within one, and whether a region's settlements
|
|
825
|
+
* table agrees with its holding model and the notes it links. A type declares
|
|
826
|
+
* one whole-note check, so the three are composed here rather than any being
|
|
827
|
+
* folded into another.
|
|
650
828
|
*
|
|
651
829
|
* @param {object} note - The note.
|
|
652
830
|
* @param {object} [opts] - The lint's options, passed through unchanged.
|
|
653
|
-
* @returns {object[]} Findings from
|
|
831
|
+
* @returns {object[]} Findings from all three.
|
|
654
832
|
*/
|
|
655
833
|
function checkPlace(note, opts) {
|
|
656
|
-
return [
|
|
834
|
+
return [
|
|
835
|
+
...checkGovernment(note),
|
|
836
|
+
...checkWorldFacts(note, opts),
|
|
837
|
+
...checkSettlementTables(note, opts),
|
|
838
|
+
];
|
|
657
839
|
}
|
|
658
840
|
|
|
659
841
|
/** Validate a being's authored date using the shared note-date grammar. */
|
|
@@ -1036,7 +1218,8 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1036
1218
|
describe:
|
|
1037
1219
|
`One of ${wordList(BEING_GENDERS)}. ` +
|
|
1038
1220
|
"`none` says the being has no gender; an absent field says its " +
|
|
1039
|
-
"gender is unrecorded."
|
|
1221
|
+
"gender is unrecorded. See [a being's look](format-details.md#a-beings-look) " +
|
|
1222
|
+
"for what each word means.",
|
|
1040
1223
|
},
|
|
1041
1224
|
{
|
|
1042
1225
|
name: "species",
|
|
@@ -1106,22 +1289,31 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1106
1289
|
name: "frame",
|
|
1107
1290
|
...TEXT,
|
|
1108
1291
|
allowed: BEING_FRAMES,
|
|
1109
|
-
describe:
|
|
1292
|
+
describe:
|
|
1293
|
+
`Relative frame — one of ${wordList(BEING_FRAMES)}. ` +
|
|
1294
|
+
"See [a being's look](format-details.md#a-beings-look) for what each word means.",
|
|
1110
1295
|
},
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1296
|
+
lookField("appearance.eye_color", "Eye colour"),
|
|
1297
|
+
lookField("appearance.hair_color", "Hair colour"),
|
|
1298
|
+
lookField("appearance.skin_color", "Skin colour"),
|
|
1114
1299
|
{
|
|
1115
|
-
|
|
1300
|
+
...lookField("appearance.complexion", "The skin's condition"),
|
|
1116
1301
|
...TEXT_OR_LIST,
|
|
1117
1302
|
describe:
|
|
1118
|
-
|
|
1119
|
-
"
|
|
1303
|
+
`The skin's condition: ${wordList(lookValues("appearance.complexion"))}, ` +
|
|
1304
|
+
"one word or a list of several, because a face carries more than one at " +
|
|
1305
|
+
"once. A single word means a list of one. See " +
|
|
1306
|
+
"[a being's look](format-details.md#a-beings-look) for what each word means.",
|
|
1307
|
+
entries: Object.freeze({
|
|
1308
|
+
kind: "string",
|
|
1309
|
+
shape: "a string",
|
|
1310
|
+
allowed: lookValues("appearance.complexion"),
|
|
1311
|
+
}),
|
|
1120
1312
|
},
|
|
1121
1313
|
{
|
|
1122
1314
|
name: "appearance.extra_features",
|
|
1123
1315
|
...TEXT_LIST,
|
|
1124
|
-
describe: "Anything else a stranger would notice.",
|
|
1316
|
+
describe: "Anything else a stranger would notice, in free text.",
|
|
1125
1317
|
},
|
|
1126
1318
|
]),
|
|
1127
1319
|
}),
|