@heroiclands/package-build 22.8.0 → 22.9.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 CHANGED
@@ -1,5 +1,19 @@
1
1
  # @heroiclands/package-build
2
2
 
3
+ ## 22.9.0
4
+
5
+ ### Minor Changes
6
+
7
+ **Calendars are content, not configuration.** The `calendar:` block retires from `package-build.config.yaml`, and a configuration that still declares it is refused by name.
8
+
9
+ **A calendar is a note.** A `lore` note with `subType: calendar` states the months it keeps, the weekdays it names, the seasons it marks and the year-counts kept in it, and a date names one of those by writing `<calendar shortcode>.<era shortcode>`. Days that belong to no month are a short month like any other, sitting in the list where they fall in the year.
10
+
11
+ **The world states its own year.** How long the year is, how the day divides and how the moon moves are written once on the world's own `place` note and on its moon's, so no calendar restates them — and a calendar whose months do not add up to that year is refused, naming the year rather than the arithmetic.
12
+
13
+ **A body is a place.** `place` gains `celestial`, for something observed from a world rather than located on one.
14
+
15
+ **A written day is bounded by the month it names.** A five-day month accepts five days and a thirty-day month accepts thirty, in whatever order a calendar keeps them; a package that names no calendar bounds nothing.
16
+
3
17
  ## 22.8.0
4
18
 
5
19
  ### Minor Changes
@@ -76,7 +76,6 @@ import { EMPTY_ICON_REGISTRY, checkIconRegistry } from "./engine/content-icons.m
76
76
  import { MAP_TYPES, NO_PACK, PACK_BY_TYPE } from "./engine/ids.mjs";
77
77
  import { ACTOR_TYPES } from "./engine/subtype-registry.mjs";
78
78
  import { NOTE_VOCABULARY } from "./engine/note-vocabulary.mjs";
79
- import { UNKNOWN_DATE, parseNoteDate } from "./engine/note-dates.mjs";
80
79
 
