@heroiclands/package-build 22.9.2 → 22.11.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.
Files changed (97) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/CONTENT.md +54 -0
  3. package/bin/content-build.mjs +3 -1
  4. package/docs/api.md +11 -6
  5. package/docs/commands.md +1 -1
  6. package/docs/content-format.md +411 -201
  7. package/engine/actor-compiler.mjs +20 -7
  8. package/engine/address-fields.mjs +426 -0
  9. package/engine/address-values.mjs +80 -0
  10. package/engine/address.mjs +769 -0
  11. package/engine/art-fields.mjs +112 -97
  12. package/engine/art-slots.mjs +81 -0
  13. package/engine/base-compiler.mjs +61 -7
  14. package/engine/bundle-notes.mjs +5 -2
  15. package/engine/bundles.mjs +17 -10
  16. package/engine/content-address.mjs +23 -186
  17. package/engine/content-embeds.mjs +18 -4
  18. package/engine/content-format-check.mjs +50 -22
  19. package/engine/content-index.mjs +85 -43
  20. package/engine/content-links.mjs +105 -104
  21. package/engine/content-lint.mjs +11 -1
  22. package/engine/content-tables.mjs +9 -1
  23. package/engine/field-spec.mjs +21 -4
  24. package/engine/folder-notes.mjs +44 -15
  25. package/engine/foundry-entries.mjs +3 -0
  26. package/engine/frontmatter-lint.mjs +150 -47
  27. package/engine/frontmatter.mjs +55 -10
  28. package/engine/generate.mjs +7 -1
  29. package/engine/helpers.mjs +37 -9
  30. package/engine/holdings.mjs +41 -12
  31. package/engine/index-records.mjs +2 -0
  32. package/engine/infobox-render.mjs +2 -1
  33. package/engine/infobox.mjs +26 -6
  34. package/engine/item-compiler.mjs +5 -0
  35. package/engine/item-docs.mjs +7 -44
  36. package/engine/map-dot.mjs +2 -0
  37. package/engine/map-notes.mjs +7 -2
  38. package/engine/map-places.mjs +42 -10
  39. package/engine/metadata-index.mjs +60 -5
  40. package/engine/note-addresses.mjs +325 -0
  41. package/engine/note-vocabulary.mjs +140 -28
  42. package/engine/pdf-build.mjs +1 -1
  43. package/engine/pdf-toc.mjs +17 -2
  44. package/engine/place-relations.mjs +160 -50
  45. package/engine/populations.mjs +17 -9
  46. package/engine/reference-targets.mjs +54 -0
  47. package/engine/scenes.mjs +5 -2
  48. package/engine/shortcode-references.mjs +68 -0
  49. package/engine/site-build.mjs +39 -15
  50. package/engine/site-index.mjs +57 -75
  51. package/engine/sql-tables.mjs +43 -9
  52. package/engine/system-block.mjs +21 -0
  53. package/engine/system-vocabulary.mjs +46 -0
  54. package/engine/web-wikilinks.mjs +11 -9
  55. package/engine/wikilink-syntax.mjs +21 -0
  56. package/engine/wikilinks.mjs +92 -306
  57. package/hm3/actors.mjs +8 -24
  58. package/hm3/item-fields.mjs +15 -5
  59. package/package.json +1 -1
  60. package/sohl/infobox.mjs +111 -17
  61. package/sohl/item-fields.mjs +131 -41
  62. package/types/engine/address-fields.d.mts +95 -0
  63. package/types/engine/address-values.d.mts +34 -0
  64. package/types/engine/address.d.mts +452 -0
  65. package/types/engine/art-fields.d.mts +59 -53
  66. package/types/engine/art-slots.d.mts +40 -0
  67. package/types/engine/base-compiler.d.mts +24 -2
  68. package/types/engine/bundle-notes.d.mts +2 -2
  69. package/types/engine/content-address.d.mts +3 -139
  70. package/types/engine/content-embeds.d.mts +1 -1
  71. package/types/engine/content-format-check.d.mts +25 -0
  72. package/types/engine/content-index.d.mts +2 -2
  73. package/types/engine/content-links.d.mts +6 -5
  74. package/types/engine/field-spec.d.mts +46 -9
  75. package/types/engine/folder-notes.d.mts +11 -10
  76. package/types/engine/frontmatter-lint.d.mts +7 -97
  77. package/types/engine/frontmatter.d.mts +16 -1
  78. package/types/engine/helpers.d.mts +2 -2
  79. package/types/engine/infobox.d.mts +6 -0
  80. package/types/engine/item-docs.d.mts +1 -35
  81. package/types/engine/map-notes.d.mts +2 -2
  82. package/types/engine/note-addresses.d.mts +41 -0
  83. package/types/engine/note-vocabulary.d.mts +20 -3
  84. package/types/engine/place-relations.d.mts +12 -1
  85. package/types/engine/populations.d.mts +17 -2
  86. package/types/engine/reference-targets.d.mts +9 -0
  87. package/types/engine/shortcode-references.d.mts +14 -0
  88. package/types/engine/site-build.d.mts +3 -3
  89. package/types/engine/site-index.d.mts +9 -30
  90. package/types/engine/sql-tables.d.mts +2 -2
  91. package/types/engine/system-block.d.mts +17 -0
  92. package/types/engine/system-vocabulary.d.mts +16 -0
  93. package/types/engine/wikilink-syntax.d.mts +7 -0
  94. package/types/engine/wikilinks.d.mts +6 -109
  95. package/types/hm3/item-fields.d.mts +5 -0
  96. package/types/sohl/infobox.d.mts +6 -5
  97. package/types/sohl/item-fields.d.mts +8 -0