81
80
  /**
82
81
  * What kind of package this is.
@@ -726,10 +725,6 @@ export function publishesContentPages(config) {
726
725
  * @property {readonly string[]} packDirectories Derived: every pack directory
727
726
  * the build produces, in compile order —
728
727
  * each pack followed by its companions.
729
- * @property {Readonly<{months: number|null, monthDays: number|null, present: string|null}>} calendar
730
- * How this package's year is divided, and
731
- * the date it calls the present. Every
732
- * value `null` when none is declared.
733
728
  * @property {Readonly<PackageBuildSection>} packageBuild Passed through
734
729
  * frozen, uninterpreted. `{}` when absent.
735
730
  * @property {Readonly<DocsSpec>} docs Frozen; `{}` when absent.
@@ -759,13 +754,11 @@ const CONFIG_KEYS = [
759
754
  "relationships",
760
755
  "systems",
761
756
  "requiresSystem",
762
- "calendar",
763
757
  "packageBuild",
764
758
  "publish",
765
759
  "changelog",
766
760
  ];
767
761
  const SYSTEM_KEYS = ["manifest", "compatibility"];
768
- const CALENDAR_KEYS = ["months", "monthDays", "present"];
769
762
  const COMPATIBILITY_KEYS = ["minimum", "verified"];
770
763
  const DOCS_KEYS = ["itemFields"];
771
764
  const CHANGELOG_KEYS = ["labels"];
@@ -2077,76 +2070,6 @@ function normalizeRequiresSystem(value) {
2077
2070
  return requireNonEmptyString(value, "requiresSystem");
2078
2071
  }
2079
2072
 
2080
- /**
2081
- * How this package's year is divided, and the date it calls the present.
2082
- *
2083
- * **Nothing here is about which people count from what.** A reckoning is an
2084
- * era, declared by the affiliation that proclaimed it, and its epoch is the
2085
- * `start` written on that note — so the only things configuration states are
2086
- * the ones no note could.
2087
- *
2088
- * `months` and `monthDays` bound a written month and day. Both are optional,
2089
- * and an absent one is checked against nothing: a toolchain that assumed a
2090
- * Gregorian year here would refuse `667/2/30`, an ordinary date in a year of
2091
- * twelve thirty-day months. One subdivision to a package — an era moves where
2092
- * year 1 sits, it does not divide the year differently.
2093
- *
2094
- * `present` is the date the package computes an age against, written in the
2095
- * same grammar a note's dates use, so a package whose present is best said in
2096
- * an era says it that way. A package that states none computes no age, which
2097
- * is the right answer for a corpus that has no present.
2098
- *
2099
- * @param {unknown} value - The declared `calendar:` mapping.
2100
- * @returns {Readonly<{months: number|null, monthDays: number|null, present: string|null}>}
2101
- * Frozen; every value `null` when absent.
2102
- */
2103
- function normalizeCalendar(value) {
2104
- if (value === undefined || value === null) {
2105
- return Object.freeze({ months: null, monthDays: null, present: null });
2106
- }
2107
- if (!isPlainObject(value)) fail("calendar", "must be a mapping");
2108
- const input = /** @type {Record<string, unknown>} */ (value);
2109
- rejectUnknownKeys(input, CALENDAR_KEYS, "calendar.");
2110
-
2111
- const bound = (key) => {
2112
- const held = input[key];
2113
- if (held === undefined || held === null) return null;
2114
- if (!Number.isInteger(held) || /** @type {number} */ (held) < 1) {
2115
- fail(`calendar.${key}`, "must be a positive integer");
2116
- }
2117
- return /** @type {number} */ (held);
2118
- };
2119
- const months = bound("months");
2120
- const monthDays = bound("monthDays");
2121
-
2122
- // Read through the parser the notes use rather than a second grammar, and
2123
- // against this package's own bounds — a present outside the year it
2124
- // declares is the one date nothing downstream would report.
2125
- let present = null;
2126
- if (input.present !== undefined && input.present !== null) {
2127
- if (typeof input.present !== "string" && typeof input.present !== "number") {
2128
- fail("calendar.present", "must be a date written as a string");
2129
- }
2130
- if (String(input.present).trim() === UNKNOWN_DATE) {
2131
- fail(
2132
- "calendar.present",
2133
- `must be a date — a package that states no present computes no age, ` +
2134
- `so leave the key out rather than writing \`${UNKNOWN_DATE}\``,
2135
- );
2136
- }
2137
- const { date, findings } = parseNoteDate(input.present, {
2138
- calendar: { months, monthDays },
2139
- allowUnknown: false,
2140
- });
2141
- const refused = findings.find((finding) => finding.severity === "error");
2142
- if (refused)
2143
- fail("calendar.present", `is not a date this package can read — ${refused.message}`);
2144
- present = /** @type {{text: string}} */ (date).text;
2145
- }
2146
-
2147
- return Object.freeze({ months, monthDays, present });
2148
- }
2149
-
2150
2073
  /**
2151
2074
  * Validate the declared relationships.
2152
2075
  *
@@ -2878,7 +2801,6 @@ export function defineConfig(config) {
2878
2801
  relationships: normalizeRelationships(input.relationships),
2879
2802
  systems,
2880
2803
  requiresSystem,
2881
- calendar: normalizeCalendar(input.calendar),
2882
2804
  packageBuild: normalizePackageBuild(input.packageBuild),
2883
2805
  publish: normalizePublish(input.publish),
2884
2806
  changelog: normalizeChangelog(input.changelog),
package/docs/api.md CHANGED
@@ -606,18 +606,23 @@ The closed registry of system ids, and the `none` that stands for no system at a
606
606
 
607
607
  ### `engine.calendars`
608
608
 
609
- The axis every date normalises onto, and the one function that puts a reckoning on it. A reckoning is an **era** declared by the affiliation that proclaimed it, and its epoch is the `start` written on that note — nothing is registered in configuration. The conversion is piecewise on the sign, because the authored numbering has no year zero while the normalised value does: authored `-1` is `0`, and `-1` and `1` are adjacent.
610
-
611
- | Export | Signature | Returns | Use it when |
612
- | ------------------- | ------------------------------- | --------------------------------------------- | ------------------------------------------------------------------------------- |
613
- | `CANONICAL_EPOCH` | `const CANONICAL_EPOCH` | — | naming where the canonical axis's own year 1 sits |
614
- | `canonicalYear` | `canonicalYear(year, epoch?)` | `number` — the signed year on the axis | converting a year written in one reckoning to the number everything compares on |
615
- | `dateSortKey` | `dateSortKey(year, month, day)` | `number` | ordering mixed-precision dates against a single number |
616
- | `calendarStructure` | `calendarStructure(calendar)` | `{months, monthDays}` — `null` where unstated | reading the month bounds a package declares, before phrasing a finding |
609
+ The axis every date normalises onto, the arithmetic over a calendar's month list, and the moon. A reckoning is an **era** declared on the note about the calendar that counts those years, and its epoch is the `start` written on that row — nothing is registered in configuration. The year conversion is piecewise on the sign, because the authored numbering has no year zero while the normalised value does: authored `-1` is `0`, and `-1` and `1` are adjacent. `lunarPhase` takes a floored modulo for the same reason the conversion branches: a truncated one is right on one side of an epoch and wrong on the other.
610
+
611
+ | Export | Signature | Returns | Use it when |
612
+ | ------------------- | ---------------------------------- | -------------------------------------- | ------------------------------------------------------------------------------- |
613
+ | `CANONICAL_EPOCH` | `const CANONICAL_EPOCH` | — | naming where the canonical axis's own year 1 sits |
614
+ | `canonicalYear` | `canonicalYear(year, epoch?)` | `number` — the signed year on the axis | converting a year written in one reckoning to the number everything compares on |
615
+ | `dateSortKey` | `dateSortKey(year, month, day)` | `number` | ordering mixed-precision dates against a single number |
616
+ | `calendarStructure` | `calendarStructure(calendar)` | `{months}` — `null` where unstated | reading the months a calendar keeps, before phrasing a finding |
617
+ | `daysInMonth` | `daysInMonth(calendar, month)` | `number` — `null` where unstated | bounding a written day against the month it names |
618
+ | `daysInYear` | `daysInYear(months)` | `number` | asking what a month list claims the year is |
619
+ | `monthStarts` | `monthStarts(months)` | `number[]` | reading the same sum as a sequence, which is what an ordering error moves |
620
+ | `dayOfYear` | `dayOfYear(months, month, day)` | `number` | placing a written month and day in the year |
621
+ | `lunarPhase` | `lunarPhase(day, epochDay, cycle)` | `number` — `0` to `cycle - 1` | asking how far into its cycle a moon is, on either side of the epoch |
617
622
 
618
623
  ### `engine.noteDates`
619
624
 
620
- A date on a note is one authored string — `[~][-]YYYY[/MM[/DD]] [<affiliation shortcode>.<era shortcode>]` — and every field holding one parses through a single grammar into a single record. Print `text`, order on `sort`, do arithmetic on `canonicalYear`. A bare value sits on the canonical axis and carries `era: null`; one naming an era carries the qualifier and no `canonicalYear` or `sort` keys at all until the era is resolved against the corpus. The literal `unknown` parses to the same record with the year half empty, carrying a null `sort` and a null `canonicalYear`, so nothing can order it to one end of a list.
625
+ A date on a note is one authored string — `[~][-]YYYY[/MM[/DD]] [<calendar shortcode>.<era shortcode>]` — and every field holding one parses through a single grammar into a single record. Print `text`, order on `sort`, do arithmetic on `canonicalYear`. A bare value sits on the canonical axis and carries `era: null`; one naming an era carries the qualifier and no `canonicalYear` or `sort` keys at all until the era is resolved against the corpus. The literal `unknown` parses to the same record with the year half empty, carrying a null `sort` and a null `canonicalYear`, so nothing can order it to one end of a list.
621
626
 
622
627
  | Export | Signature | Returns | Use it when |
623
628
  | ----------------------- | ------------------------------- | -------------------------------------------------- | ------------------------------------------------------------- |
@@ -626,6 +631,24 @@ A date on a note is one authored string — `[~][-]YYYY[/MM[/DD]] [<affiliation
626
631
  | `ERA_QUALIFIER_PATTERN` | `const ERA_QUALIFIER_PATTERN` | — | checking that a string is an era qualifier a date could carry |
627
632
  | `parseNoteDate` | `parseNoteDate(value, options)` | `{date, findings}` — `date` is `null` when refused | reading a note's authored date, with every finding it earned |
628
633
 
634
+ ### `engine.calendarNotes`
635
+
636
+ A calendar is a `lore` note with `subType: calendar`, declaring the months it keeps, the weekdays it names, the seasons it marks and the eras it counts years in. How long the year is, how the day divides and how the moon moves are facts about the **world**, written on its `place` note and its moon's, and no calendar may restate them — which is why the months summing to the world's year is the only arithmetic a calendar note is held to. One definition is emitted per calendar, in array shape: Foundry core prunes the keys outside its schema, and Calendaria converts the arrays to its own keyed collections.
637
+
638
+ | Export | Signature | Returns | Use it when |
639
+ | -------------------- | ----------------------------------------------------- | ------------------------------------------ | --------------------------------------------------------------------------- |
640
+ | `CALENDAR_SUBTYPE` | `const CALENDAR_SUBTYPE` | — | naming the `lore` subType whose notes are calendars |
641
+ | `INVARIANT_SUBTYPES` | `const INVARIANT_SUBTYPES` | — | naming the `place` subTypes a world fact may be written on |
642
+ | `CALENDAR_FIELDS` | `const CALENDAR_FIELDS` | — | reading the closed list of what a calendar note may declare |
643
+ | `INVARIANT_FIELDS` | `const INVARIANT_FIELDS` | — | reading the closed list of what a body may declare about itself |
644
+ | `checkCalendarNote` | `checkCalendarNote(note, opts)` | `object[]` — findings | holding a `lore` note to the family and to the year the world keeps |
645
+ | `checkWorldFacts` | `checkWorldFacts(note, opts)` | `object[]` — findings | holding a `place` note's world facts to one body, one year and one moon |
646
+ | `worldInvariants` | `worldInvariants(index)` | `{year, present, moon, body, moonName, …}` | reading the year, the present and the moon a package's own notes state |
647
+ | `quarterDays` | `quarterDays(daysPerYear)` | `number[]` — four days of the year | asking which days the year turns on |
648
+ | `compileCalendar` | `compileCalendar({note, invariants, contentPackage})` | `object` — the definition | building the one object both a core consumer and Calendaria read |
649
+ | `compileCalendars` | `compileCalendars(index, opts)` | `{calendars, invariants}` | building every calendar a package declares, or none where it states no year |
650
+ | `calendariaEnvelope` | `calendariaEnvelope(definition, opts)` | `object` — the import envelope | wrapping a definition for a hand import through Calendaria's settings |
651
+
629
652
  ### `engine.contentAddress`
630
653
 
631
654
  Where a content note publishes on the web. One rule, in one place, because two builds need the same answer: the knowledgebase build renders the page, and the link manifest records the address other packages link to. Stating it twice is how a manifest comes to assert a URL that resolves at build time and 404s for the reader.
@@ -74,14 +74,13 @@ authoring.
74
74
  | [`relationships`](#relationships) | object | no; refused in a `documentation` package | `{}` |
75
75
  | [`systems`](#systems) | object | no; refused in a `documentation` package | `{}` |
76
76
  | [`requiresSystem`](#requiressystem) | string | no; refused in a `documentation` package | `null` |
77
- | [`calendar`](#calendar) | object | no | no bounds, no present |
78
77
  | [`packageBuild`](#the-packagebuild-section) | object | no | `{}` |
79
78
  | [`publish`](#publish) | object | no; **required**, with `site: content`, in a `documentation` package | `{site: "homepage", address: {prefix: ""}}` |
80
79
  | [`changelog`](#changelog) | object | no | `{labels: null}` |
81
80
 
82
81
  Any key outside this list is refused:
83
82
 
84
- > `` `<key>` is not a recognized option (expected one of: rootDir, contentPackage, foundryPackage, homepage, author, packageKind, stats, itemBuilders, paths, skipDirectories, icons, packs, docs, site, pdf, compatibility, relationships, systems, requiresSystem, calendar, packageBuild, publish, changelog). ``
83
+ > `` `<key>` is not a recognized option (expected one of: rootDir, contentPackage, foundryPackage, homepage, author, packageKind, stats, itemBuilders, paths, skipDirectories, icons, packs, docs, site, pdf, compatibility, relationships, systems, requiresSystem, packageBuild, publish, changelog). ``
85
84
 
86
85
  (`rootDir` appears in that list because it is a key `defineConfig` itself
87
86
  accepts — an `.mjs` configuration authors it directly. A YAML configuration
@@ -1298,59 +1297,6 @@ With one, naming what it does declare instead:
1298
1297
 
1299
1298
  > ``package-build config: `requiresSystem` must be a non-empty string.``
1300
1299
 
1301
- ### `calendar`
1302
-
1303
- **Type:** object · **Optional** · default: no bounds and no present.
1304
-
1305
- How this package's year is divided, and the date it calls the present:
1306
-
1307
- ```yaml
1308
- calendar:
1309
- months: 12
1310
- monthDays: 30
1311
- present: 720/6/19
1312
- ```
1313
-
1314
- **Nothing here is about which people count from what.** A reckoning is an
1315
- **era**, declared by the affiliation that proclaimed it, and its epoch is the
1316
- `start` written on that note — so the only things this block states are the ones
1317
- no note could.
1318
-
1319
- | Key (under `calendar`) | Type | Required | Default |
1320
- | ---------------------- | ---------------- | -------- | ------- |
1321
- | `calendar.months` | positive integer | no | `null` |
1322
- | `calendar.monthDays` | positive integer | no | `null` |
1323
- | `calendar.present` | date string | no | `null` |
1324
-
1325
- `months` and `monthDays` bound a written month and day. **Both are optional, and
1326
- an absent one is checked against nothing** — a year of twelve thirty-day months
1327
- is as ordinary here as a Gregorian one, so the bound is declared or it is not
1328
- applied. One subdivision to a package: an era moves where year 1 sits, it does
1329
- not divide the year differently.
1330
-
1331
- `present` is the date an age is computed against. It is written in the same
1332
- grammar a note's dates use — `[~][-]YYYY[/MM[/DD]] [<affiliation shortcode>.<era
1333
- shortcode>]` — so a package whose present is best said in an era says it that
1334
- way, and one that states no present computes no age at all.
1335
-
1336
- An age is a function of two **dates**, not two years: subtracting years alone
1337
- reds every note whose birthday falls later in the year than the present date, so
1338
- the day is part of what is declared.
1339
-
1340
- > ``package-build config: `calendar` must be a mapping.``
1341
-
1342
- > ``package-build config: `calendar.months` must be a positive integer.``
1343
-
1344
- > ``package-build config: `calendar.monthDays` must be a positive integer.``
1345
-
1346
- > ``package-build config: `calendar.present` must be a date written as a string.``
1347
-
1348
- > ``package-build config: `calendar.present` must be a date — a package that states no present computes no age, so leave the key out rather than writing `unknown`.``
1349
-
1350
- > ``package-build config: `calendar.present` is not a date this package can read — `720/13/1` writes month 13, and `calendar.months` declares a year of 12.``
1351
-
1352
- > ``package-build config: `calendar.<key>` is not a recognized option (expected one of: months, monthDays, present).``
1353
-
1354
1300
  ### `packageBuild`
1355
1301
 
1356
1302
  Reserved for `@heroiclands/package-build`'s own packaging half — see
@@ -3217,8 +3217,65 @@ In-world information about people, places, or concepts.
3217
3217
  tournament is not a matter of time-reckoning at all — and from `culture`, which is a grouping
3218
3218
  of people rather than an occasion they attend.
3219
3219
 
3220
- | `data` property | Values | Description |
3221
- | --------------- | ------ | ----------- |
3220
+ | `data` property | Values | Description |
3221
+ | --------------- | ----------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
3222
+ | `epoch` | `date` | Which in-world day the world's clock reads zero on |
3223
+ | `months` | `{ name, abbreviation?, days }[]` | The months this calendar keeps, in order — the list sums to the world's year |
3224
+ | `weekdays` | `{ name, abbreviation? }[]` | The days of the week it names, in order; a calendar with no week writes none |
3225
+ | `seasons` | `{ name, abbreviation?, monthStart?, monthEnd?, dayStart?, dayEnd? }[]` | The seasons it marks, bounded by month or by day of year |
3226
+ | `eras` | `{ shortcode, name, abbreviation?, proclaimedBy?, start, end? }[]` | The year-counts kept in it, each addressed `<calendar shortcode>.<era shortcode>` |
3227
+
3228
+ #### What a calendar note declares
3229
+
3230
+ **Those five keys belong to `subType: calendar` and to no other genre of
3231
+ `lore`.** A note carrying one under any other subType is refused, and a calendar
3232
+ note that states no `months` and no `epoch` is refused too: a calendar divides
3233
+ the year and says where the count begins, and one that does neither is prose
3234
+ about time rather than a calendar.
3235
+
3236
+ ```yaml
3237
+ type: lore
3238
+ subType: calendar
3239
+ shortcode: commoncal
3240
+ name:
3241
+ full: The Common Calendar
3242
+ data:
3243
+ epoch: 720/1/1
3244
+ months:
3245
+ - { name: Floralis, abbreviation: Flor, days: 30 }
3246
+ - { name: Lusenar, abbreviation: Luse, days: 31 }
3247
+ # … ten more, summing to the world's year
3248
+ seasons:
3249
+ - { name: Spring, monthStart: 1, monthEnd: 3 }
3250
+ eras:
3251
+ - shortcode: founding
3252
+ name: After the Founding
3253
+ abbreviation: AF
3254
+ proclaimedBy: vylarinmpr
3255
+ start: 1
3256
+ ```
3257
+
3258
+ **Position in the list is position in the year.** Days that belong to no month
3259
+ are a short month like any other: a five-day festival is one entry, and where it
3260
+ sits in the list is where it falls in the year. There is no intercalary
3261
+ mechanism, because there is nothing for one to do.
3262
+
3263
+ **How long the year is, how the day divides and how the moon moves are facts
3264
+ about the world, not about a calendar**, so no calendar note states any of them
3265
+ — they are written once on the world's own `place` note and on its moon's, and
3266
+ every calendar is compiled against them. The one arithmetic check follows from
3267
+ that: `months` must sum to `year.days`, and a list that does not describes a
3268
+ different world rather than a different calendar.
3269
+
3270
+ **An era is a way of counting years within a calendar**, so its rows sit beside
3271
+ the months. Each states its own `shortcode`, unique within the note, and a date
3272
+ names one by writing it after the calendar's address — `3 commoncal.founding`.
3273
+ `proclaimedBy` is optional: a reckoning whose proclaiming body is unknown, or
3274
+ whose body has no note, says so by leaving it out.
3275
+
3276
+ **A calendar with no week writes no `weekdays`.** An empty list and an absent
3277
+ one say the same thing, and nothing downstream shows a weekday for a calendar
3278
+ that names none.
3222
3279
 
3223
3280
  ### type: map
3224
3281
 
@@ -3370,16 +3427,72 @@ three maps, and none of those maps needs to know it is a keep.
3370
3427
  - site: A place significant by what was made or done there — ruins, monoliths, henges, works, battlefields.
3371
3428
  - structure: A single building or habitation — halls, keeps, temples, inns, towers.
3372
3429
  - feature: A place significant by its terrain — forests, rivers, falls, passes, fords.
3430
+ - celestial: A body observed from a world rather than located on one — a sun, a planet, a comet.
3431
+
3432
+ | `data` property | Values | Description |
3433
+ | --------------------------------- | --------------------------------------------------- | ---------------------------------------------------------------------------- |
3434
+ | `demonym` | `string` | What a person from this place is called — a Vylarian |
3435
+ | `lore` | `WikiLink[]` | Lore concerning this place — its peoples, its law, its calendar, its history |
3436
+ | `parents` | `WikiLink[]` | Enclosing places within which this place is located |
3437
+ | `population` | `number` | Approximate population (precision 2 significant digits) |
3438
+ | `market` | `number` | Market class, 1 to 6 — what trade a settlement supports |
3439
+ | `borders` | `{ to, bearing }[]` | Places sharing a frontier with this one, and where each lies from here |
3440
+ | `routes` | `{ to, bearing, mode, days, terrain?, leagues? }[]` | Journeys from this place's centre to another place |
3441
+ | `world.equatorialCircumferenceKm` | `number` | The distance round the body at its equator, in kilometres |
3442
+ | `world.surfaceGravityG` | `number` | Surface gravity, as a multiple of Earth's |
3443
+ | `world.axialTiltDegrees` | `number` | The tilt of the axis, in degrees |
3444
+ | `year.days` | `number` | How many days the body's year holds |
3445
+ | `year.hoursPerDay` | `number` | How many hours the day divides into |
3446
+ | `year.minutesPerHour` | `number` | How many minutes the hour divides into |
3447
+ | `year.secondsPerMinute` | `number` | How many seconds the minute divides into |
3448
+ | `present` | `date` | The day the setting stops and play begins |
3449
+ | `body.diameterKm` | `number` | The body's diameter, in kilometres |
3450
+ | `body.orbitalRadiusKm` | `number` | How far it orbits from the body it circles, in kilometres |
3451
+ | `body.orbit` | `string` | The orbit's shape — `circular` means the cycle never varies |
3452
+ | `body.inclined` | `boolean` | Whether the orbit is inclined to the plane the world orbits in |
3453
+ | `moon.cycle` | `number` | How many days it takes to return to the same phase |
3454
+ | `moon.newOn` | `date` | A day it was new, written in the reference calendar |
3455
+ | `moon.eclipses` | `string` | `never`, `rare`, `occasional` or `frequent` |
3456
+
3457
+ #### What a body states about itself
3458
+
3459
+ **The four groups above are the world's own facts, and they are legal on a
3460
+ `world` or a `celestial` note and nowhere else.** A region, a settlement, a
3461
+ site, a structure or a feature is somewhere _within_ a world and says nothing
3462
+ about the world's year.
3463
+
3464
+ ```yaml
3465
+ # the world — type: place, subType: world
3466
+ data:
3467
+ world: { equatorialCircumferenceKm: 40000, surfaceGravityG: 1.0, axialTiltDegrees: 23.5 }
3468
+ year: { days: 365, hoursPerDay: 24, minutesPerHour: 60, secondsPerMinute: 60 }
3469
+ present: 720
3470
+
3471
+ # its moon — a note of its own, because a moon is a body and not a property
3472
+ data:
3473
+ body: { diameterKm: 3800, orbitalRadiusKm: 388600, orbit: circular, inclined: true }
3474
+ moon: { cycle: 30, newOn: 720/1/1, eclipses: rare }
3475
+ ```
3373
3476
 
3374
- | `data` property | Values | Description |
3375
- | --------------- | --------------------------------------------------- | ---------------------------------------------------------------------------- |
3376
- | `demonym` | `string` | What a person from this place is called — a Vylarian |
3377
- | `lore` | `WikiLink[]` | Lore concerning this place — its peoples, its law, its calendar, its history |
3378
- | `parents` | `WikiLink[]` | Enclosing places within which this place is located |
3379
- | `population` | `number` | Approximate population (precision 2 significant digits) |
3380
- | `market` | `number` | Market class, 1 to 6 — what trade a settlement supports |
3381
- | `borders` | `{ to, bearing }[]` | Places sharing a frontier with this one, and where each lies from here |
3382
- | `routes` | `{ to, bearing, mode, days, terrain?, leagues? }[]` | Journeys from this place's centre to another place |
3477
+ **`year.days` is where the year's length lives, and every calendar in the
3478
+ package is compiled against it.** A package whose notes state no `year` has no
3479
+ calendar mechanism at all: no sum is checked, and nothing is emitted. That is
3480
+ the right answer for a package building a setting whose world facts somebody
3481
+ else wrote down.
3482
+
3483
+ **One year and one moon to a package.** Two years is two settings and nothing
3484
+ can say which one a calendar divides; a `moon` with no `year` anywhere is
3485
+ refused, because a cycle counted in days has no year to be measured against.
3486
+
3487
+ **`present` is the day an age is computed against**, written in the grammar a
3488
+ note's dates are written in, so a setting whose present is best said in an era
3489
+ says it that way.
3490
+
3491
+ **`world.axialTiltDegrees`, the circumference, the gravity, the moon's diameter
3492
+ and its orbital radius are read by nobody.** They are there so a reader knows
3493
+ what kind of world this is. What the build consumes is the year, the day's
3494
+ divisions, the present, the moon's cycle and epoch, and whether its orbit is
3495
+ circular and inclined.
3383
3496
 
3384
3497
  #### What a settlement's market supports
3385
3498