package/sohl/infobox.mjs CHANGED
@@ -51,6 +51,8 @@
51
51
  * @module
52
52
  */
53
53
 
54
+ import { parseAddress, isAddressTuple, completeAddress } from "../engine/address.mjs";
55
+ import { contentPackage } from "../engine/content-package.mjs";
54
56
  import {
55
57
  DURATION_LABELS,
56
58
  GEAR_UNITS,
@@ -63,6 +65,8 @@ import {
63
65
  import { currentType } from "../engine/ids.mjs";
64
66
  import { subTypes } from "../engine/note-vocabulary.mjs";
65
67
  import { systemBlock, systemData } from "../engine/system-block.mjs";
68
+ import { readCanonicalKey } from "../engine/content-address.mjs";
69
+ import { readQualifier } from "../engine/address.mjs";
66
70
  import { NOTE_SCHEMAS } from "./note-schemas.mjs";
67
71
  import { GEAR_TYPE_TO_KEY } from "./being-info.mjs";
68
72
 
@@ -174,15 +178,45 @@ function isMapping(value) {
174
178
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
175
179
  }
176
180
 
181
+ /**
182
+ * Every content type `model:` may name — the note vocabulary this system
183
+ * declares, engine types included.
184
+ *
185
+ * @type {ReadonlySet<string>}
186
+ */
187
+ const MODEL_TYPES = new Set(Object.keys(NOTE_SCHEMAS));
188
+
189
+ /**
190
+ * The `(type, shortcode)` an entry's `model:` names, or `undefined`.
191
+ *
192
+ * `model` is an Address, read by the same grammar every wikilink is —
193
+ * `type-shortcode` within this package, `package-system-type-shortcode` to
194
+ * name a dependency's catalogue. This decode answers neither of the two
195
+ * questions {@link module:engine/address} asks about the package: a runtime
196
+ * lookup among one actor's embedded items resolves by `(type, shortcode)`
197
+ * alone, where packages do not exist, so a fully qualified `model` is read for
198
+ * its last two segments exactly as a bare one is.
199
+ *
200
+ * @param {unknown} model - The authored value.
201
+ * @returns {{type: string, shortcode: string}|undefined} What it names.
202
+ */
203
+ function modelAddress(model) {
204
+ const target = parseAddress(
205
+ model,
206
+ { package: contentPackage(), system: "sohl", types: MODEL_TYPES },
207
+ { declared: true, legacyShortcodeCase: true },
208
+ );
209
+ return target.reason ? undefined : target;
210
+ }
211
+
177
212
  /**
178
213
  * What one entry of `sohl.items` names.
179
214
  *
180
215
  * An entry addresses its item three ways and every tree uses all three: a
181
- * `model` address whose last two hyphen-separated segments are always
182
- * `<type>-<shortcode>`, explicit `type` / `shortcode` keys, or a locally
183
- * authored item stating its type with the shortcode inside its own `system`
184
- * block. One decode covers all of them, so nothing downstream has to know
185
- * which form a note happened to use.
216
+ * `model` address, explicit `type` / `shortcode` keys, or a locally authored
217
+ * item stating its type with the shortcode inside its own `system` block. One
218
+ * decode covers all of them, so nothing downstream has to know which form a
219
+ * note happened to use.
186
220
  *
187
221
  * @param {object} entry - One `sohl.items` entry.
188
222
  * @returns {{type: string, shortcode: string, name: string, system: object}|undefined}
@@ -190,19 +224,21 @@ function isMapping(value) {
190
224
  */
191
225
  export function decodeItem(entry) {
192
226
  if (!isMapping(entry)) return undefined;
227
+ const address = entry.model ? modelAddress(entry.model) : undefined;
193
228
  let type = typeof entry.type === "string" ? entry.type : "";
194
229
  let shortcode = typeof entry.shortcode === "string" ? entry.shortcode : "";
195
- if (!type && typeof entry.model === "string") {
196
- const segments = entry.model.split("-");
197
- if (segments.length >= 2) {
198
- type = segments[segments.length - 2];
199
- if (!shortcode) shortcode = segments[segments.length - 1];
230
+ if (!type) {
231
+ const named = address;
232
+ if (named) {
233
+ type = named.type;
234
+ if (!shortcode) shortcode = named.shortcode;
200
235
  }
201
236
  }
202
237
  const system = isMapping(entry.system) ? entry.system : {};
203
238
  if (!shortcode && typeof system.shortcode === "string") shortcode = system.shortcode;
204
239
  if (!type) return undefined;
205
240
  return {
241
+ ...(address ? { address } : {}),
206
242
  type: currentType(type),
207
243
  shortcode,
208
244
  name: typeof entry.name === "string" ? entry.name : "",
@@ -222,6 +258,32 @@ function itemsOf(fm, block) {
222
258
  return (Array.isArray(declared) ? declared : []).map(decodeItem).filter(Boolean);
223
259
  }
224
260
 
261
+ /**
262
+ * A model's documentation presentation, with its native name as plain text
263
+ * when no documentation is published. The model keeps its native identity.
264
+ * @param {object} item - Decoded native item.
265
+ * @param {Function} resolve - The medium's reference resolver.
266
+ * @param {string} block - The native system.
267
+ * @returns {object|undefined} Documentation links and display metadata.
268
+ */
269
+ function itemPresentation(item, resolve, block) {
270
+ if (!item.shortcode) return undefined;
271
+ if (!item.address)
272
+ return resolve?.(item.shortcode, { kind: "shortcode", type: item.type, system: block });
273
+ const documentation = completeAddress({ ...item.address, system: "none" });
274
+ const found = resolve?.(documentation, { type: documentation.type });
275
+ const native =
276
+ !found?.name || !found?.subType ? resolve?.(item.address, { type: item.type }) : undefined;
277
+ if (!found && !native) return undefined;
278
+ return {
279
+ name: found?.name ?? native?.name,
280
+ subType: found?.subType ?? native?.subType,
281
+ address: documentation,
282
+ ...(found?.url ? { url: found.url } : {}),
283
+ ...(found?.uuid ? { uuid: found.uuid } : {}),
284
+ };
285
+ }
286
+
225
287
  /**
226
288
  * One item as a value a renderer can draw: its name, and a link where the
227
289
  * index reached it.
@@ -229,10 +291,11 @@ function itemsOf(fm, block) {
229
291
  * @param {object} item - A decoded item.
230
292
  * @param {(ref: unknown, hint?: object) => object|undefined} resolve - The
231
293
  * medium's resolver.
232
- * @returns {{text: string, url?: string, uuid?: string, address?: string}} The value.
294
+ * @param {string} block - The native system.
295
+ * @returns {{text: string, url?: string, uuid?: string, address?: import("../engine/address.mjs").AddressTuple}} The value.
233
296
  */
234
- function itemValue(item, resolve) {
235
- const found = item.shortcode ? resolve?.(item.shortcode, { type: item.type }) : undefined;
297
+ function itemValue(item, resolve, block) {
298
+ const found = itemPresentation(item, resolve, block);
236
299
  const value = { text: item.name || found?.name || humanizeValue(item.shortcode) };
237
300
  if (found?.url) value.url = found.url;
238
301
  if (found?.uuid) value.uuid = found.uuid;
@@ -277,9 +340,9 @@ export function beingSections(fm, { block, resolve }) {
277
340
  if (item.type !== "skill") continue;
278
341
  const mastery = item.system.masteryLevelBase;
279
342
  if (!hasValue(mastery)) continue;
280
- const found = item.shortcode ? resolve?.(item.shortcode, { type: "skill" }) : undefined;
343
+ const found = itemPresentation(item, resolve, block);
281
344
  const family = item.system.subType ?? found?.subType;
282
- const value = itemValue(item, resolve);
345
+ const value = itemValue(item, resolve, block);
283
346
  const entry = { ...value, text: `${value.text} ${mastery}` };
284
347
  if (family && families.has(family)) families.get(family).push(entry);
285
348
  else unplaced.push(entry);
@@ -298,7 +361,7 @@ export function beingSections(fm, { block, resolve }) {
298
361
  const mystical = [];
299
362
  for (const item of items) {
300
363
  if (item.type !== "mysticalability") continue;
301
- mystical.push(itemValue(item, resolve));
364
+ mystical.push(itemValue(item, resolve, block));
302
365
  }
303
366
  if (mystical.length) {
304
367
  sections.push({
@@ -313,7 +376,7 @@ export function beingSections(fm, { block, resolve }) {
313
376
  for (const item of items) {
314
377
  const key = GEAR_TYPE_TO_KEY[item.type];
315
378
  if (!key) continue;
316
- gear.get(key).push(itemValue(item, resolve));
379
+ gear.get(key).push(itemValue(item, resolve, block));
317
380
  }
318
381
  const gearGroups = [];
319
382
  for (const [key, entries] of gear) {
@@ -480,6 +543,36 @@ function genericSections(fm, type, ctx) {
480
543
  return systemRowsSection(fm, NOTE_SCHEMAS[type], ctx);
481
544
  }
482
545
 
546
+ /** An affiliation presents skill documentation while retaining native Item identity. */
547
+ export function affiliationSections(fm, ctx) {
548
+ const field = NOTE_SCHEMAS.affiliation.find((entry) => entry.to === "commonSkills");
549
+ const sections = systemRowsSection(
550
+ fm,
551
+ NOTE_SCHEMAS.affiliation.filter((entry) => entry !== field),
552
+ ctx,
553
+ );
554
+ const { value } = ctx.resolveField(field, fm, { block: ctx.block });
555
+ if (!Array.isArray(value) || value.length === 0) return sections;
556
+ const entries = value.map((skill) => {
557
+ const native = ctx.resolve?.(skill, { type: "skill", system: "sohl" });
558
+ const docAddress =
559
+ isAddressTuple(skill) ? completeAddress({ ...skill, system: "none" }) : undefined;
560
+ const doc = docAddress ? ctx.resolve?.(docAddress, { type: docAddress.type }) : undefined;
561
+ return {
562
+ text: doc?.name || native?.name || humanizeValue(skill),
563
+ ...(doc?.url ? { url: doc.url } : {}),
564
+ ...(doc?.uuid ? { uuid: doc.uuid } : {}),
565
+ ...(docAddress && (doc?.url || doc?.uuid) ? { address: docAddress } : {}),
566
+ };
567
+ });
568
+ sections.push({
569
+ id: "commonskills",
570
+ layout: "rows",
571
+ rows: [{ label: "Common skills", kind: "links", value: entries }],
572
+ });
573
+ return sections;
574
+ }
575
+
483
576
  /**
484
577
  * SoHL's infobox declaration.
485
578
  *
@@ -492,6 +585,7 @@ export const SOHL_INFOBOX = defineInfobox({
492
585
  presentation: SOHL_FIELD_PRESENTATION,
493
586
  sections: {
494
587
  being: beingSections,
588
+ affiliation: affiliationSections,
495
589
  armorgear: armorSections,
496
590
  weapongear: weaponSections,
497
591
  projectilegear: projectileSections,
@@ -35,6 +35,7 @@
35
35
  * @module
36
36
  */
37
37
 
38
+ import { isAddressTuple } from "../engine/address.mjs";
38
39
  import {
39
40
  AS_AUTHORED,
40
41
  BLANK_IS_DEFAULT,
@@ -143,14 +144,56 @@ const SKILL_APTITUDES = Object.freeze({
143
144
  read: (_raw, { fm }) => resolveSkillAptitudes(fm, noteContext(fm, "mystery")),
144
145
  });
145
146
 
147
+ /* --------------------------------------------------------------------- */
148
+ /* The Address positions */
149
+ /* --------------------------------------------------------------------- */
150
+
151
+ /*
152
+ * An affiliation names other notes in four of its fields, and each of those
153
+ * values is an **Address** — written at whatever length says what it means —
154
+ * while the compiled Item holds the **Shortcode** the runtime resolves among
155
+ * one actor's items. `engine/address-fields.mjs` is the step between them, and
156
+ * these are the positions it reads.
157
+ *
158
+ * Each states two things, and they are not the same statement: `type` is the
159
+ * default an omitted `<type>` segment takes, and `accepts` is the set of types
160
+ * the field takes at all. They coincide at all four, which is the common case
161
+ * rather than the rule — an art slot defaults to `icon` and accepts `icon` or
162
+ * `image` — so each is written out rather than derived from the other.
163
+ */
164
+
165
+ /** One place: an affiliation's seat. */
166
+ const PLACE_VALUE = Object.freeze({ type: "place", accepts: Object.freeze(["place"]) });
167
+
168
+ /** A list of places: the ones an affiliation holds sway over. */
169
+ const PLACE_ITEMS = Object.freeze({ ...PLACE_VALUE, holds: "items" });
170
+
171
+ /** A list of affiliations: the ones this affiliation answers to. */
172
+ const AFFILIATION_ITEMS = Object.freeze({
173
+ type: "affiliation",
174
+ accepts: Object.freeze(["affiliation"]),
175
+ holds: "items",
176
+ });
177
+
178
+ /** A map keyed by affiliation: this one's standing toward each. */
179
+ const AFFILIATION_KEYS = Object.freeze({ ...AFFILIATION_ITEMS, holds: "keys" });
180
+
181
+ /** Native skill references retain the published Item UUID. */
182
+ const SKILL_UUID_ITEMS = Object.freeze({
183
+ type: "skill",
184
+ accepts: Object.freeze(["skill"]),
185
+ holds: "items",
186
+ emit: "uuid",
187
+ });
188
+
146
189
  /** Standings toward other affiliations, validated against the closed list. */
147
190
  const RELATION = Object.freeze({
148
- shape: "map of affiliation shortcode → standing",
191
+ shape: "map of affiliation address → standing",
149
192
  read: (_raw, { fm }) => resolveRelation(fm, noteContext(fm, "affiliation")),
150
193
  });
151
194
 
152
195
  /**
153
- * A list of references, each a bare shortcode.
196
+ * A list of references, each an address.
154
197
  *
155
198
  * Blank entries are dropped rather than emitted: a list that has been edited
156
199
  * down in a property editor keeps its empty rows, and an empty string is not a
@@ -158,15 +201,15 @@ const RELATION = Object.freeze({
158
201
  * value here — a sovereign polity is subordinate to nobody — rather than an
159
202
  * unset one.
160
203
  */
161
- const SHORTCODE_LIST = Object.freeze({
162
- shape: "list of shortcodes",
204
+ const ADDRESS_LIST = Object.freeze({
205
+ shape: "list of addresses",
163
206
  kind: "list",
164
207
  read: (raw) =>
165
208
  (Array.isArray(raw) ? raw
166
209
  : raw == null || raw === "" ? []
167
210
  : [raw]
168
211
  )
169
- .map((entry) => String(entry ?? "").trim())
212
+ .map((entry) => (isAddressTuple(entry) ? entry : String(entry ?? "").trim()))
170
213
  .filter((entry) => entry !== ""),
171
214
  });
172
215
 
@@ -237,7 +280,8 @@ const GEAR_COMMON = Object.freeze([
237
280
  describe: "How many the stack holds. A compendium article ships one.",
238
281
  },
239
282
  {
240
- name: "weight",
283
+ name: "data.weight",
284
+ legacyKey: "weight",
241
285
  to: "weightBase",
242
286
  ...AS_AUTHORED,
243
287
  kind: "number",
@@ -245,7 +289,8 @@ const GEAR_COMMON = Object.freeze([
245
289
  describe: "Weight of one, in pounds.",
246
290
  },
247
291
  {
248
- name: "value",
292
+ name: "data.value",
293
+ legacyKey: "value",
249
294
  to: "valueBase",
250
295
  ...AS_AUTHORED,
251
296
  kind: "number",
@@ -253,7 +298,8 @@ const GEAR_COMMON = Object.freeze([
253
298
  describe: "Worth of one, in pence.",
254
299
  },
255
300
  {
256
- name: "quality",
301
+ name: "data.quality",
302
+ legacyKey: "quality",
257
303
  to: "qualityBase",
258
304
  ...AS_AUTHORED,
259
305
  kind: "number",
@@ -261,7 +307,8 @@ const GEAR_COMMON = Object.freeze([
261
307
  describe: "Craftsmanship, as a modifier to what the article does.",
262
308
  },
263
309
  {
264
- name: "durability",
310
+ name: "data.durability",
311
+ legacyKey: "durability",
265
312
  to: "durabilityBase",
266
313
  ...AS_AUTHORED,
267
314
  kind: "number",
@@ -293,6 +340,14 @@ const GEAR_COMMON = Object.freeze([
293
340
  * Every item type's frontmatter vocabulary, in the order the `system` block
294
341
  * emits it.
295
342
  *
343
+ * **A field whose shared source is a path into `data:` names both positions.**
344
+ * `name` is the source the content format's mapping tables state — `data.seat`,
345
+ * `data.weight` — and `legacyKey` is the key the system block still carries, which
346
+ * wins while a tree writes it. Naming only the source would move the in-block
347
+ * read to a key spelled `data.seat`, which nothing authors; naming only the key
348
+ * leaves the container the specification points at unread, and the field then
349
+ * compiles its default over an authored value with nothing said.
350
+ *
296
351
  * @type {Readonly<Record<string, readonly import("../engine/field-spec.mjs").FieldSpec[]>>}
297
352
  */
298
353
  export const ITEM_FIELDS = Object.freeze({
@@ -338,12 +393,23 @@ export const ITEM_FIELDS = Object.freeze({
338
393
  default: 0,
339
394
  describe: "Standing within the society.",
340
395
  },
396
+ {
397
+ name: "commonSkills",
398
+ to: "commonSkills",
399
+ ...ADDRESS_LIST,
400
+ address: SKILL_UUID_ITEMS,
401
+ default: [],
402
+ topLevelMeans: "Common skills belong to the SoHL affiliation item.",
403
+ describe: "Native SoHL skill Items common among members, by address.",
404
+ },
341
405
  {
342
406
  // Plural because the field holds a map of many standings, one per
343
407
  // affiliation — the singular reads as a misnomer.
344
- name: "relations",
408
+ name: "data.relations",
409
+ legacyKey: "relations",
345
410
  to: "relations",
346
411
  ...RELATION,
412
+ address: AFFILIATION_KEYS,
347
413
  default: {},
348
414
  describe: "How this society regards others: aligned, unaligned, rival or nemesis.",
349
415
  },
@@ -351,21 +417,25 @@ export const ITEM_FIELDS = Object.freeze({
351
417
  // The organisational relation: which bodies this one answers to.
352
418
  // A list, because an affiliation may sit under more than one at
353
419
  // once — an arcane tradition within an order, say.
354
- name: "parents",
420
+ name: "data.parents",
421
+ legacyKey: "parents",
355
422
  to: "parents",
356
- ...SHORTCODE_LIST,
423
+ ...ADDRESS_LIST,
424
+ address: AFFILIATION_ITEMS,
357
425
  default: [],
358
- describe: "Affiliations this one is subordinate to, by shortcode.",
426
+ describe: "Affiliations this one is subordinate to, by address.",
359
427
  },
360
428
  {
361
429
  // Not `capital` or `headquarters`: each fits about half the eleven
362
430
  // subTypes, while a seat covers a polity, a guild, an order and a
363
431
  // faith alike.
364
- name: "seat",
432
+ name: "data.seat",
433
+ legacyKey: "seat",
365
434
  to: "seat",
366
435
  ...BLANK_IS_NULL,
436
+ address: PLACE_VALUE,
367
437
  default: null,
368
- describe: "Where the affiliation's authority sits, by place shortcode.",
438
+ describe: "Where the affiliation's authority sits, by place address.",
369
439
  },
370
440
  {
371
441
  // The geographic relation, kept apart from the organisational one
@@ -375,11 +445,13 @@ export const ITEM_FIELDS = Object.freeze({
375
445
  // mapping row states (`data.domains` → `system.domain`). The two
376
446
  // spellings are deliberate rather than a slip, so the declaration
377
447
  // carries both rather than either side guessing.
378
- name: "domains",
448
+ name: "data.domains",
449
+ legacyKey: "domains",
379
450
  to: "domain",
380
- ...SHORTCODE_LIST,
451
+ ...ADDRESS_LIST,
452
+ address: PLACE_ITEMS,
381
453
  default: [],
382
- describe: "Places this affiliation holds sway over, by shortcode.",
454
+ describe: "Places this affiliation holds sway over, by address.",
383
455
  },
384
456
  ]),
385
457
 
@@ -418,14 +490,16 @@ export const ITEM_FIELDS = Object.freeze({
418
490
  describe: "How readily the host throws it off.",
419
491
  },
420
492
  {
421
- name: "contagionIndex",
493
+ name: "data.contagionIndex",
494
+ legacyKey: "contagionIndex",
422
495
  to: "contagionIndexBase",
423
496
  ...NUMBER,
424
497
  default: 0,
425
498
  describe: "How readily it passes to someone else.",
426
499
  },
427
500
  {
428
- name: "transmission",
501
+ name: "data.transmission",
502
+ legacyKey: "transmission",
429
503
  to: "transmission",
430
504
  ...AS_AUTHORED,
431
505
  default: "none",
@@ -440,7 +514,8 @@ export const ITEM_FIELDS = Object.freeze({
440
514
  "Days from contracting to onset, rolled by the receiving actor. Unset means no incubation.",
441
515
  },
442
516
  {
443
- name: "outcome",
517
+ name: "data.outcome",
518
+ legacyKey: "outcome",
444
519
  to: "outcome",
445
520
  ...BLANK_IS_DEFAULT,
446
521
  default: "cured",
@@ -464,7 +539,8 @@ export const ITEM_FIELDS = Object.freeze({
464
539
  // cannot be grepped, and each phase's prose differs anyway — which is
465
540
  // the whole content of the declaration.
466
541
  {
467
- name: "onsetDurationFormula",
542
+ name: "data.onsetDurationFormula",
543
+ legacyKey: "onsetDurationFormula",
468
544
  to: "onsetDurationFormula",
469
545
  ...DURATION_FORMULA,
470
546
  omitWhenAbsent: true,
@@ -472,7 +548,8 @@ export const ITEM_FIELDS = Object.freeze({
472
548
  "Interval from contracting the affliction to the start of onset. Omitted when unset, leaving no incubation.",
473
549
  },
474
550
  {
475
- name: "onsetDurationBase",
551
+ name: "data.onsetDurationBase",
552
+ legacyKey: "onsetDurationBase",
476
553
  to: "onsetDurationBase",
477
554
  ...DURATION_BASE,
478
555
  omitWhenAbsent: true,
@@ -480,7 +557,8 @@ export const ITEM_FIELDS = Object.freeze({
480
557
  "The onset interval in seconds, standing in for a roll of the formula. Omitted when unset.",
481
558
  },
482
559
  {
483
- name: "healingCheckDurationFormula",
560
+ name: "data.healingCheckDurationFormula",
561
+ legacyKey: "healingCheckDurationFormula",
484
562
  to: "healingCheckDurationFormula",
485
563
  ...DURATION_FORMULA,
486
564
  omitWhenAbsent: true,
@@ -488,7 +566,8 @@ export const ITEM_FIELDS = Object.freeze({
488
566
  "Interval between healing checks, once the affliction is symptomatic. Omitted when unset.",
489
567
  },
490
568
  {
491
- name: "healingCheckDurationBase",
569
+ name: "data.healingCheckDurationBase",
570
+ legacyKey: "healingCheckDurationBase",
492
571
  to: "healingCheckDurationBase",
493
572
  ...DURATION_BASE,
494
573
  omitWhenAbsent: true,
@@ -496,7 +575,8 @@ export const ITEM_FIELDS = Object.freeze({
496
575
  "The healing-check interval in seconds, standing in for a roll of the formula. Omitted when unset.",
497
576
  },
498
577
  {
499
- name: "resolutionDurationFormula",
578
+ name: "data.resolutionDurationFormula",
579
+ legacyKey: "resolutionDurationFormula",
500
580
  to: "resolutionDurationFormula",
501
581
  ...DURATION_FORMULA,
502
582
  omitWhenAbsent: true,
@@ -504,7 +584,8 @@ export const ITEM_FIELDS = Object.freeze({
504
584
  "Interval from onset to the affliction running its course. Omitted when unset.",
505
585
  },
506
586
  {
507
- name: "resolutionDurationBase",
587
+ name: "data.resolutionDurationBase",
588
+ legacyKey: "resolutionDurationBase",
508
589
  to: "resolutionDurationBase",
509
590
  ...DURATION_BASE,
510
591
  omitWhenAbsent: true,
@@ -711,7 +792,7 @@ export const ITEM_FIELDS = Object.freeze({
711
792
  {
712
793
  name: "assocSkillCode",
713
794
  to: "assocSkillCode",
714
- ref: "skill",
795
+ code: "skill",
715
796
  ...BLANK_IS_NULL,
716
797
  default: null,
717
798
  describe: "Shortcode of the skill the mystery is tested against.",
@@ -719,7 +800,7 @@ export const ITEM_FIELDS = Object.freeze({
719
800
  {
720
801
  name: "assocAffiliationCode",
721
802
  to: "assocAffiliationCode",
722
- ref: "affiliation",
803
+ code: "affiliation",
723
804
  ...BLANK_IS_NULL,
724
805
  default: null,
725
806
  describe:
@@ -733,14 +814,16 @@ export const ITEM_FIELDS = Object.freeze({
733
814
  describe: "The mystery's level before any modifier.",
734
815
  },
735
816
  {
736
- name: "skillAptitudes",
817
+ name: "data.skillAptitudes",
818
+ legacyKey: "skillAptitudes",
737
819
  to: "skillAptitudes",
738
820
  ...SKILL_APTITUDES,
739
821
  default: {},
740
822
  describe: "Aptitude the mystery grants, per skill selector.",
741
823
  },
742
824
  {
743
- name: "charges",
825
+ name: "data.charges",
826
+ legacyKey: "charges",
744
827
  to: "charges",
745
828
  ...CHARGES,
746
829
  default: { value: null, max: null },
@@ -760,7 +843,7 @@ export const ITEM_FIELDS = Object.freeze({
760
843
  {
761
844
  name: "assocSkillCode",
762
845
  to: "assocSkillCode",
763
- ref: "skill",
846
+ code: "skill",
764
847
  ...AS_AUTHORED,
765
848
  default: "",
766
849
  describe: "Shortcode of the skill the ability is tested against.",
@@ -768,7 +851,7 @@ export const ITEM_FIELDS = Object.freeze({
768
851
  {
769
852
  name: "assocAffiliationCode",
770
853
  to: "assocAffiliationCode",
771
- ref: "affiliation",
854
+ code: "affiliation",
772
855
  ...BLANK_IS_NULL,
773
856
  default: null,
774
857
  describe:
@@ -796,7 +879,8 @@ export const ITEM_FIELDS = Object.freeze({
796
879
  describe: "The ability's level before any modifier.",
797
880
  },
798
881
  {
799
- name: "charges",
882
+ name: "data.charges",
883
+ legacyKey: "charges",
800
884
  to: "charges",
801
885
  ...CHARGES,
802
886
  default: { value: null, max: null },
@@ -884,7 +968,7 @@ export const ITEM_FIELDS = Object.freeze({
884
968
  {
885
969
  name: "parentSkillCode",
886
970
  to: "parentSkillCode",
887
- ref: "skill",
971
+ code: "skill",
888
972
  ...AS_AUTHORED,
889
973
  default: "",
890
974
  describe: "Shortcode of the skill this one specialises, for a specialisation.",
@@ -959,7 +1043,8 @@ export const ITEM_FIELDS = Object.freeze({
959
1043
  // `bloodLossAdvance*` is the one an author reaches for most: a trauma
960
1044
  // that sets it bleeds, and one that leaves it unset does not.
961
1045
  {
962
- name: "healingCheckDurationFormula",
1046
+ name: "data.healingCheckDurationFormula",
1047
+ legacyKey: "healingCheckDurationFormula",
963
1048
  to: "healingCheckDurationFormula",
964
1049
  ...DURATION_FORMULA,
965
1050
  omitWhenAbsent: true,
@@ -967,7 +1052,8 @@ export const ITEM_FIELDS = Object.freeze({
967
1052
  "Interval between healing checks. Omitted when unset, leaving the world's configured interval to apply.",
968
1053
  },
969
1054
  {
970
- name: "healingCheckDurationBase",
1055
+ name: "data.healingCheckDurationBase",
1056
+ legacyKey: "healingCheckDurationBase",
971
1057
  to: "healingCheckDurationBase",
972
1058
  ...DURATION_BASE,
973
1059
  omitWhenAbsent: true,
@@ -975,7 +1061,8 @@ export const ITEM_FIELDS = Object.freeze({
975
1061
  "The healing-check interval in seconds, standing in for a roll of the formula. Omitted when unset.",
976
1062
  },
977
1063
  {
978
- name: "bloodLossAdvanceDurationFormula",
1064
+ name: "data.bloodLossAdvanceDurationFormula",
1065
+ legacyKey: "bloodLossAdvanceDurationFormula",
979
1066
  to: "bloodLossAdvanceDurationFormula",
980
1067
  ...DURATION_FORMULA,
981
1068
  omitWhenAbsent: true,
@@ -983,7 +1070,8 @@ export const ITEM_FIELDS = Object.freeze({
983
1070
  "Interval between blood-loss advances. Omitted when unset, leaving the world's configured interval to apply.",
984
1071
  },
985
1072
  {
986
- name: "bloodLossAdvanceDurationBase",
1073
+ name: "data.bloodLossAdvanceDurationBase",
1074
+ legacyKey: "bloodLossAdvanceDurationBase",
987
1075
  to: "bloodLossAdvanceDurationBase",
988
1076
  ...DURATION_BASE,
989
1077
  omitWhenAbsent: true,
@@ -991,7 +1079,8 @@ export const ITEM_FIELDS = Object.freeze({
991
1079
  "The blood-loss interval in seconds. Setting it is what makes the wound bleed; omitted when unset, and the wound does not.",
992
1080
  },
993
1081
  {
994
- name: "courseDurationFormula",
1082
+ name: "data.courseDurationFormula",
1083
+ legacyKey: "courseDurationFormula",
995
1084
  to: "courseDurationFormula",
996
1085
  ...DURATION_FORMULA,
997
1086
  omitWhenAbsent: true,
@@ -999,7 +1088,8 @@ export const ITEM_FIELDS = Object.freeze({
999
1088
  "Interval between course tests, for a condition that runs one — shock, coma, infection. Omitted when unset.",
1000
1089
  },
1001
1090
  {
1002
- name: "courseDurationBase",
1091
+ name: "data.courseDurationBase",
1092
+ legacyKey: "courseDurationBase",
1003
1093
  to: "courseDurationBase",
1004
1094
  ...DURATION_BASE,
1005
1095
  omitWhenAbsent: true,