@heroiclands/package-build 22.14.3 → 22.15.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 (62) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/CONTENT.md +25 -1
  3. package/bin/content-build.mjs +3 -7
  4. package/bin/package-build.mjs +25 -0
  5. package/docs/api.md +64 -36
  6. package/docs/commands.md +65 -9
  7. package/docs/configuration.md +6 -3
  8. package/docs/content-format.md +187 -140
  9. package/docs/diagnostics.md +1 -1
  10. package/docs/getting-started.md +27 -3
  11. package/engine/base-compiler.mjs +3 -1
  12. package/engine/being-age.mjs +16 -24
  13. package/engine/calendar-artifacts.mjs +153 -0
  14. package/engine/calendar-notes.mjs +64 -16
  15. package/engine/calendars.mjs +129 -0
  16. package/engine/content-images.mjs +20 -10
  17. package/engine/content-index.mjs +3 -1
  18. package/engine/exported-scene.mjs +230 -0
  19. package/engine/generate.mjs +1 -0
  20. package/engine/ids.mjs +1 -1
  21. package/engine/index.mjs +6 -0
  22. package/engine/infobox.mjs +4 -0
  23. package/engine/itinerary-scenes.mjs +170 -0
  24. package/engine/map-build.mjs +10 -45
  25. package/engine/map-dot.mjs +6 -3
  26. package/engine/map-graphviz.mjs +50 -131
  27. package/engine/map-notes.mjs +26 -3
  28. package/engine/map-places.mjs +12 -6
  29. package/engine/map-raster.mjs +49 -0
  30. package/engine/note-dates.mjs +125 -85
  31. package/engine/note-vocabulary.mjs +22 -6
  32. package/engine/pdf-build.mjs +57 -1
  33. package/engine/pdf-render.mjs +48 -7
  34. package/engine/reckoning-markers.mjs +207 -0
  35. package/engine/scenes.mjs +113 -26
  36. package/engine/site-build.mjs +11 -13
  37. package/engine/site-maps.mjs +5 -35
  38. package/engine/site-preview.mjs +6 -1
  39. package/engine/social-tie-terms.mjs +3 -0
  40. package/engine/social-ties.mjs +30 -19
  41. package/package.json +3 -1
  42. package/types/engine/being-age.d.mts +2 -2
  43. package/types/engine/calendar-artifacts.d.mts +16 -0
  44. package/types/engine/calendar-notes.d.mts +1 -1
  45. package/types/engine/calendars.d.mts +65 -0
  46. package/types/engine/content-images.d.mts +7 -4
  47. package/types/engine/exported-scene.d.mts +12 -0
  48. package/types/engine/index.d.mts +2 -0
  49. package/types/engine/itinerary-scenes.d.mts +29 -0
  50. package/types/engine/map-build.d.mts +4 -13
  51. package/types/engine/map-graphviz.d.mts +17 -59
  52. package/types/engine/map-places.d.mts +5 -1
  53. package/types/engine/map-raster.d.mts +8 -0
  54. package/types/engine/note-dates.d.mts +6 -0
  55. package/types/engine/note-vocabulary.d.mts +3 -3
  56. package/types/engine/pdf-render.d.mts +16 -1
  57. package/types/engine/reckoning-markers.d.mts +27 -0
  58. package/types/engine/scenes.d.mts +9 -2
  59. package/types/engine/site-build.d.mts +1 -5
  60. package/types/engine/site-maps.d.mts +2 -7
  61. package/types/engine/social-tie-terms.d.mts +2 -0
  62. package/types/engine/social-ties.d.mts +2 -1
@@ -326,6 +326,42 @@ flow collection is formatted into one of those forms; folded collections are
326
326
  not used. `content-build format --write` applies this rule and the top-level
327
327
  order without changing the Markdown body.
328
328
 
329
+ #### Dates and reckoning markers
330
+
331
+ A calendar names and orders the days within a year. An **era** states where its
332
+ year count begins. Several eras may use one calendar; each has its own origin.
333
+ An era may declare a short, uppercase `marker` for authored dates. The marker
334
+ selects that era and the calendar whose months interpret the date inside it:
335
+
336
+ ```yaml
337
+ data:
338
+ born: VR(676/12/5)
339
+ ```
340
+
341
+ `VR` is a marker declared on an era row of the Common Calendar. `VR(720/5/14)`
342
+ means its year 720, fifth month, fourteenth day. `VR(720/5)` states month
343
+ precision, and `VR(720)` states year precision. The parser retains that
344
+ precision; it does not invent a first day when the author only knows a year.
345
+ `VR(~-480)` keeps an approximate year. A date whose occurrence is known but
346
+ whose value is not recorded is `unknown`, with no marker.
347
+
348
+ The calendar-neutral spelling is `<year>.<day>[:HHMMSS]`: `720.136` names day
349
+ 136 of canonical year 720, and `720.136:143005` adds 14:30:05. The dot means
350
+ **day of year**, not a month. Its day must fit the world's `data.year.days`,
351
+ and its time must fit a 24-hour day. A bare year is also accepted. Unmarked
352
+ slash dates are accepted as calendar-neutral numeric month/day values; their
353
+ month division has no named calendar, so use a marker when the month is part
354
+ of a people's reckoning.
355
+
356
+ An era's `start` locates its first year on the canonical timeline. The marker's
357
+ own start is written canonically, such as `start: 1`, so its definition has no
358
+ circular reference. Another era may place its start using a marker already
359
+ defined in the corpus. A year-precision start anchors the reckoning at the
360
+ first day of that canonical year for conversion; it does not date the event
361
+ that gave the era its name. A marker must be unique across the corpus and contain
362
+ uppercase letters or digits, beginning with a letter. An optional `end` bounds
363
+ dates in that era; a year-precision end includes the whole named year.
364
+
329
365
  **`data:` is closed.** It holds the type-specific facts about the
330
366
  subject — a weapon's weight, an affliction's transmission, a being's species —
331
367
  and every note type declares which keys it may carry. A misspelled key is a
@@ -2066,10 +2102,10 @@ place for those words and this is it.
2066
2102
 
2067
2103
  ##### Width is a class, and the ordinary width carries no marker
2068
2104
 
2069
- | `class` value | book | website | Foundry journal |
2070
- | ------------- | ------------- | ------------------ | ---------------- |
2071
- | _(none)_ | one column | the text measure | the page measure |
2072
- | `.full-width` | the full page | full content width | full page width |
2105
+ | `class` value | book | website | Foundry journal |
2106
+ | ------------- | ----------------------------- | ------------------ | --------------- |
2107
+ | _(none)_ | natural width within a column | natural width | natural width |
2108
+ | `.full-width` | page measure | full content width | full page width |
2073
2109
 
2074
2110
  The simple case needs no spelling, which is why the ordinary width has no name.
2075
2111
 
@@ -2080,21 +2116,24 @@ refused.
2080
2116
 
2081
2117
  ##### Named size is `size:`
2082
2118
 
2083
- | `size` value | display |
2084
- | ------------ | ------------------------ |
2085
- | `auto` | same as omitting `size:` |
2086
- | `small` | same as omitting `size:` |
2087
- | `medium` | same as omitting `size:` |
2088
- | `large` | same as omitting `size:` |
2089
- | `xlarge` | same as omitting `size:` |
2090
- | `full-width` | same as omitting `size:` |
2091
-
2092
- The parser accepts and records these names for Markdown images and embedded
2093
- assets. **A `size:` value does not change the displayed dimensions** on the
2094
- website, in Foundry, or in the book. With no `size:`, the parser records `auto`;
2095
- the existing rendering rules apply. In particular, book images occupy a column
2096
- unless they carry `.full-width`. A `size:` may sit before or after `float:` in
2097
- the same directive, and neither changes the other's behavior.
2119
+ | `size` value | website and Foundry | book |
2120
+ | ------------ | ------------------- | ------------ |
2121
+ | `auto` | natural size | natural size |
2122
+ | `small` | 64 CSS pixels | 1.6 cm |
2123
+ | `medium` | 128 CSS pixels | 3.2 cm |
2124
+ | `large` | 256 CSS pixels | 5.6 cm |
2125
+ | `xlarge` | 512 CSS pixels | 8 cm |
2126
+ | `full-width` | content measure | page measure |
2127
+
2128
+ These are maximum display widths for Markdown images and embedded assets;
2129
+ height follows the file's aspect ratio. The available column, page, or browser
2130
+ measure caps every size, and print also caps image height within the page.
2131
+ Omitting `size:` means `auto`, which uses the file's natural dimensions within
2132
+ those bounds. `full-width` spans the book's page rather than one column. The
2133
+ `.full-width` class also grants page scope in print; when it accompanies a
2134
+ bounded `size:`, the image keeps that bounded width inside the page figure.
2135
+ A `size:` may sit before or after `float:` in the same directive, and neither
2136
+ changes the other's behavior.
2098
2137
 
2099
2138
  ##### Position is `float:`
2100
2139
 
@@ -2449,6 +2488,19 @@ an acquaintance knows them without a strong disposition; a rival opposes them
2449
2488
  without implacable hostility; a nemesis opposes them personally and
2450
2489
  implacably. The infobox groups targets by term and links to their notes.
2451
2490
 
2491
+ ```yaml
2492
+ data:
2493
+ socialTies:
2494
+ being-kaldor: patron
2495
+ being-alys: dependent
2496
+ affiliation-silverguild: nemesis
2497
+ kethira-note-being-kaldas: rival
2498
+ ```
2499
+
2500
+ The full Address names a target in another package. Each target has one tie:
2501
+ writing its short and full Address in the same map still names one target and is
2502
+ an error.
2503
+
2452
2504
  Each tie is one-sided. The target need not name the subject back, even when
2453
2505
  `patron` and `dependent` would describe opposite ends of the same support.
2454
2506
  Kinship, household membership, organizational membership, rank, and office are
@@ -3422,15 +3474,15 @@ In-world information about people, places, or concepts.
3422
3474
  tournament is not a matter of time-reckoning at all — and from `culture`, which is a grouping
3423
3475
  of people rather than an occasion they attend.
3424
3476
 
3425
- | `data` property | Values | Description |
3426
- | --------------- | ----------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
3427
- | `epoch` | `date` | Which in-world day the world's clock reads zero on |
3428
- | `months` | `{ name, abbreviation?, days }[]` | The months this calendar keeps, in order — the list sums to the world's year |
3429
- | `weekdays` | `{ name, abbreviation? }[]` | The days of the week it names, in order; a calendar with no week writes none |
3430
- | `seasons` | `{ name, abbreviation?, monthStart?, monthEnd?, dayStart?, dayEnd? }[]` | The seasons it marks, bounded by month or by day of year |
3431
- | `eras` | `{ shortcode, name, abbreviation?, proclaimedBy?, start, end? }[]` | The year-counts kept in it, each addressed `<calendar shortcode>.<era shortcode>` |
3432
- | `dateFormats` | `Map<slot, format>` | Calendaria display formats for this calendar |
3433
- | `event` | `Map<field, value>` | A dated occurrence, with its sources, locations, reach, and relationships |
3477
+ | `data` property | Values | Description |
3478
+ | --------------- | --------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
3479
+ | `epoch` | `date` | Which in-world day the world's clock reads zero on |
3480
+ | `months` | `{ name, abbreviation?, days }[]` | The months this calendar keeps, in order — the list sums to the world's year |
3481
+ | `weekdays` | `{ name, abbreviation? }[]` | The days of the week it names, in order; a calendar with no week writes none |
3482
+ | `seasons` | `{ name, abbreviation?, monthStart?, monthEnd?, dayStart?, dayEnd? }[]` | The seasons it marks, bounded by month or by day of year |
3483
+ | `eras` | `{ shortcode, name, marker?, abbreviation?, proclaimedBy?, start, end? }[]` | The year-counts kept in it; a marker names one era and uses this calendar's months |
3484
+ | `dateFormats` | `Map<slot, format>` | Calendaria display formats for this calendar |
3485
+ | `event` | `Map<field, value>` | A dated occurrence, with its sources, locations, reach, and relationships |
3434
3486
 
3435
3487
  `data.event` is available on every `lore` subType. It holds structured
3436
3488
  chronology metadata, including the event's kind, date, sources, and the places
@@ -3447,13 +3499,13 @@ the year and says where the count begins, and one that does neither is prose
3447
3499
  about time rather than a calendar.
3448
3500
 
3449
3501
  ```yaml
3450
- type: lore
3451
- subType: calendar
3452
3502
  shortcode: commoncal
3453
3503
  name:
3454
3504
  full: The Common Calendar
3505
+ type: lore
3506
+ subType: calendar
3455
3507
  data:
3456
- epoch: 720/1/1
3508
+ epoch: VR(720/1/1)
3457
3509
  months:
3458
3510
  - { name: Floralis, abbreviation: Flor, days: 30 }
3459
3511
  - { name: Lusenar, abbreviation: Luse, days: 31 }
@@ -3463,6 +3515,7 @@ data:
3463
3515
  eras:
3464
3516
  - shortcode: founding
3465
3517
  name: After the Founding
3518
+ marker: VR
3466
3519
  abbreviation: AF
3467
3520
  proclaimedBy: vylarinmpr
3468
3521
  start: 1
@@ -3489,8 +3542,10 @@ that: `months` must sum to `year.days`, and a list that does not describes a
3489
3542
  different world rather than a different calendar.
3490
3543
 
3491
3544
  **An era is a way of counting years within a calendar**, so its rows sit beside
3492
- the months. Each states its own `shortcode`, unique within the note, and a date
3493
- names one by writing it after the calendar's address — `3 commoncal.founding`.
3545
+ the months. Each states its own `shortcode`, unique within the note. A date
3546
+ names an era with its marker, such as `VR(720/5/14)`. A calendar may contain
3547
+ several era rows, each with its own marker and year origin. The marker is an
3548
+ authoring identifier; the `abbreviation` is its printed label.
3494
3549
  `proclaimedBy` is optional: a reckoning whose proclaiming body is unknown, or
3495
3550
  whose body has no note, says so by leaving it out.
3496
3551
 
@@ -3500,115 +3555,96 @@ that names none.
3500
3555
 
3501
3556
  ### type: map
3502
3557
 
3503
- A map — the visual rendering of a place or an encounter, plus the pinned
3504
- locations described in its own body. Produces a Foundry **Scene**, and a web page carrying the map image
3505
- and its description.
3558
+ A map note has its own prose and a Foundry Scene. A map can depict a place or an encounter. Several maps can depict the same place; the map names that place with `sohl.place`, while the place note does not list its maps.
3506
3559
 
3507
- A map is a note in its own right rather than a property of a `place`, for three
3508
- reasons. A place commonly has **several** maps — a floor per level of a keep, or
3509
- a feature's detail map alongside its local-area map — and a scene nested in a
3510
- place could only ever be one. A Scene is a substantial document in itself: walls,
3511
- doors, lights, tiles, sounds, region shapes and pins, authored in two deliberate
3512
- unit conventions. And a map has prose and named locations of its own, which makes
3513
- it a document rather than a field.
3560
+ **subType**:
3514
3561
 
3515
- A map may also exist with no place at all — an ambush on a road is an encounter
3516
- map and not a named location.
3562
+ - `battlemap`: A tactical Scene built in Foundry.
3563
+ - `localmap`: A settlement or local-area Scene built in Foundry.
3564
+ - `regionalmap`: A large-scale chart with an authored image and scale.
3565
+ - `totm`: A gridless image Scene for theatre-of-the-mind play.
3566
+
3567
+ | `data` property | Values | Description |
3568
+ | ----------------- | ------------ | ---------------------------------------------------------- |
3569
+ | `scene` | object | Exported Foundry Scene for battle and local maps |
3570
+ | `fixup` | object[] | Asset address replacements in an exported Scene |
3571
+ | `bgImage` | Address | Background image for regional and theatre-of-the-mind maps |
3572
+ | `scale` | object | Regional distance and unit per grid unit |
3573
+ | `dimensions` | `[int, int]` | Optional shared dimensions metadata |
3574
+ | `pxPerGrid` | number | Optional shared grid size metadata |
3575
+ | `navName` | string | Short Scene navigation name |
3576
+ | `levelName` | string | Embedded Level name |
3577
+ | `backgroundColor` | string | Colour outside the art |
3578
+ | `overlay` | string | Foreground art path |
3579
+ | `walls` | object | Regional wall geometry |
3580
+ | `doors` | object | Regional door geometry |
3581
+ | `lights` | object | Regional light geometry |
3582
+ | `tiles` | object | Regional tile geometry |
3583
+ | `sounds` | object | Regional sound geometry |
3584
+ | `regions` | object | Regional region geometry |
3585
+ | `notes` | object | Regional pin geometry |
3586
+ | `place` | Address | Depicted place, when one exists |
3587
+
3588
+ #### Battle and local maps
3589
+
3590
+ Author the Scene in Foundry, export it, convert the export to YAML, and put the resulting object at `data.scene`. Preserve its walls, lights, tiles, regions, levels, environment, flags, and canvas settings. The export can name art from another Foundry package; that path is used as authored in Foundry. The Scene needs positive whole-number `width` and `height`, at least one Level, a valid `initialLevel` when present, and a unique 16-character `_id` on each embedded document. The build derives the top-level Scene `_id` from the map note, adds compendium keys and build metadata, and otherwise preserves the exported fields.
3591
+
3592
+ A pin's `text` can contain `#anchor`, where the map note has a heading such as `# Big Bad Wolf {#myanchor1}`. With `text: '#myanchor1'`, the compiled pin opens the JournalEntry and page generated from this map note and displays **Big Bad Wolf**. Its position, icon, other settings, and embedded `_id` remain as exported. A missing anchor is a build error. Pins whose text does not use `#anchor` retain their exported text and references.
3593
+
3594
+ `data.fixup` replaces exported asset paths without editing the Scene object by
3595
+ hand. Each entry has `path`, `type: address`, and `value`, an asset Address in
3596
+ the local or a fetched content index. Paths start at `data.scene` and use a
3597
+ property path with array indices or stable embedded `_id` selectors. A path
3598
+ must name one existing string or null field, and the address must resolve; any
3599
+ mistake fails the build. An `_id` selector remains stable when Foundry reorders
3600
+ an array on export.
3517
3601
 
3518
- **The place is named on the map, not the other way round.** A place commonly has
3519
- several maps, so listing them from the place is the end that goes stale; naming
3520
- the place from the map is the end that cannot, because a map is written once and
3521
- depicts what it depicts. A place's maps are therefore derived — every map whose
3522
- `place` is this one — and the relation exists in exactly one place.
3602
+ ```yaml
3603
+ data:
3604
+ fixup:
3605
+ - { path: ".notes[NNNNNNNNNNNNNNNN].texture.src", type: address, value: sohl-none-icon-book }
3606
+ - { path: ".sounds[0].path", type: address, value: sohl-none-audio-forest }
3607
+ ```
3523
3608
 
3524
- **subType**:
3609
+ Fixups change the compiled copy only. The exported Scene in the note stays
3610
+ intact, and pin anchor binding runs after fixups. A package name such as
3611
+ `fvtt` is an ordinary address package name: it needs published asset records
3612
+ before its addresses can resolve.
3525
3613
 
3526
- - battlemap: Tactical scale, for a scene played out square by square.
3527
- - localmap: Roughly a kilometre across — a settlement, a holding, a small valley.
3528
- - regionalmap: Large scale, covering a region or a journey between places.
3529
-
3530
- The three differ only in the canvas defaults derived for them, which is why they
3531
- are subTypes of one type rather than three types.
3532
-
3533
- **NoteLocation** is `[GridLocation, anchor]` where the `anchor` is an anchor identified in the body of the note, and `GridLocation` represents a particular grid location on the document.
3534
-
3535
- The `data:` fields, of which three are required:
3536
-
3537
- | `data` property | Values | Description |
3538
- | ----------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
3539
- | `bgImage` | `Address` | **Required.** The map art — an `image` address. Becomes the level's `background.src`, what tokens stand on |
3540
- | `dimensions` | `[int, int]` | **Required.** `[width, height]` in whole pixels, the art's own size |
3541
- | `pxPerGrid` | `integer` | **Required.** Whole pixels per grid square; must match the art |
3542
- | `navName` | `string` | Short name for Foundry's scene navigation bar |
3543
- | `levelName` | `string` | The name of the embedded level. Defaults to `Ground` |
3544
- | `backgroundColor` | `ColorHexValue` | Shown where the art does not reach. Defaults to `#999999` |
3545
- | `overlay` | `string` | Path to **foreground** art |
3546
- | `walls` | `WallSegment[]` | List of wall segments |
3547
- | `doors` | `Door[]` | List of doors |
3548
- | `lights` | `Light[]` | List of lights |
3549
- | `tiles` | `Tile[]` | List of tiles, each naming its art in `image` |
3550
- | `sounds` | `Sound[]` | List of sounds, each naming its clip in `audio` |
3551
- | `regions` | `SceneRegion[]` | List of scene regions |
3552
- | `place` | `Address` | The place this map depicts. Optional, because an encounter map depicts no named place — but that is the exception, and a map without one is a map of nowhere |
3553
- | `notes` | `NoteLocation[]` | grid coordinates of note markers mapped to anchors in this document |
3554
-
3555
- Everything else a Scene holds is **derived**, not authored: padding, grid type,
3556
- grid distance and units, token vision and fog mode all come from the subType, and
3557
- ids, ordering and elevation are synthesised. A map note carries the map's
3558
- essence, not a Scene's schema — the same division a weapon note makes against an
3559
- Item's.
3560
-
3561
- **`levels:` is never authored, and must never be.** A Scene has to ship at least
3562
- one Level or it has no map at all, and nothing supplies one after the fact: the
3563
- client-side `_preCreate` that would create it does not run during offline pack
3564
- compilation, and the server-side migration shim is version-gated on
3565
- `_stats.coreVersion`, so a pack stamped 14.x or later skips it entirely. The
3566
- single Level is synthesised from `bgImage`, `overlay`, `levelName` and
3567
- `backgroundColor`.
3568
-
3569
- > **`bgImage` is an `Address` under `data:`, as every other type's artwork is.**
3570
- > Art is not system-specific — a Scene is a core Foundry document, and a second
3571
- > system would want the identical art — so the field sits under `data:` rather
3572
- > than inside a system block.
3573
- >
3574
- > **A map has no `img`.** `bgImage` is the only key naming a map's background,
3575
- > and a note writing `img:` or `data.img:` is refused. The two are not
3576
- > alternative spellings: `img` is a Foundry destination and no Scene has one.
3577
-
3578
- **Every art key inside a map is an address, and each is named for the type it
3579
- reaches.** A map's geometry is authored in Foundry's own units, and its art is
3580
- not authored in Foundry's own paths.
3581
-
3582
- | authored | default type | emitted as |
3583
- | -------------------- | ------------ | ---------------------------- |
3584
- | `data.bgImage` | `image` | the level's `background.src` |
3585
- | `tiles.<key>.image` | `image` | the tile's `texture.src` |
3586
- | `sounds.<key>.audio` | `audio` | the `AmbientSound`'s `path` |
3587
-
3588
- Naming each key for the type it references is what makes the default worth
3589
- having: `audio:` reaches an `audio` address without a note ever writing the
3590
- type segment. Where a tile places a glyph rather than artwork it qualifies —
3591
- `image: sohl-none-icon-chest` — which is the ordinary short-form ladder rather
3592
- than an exception to it.
3593
-
3594
- **The authored key and the emitted field differ where Foundry's name differs.**
3595
- An `AmbientSound` stores its clip in `path`, and that is what the compiled
3596
- document carries; the note writes `audio` because the note is naming an
3597
- address, not filling in a Foundry field.
3598
-
3599
- **Two unit conventions, deliberately.** Geometry — walls, doors, lights, tiles,
3600
- sounds, region shapes — is authored in **pixels**, Foundry's native storage,
3601
- because traced walls often fall between grid intersections. Map pins are
3602
- authored in **grid squares**, commonly
3603
- half-integers, because that is how a person reads a position off a map. The two
3604
- are told apart by their key: `position:` and segment or shape coordinates are
3605
- pixels, `at:` is grid squares. Mixing them fails silently and visually in
3606
- Foundry, so the build refuses rather than resolving.
3607
-
3608
- **Map pins stay within the map.** `notes:` names anchors in this map note's own
3609
- body, so a pin opens the journal page describing that spot. `data.place` names
3610
- the place depicted by the map when one exists. A place's map listing is derived
3611
- from those map notes, so each map states the relationship once.
3614
+ ```yaml
3615
+ shortcode: wolfden
3616
+ name: { full: The Wolf's Den }
3617
+ type: map
3618
+ subType: battlemap
3619
+ data:
3620
+ scene:
3621
+ name: Wolf Den Scene
3622
+ width: 1900
3623
+ height: 2600
3624
+ grid: { type: 1, size: 100, distance: 5, units: ft }
3625
+ initialLevel: defaultLevel0000
3626
+ levels:
3627
+ - { _id: defaultLevel0000, background: { src: modules/maps/den.webp } }
3628
+ notes:
3629
+ - { _id: NNNNNNNNNNNNNNNN, text: "#myanchor1", x: 950, y: 2350 }
3630
+ ---
3631
+ # Big Bad Wolf {#myanchor1}
3632
+ ```
3633
+
3634
+ When the Scene has pins, its Adventure bundle carries the Scene and the JournalEntry generated from its Markdown. Importing that bundle preserves the IDs the pins address.
3635
+
3636
+ #### Regional maps
3637
+
3638
+ A `regionalmap` uses `data.bgImage`, an image Address, and `sohl.dimensions: [width, height]` plus `sohl.pxPerGrid` to build a gridless Scene. `data.scale: {distance: positive number, unit: nonempty string}` sets the distance per grid unit. Its shared `data` fields can also name `overlay`, `backgroundColor`, `levelName`, `navName`, `walls`, `doors`, `lights`, `tiles`, `sounds`, `regions`, and `notes`; the latter geometry uses the map-note conventions described in the SoHL map authoring guide. `data.scene` is not used for a regional map.
3639
+
3640
+ An SVG `data.bgImage` remains vector art on the site and in the book. The Foundry build stages a PNG at the note's stated dimensions with the installed SVG renderer; the source SVG is unchanged. Its aspect ratio must match the stated dimensions. A selected regional SVG map takes a full page in the PDF.
3641
+
3642
+ #### Theatre-of-the-mind maps
3643
+
3644
+ A `totm` map names its background with `data.bgImage` and its canvas size with
3645
+ `sohl.dimensions: [width, height]`. It builds a gridless Scene with token vision
3646
+ off. It needs no `sohl.pxPerGrid` or `data.scale`. The Markdown body remains the
3647
+ map's prose.
3612
3648
 
3613
3649
  ### type: place
3614
3650
 
@@ -3761,9 +3797,10 @@ data:
3761
3797
  on a land route — or a road on a ship route — is an error.
3762
3798
  - `leagues` — optional on a route; a distance, only where the note states one.
3763
3799
 
3764
- **Regions border; settlements are days apart.** A frontier is a fact about two
3765
- regions, and a journey is a fact about two places a traveller sets out from and
3766
- arrives at. A pair appears in `routes` once per `mode` — a port and its
3800
+ **A border is a shared frontier between areas.** Regions commonly state one;
3801
+ a settlement does so only when it actually shares a frontier with another
3802
+ place. Most settlements use `parents` for where they sit and `routes` for
3803
+ journeys between them. A pair appears in `routes` once per `mode` — a port and its
3767
3804
  neighbour may be three days by land and one by ship — and in `borders` once.
3768
3805
  **A place never borders its own parent or child**: containment is `parents`,
3769
3806
  and restating it as adjacency makes the two contradict each other.
@@ -3778,6 +3815,16 @@ neighbour that states a different bearing, a different `days` or a different
3778
3815
  tell which. Every `to` must resolve to a place, every value must come from its
3779
3816
  closed set, and each finding is located at the entry that states it.
3780
3817
 
3818
+ The terrain names form one shared registry in package-build. A route crossing
3819
+ a package boundary uses the same terrain and mode rules at both ends.
3820
+
3821
+ For every place that participates in a border or route, the map builder draws
3822
+ an itinerary from that place. A site page carries it as a linked SVG. A PDF
3823
+ book that includes the place gives its map a separate full page after the
3824
+ entry. A configured Foundry Scene pack carries a gridless map backed by a
3825
+ generated PNG, with Notes linked to importable place journals. Places with no
3826
+ relations create no itinerary output.
3827
+
3781
3828
  **A relation that crosses a package boundary is written in full at both ends.**
3782
3829
  An omitted `<package>` segment is the package being built, in whatever note the
3783
3830
  value is read from — a note of this package's own, or a record a consumer fetched
@@ -286,7 +286,7 @@ over one would teach people to stop reading the report.
286
286
  | `content-build markdown [paths..]` | it reports any markdownlint finding |
287
287
  | `content-build site` | any gate finding (no homepage, a wikilink in frontmatter, an address that cannot be derived, an unusable or unaddressable foreign manifest, a conflicting address) or any table or wikilink error while writing pages — the run stops at the first gate that fires |
288
288
  | `content-build pdf` | it could not build the book for a reason other than a deliberate no-op — never on a reported finding by itself |
289
- | `content-build map` | `--tree` reports an `error`-severity finding (a place with no parent, a parent that does not resolve, a cycle), a named place is not one, the GraphViz engine a drawing needs is not installed, or the corpus cannot be indexed — never on a warning |
289
+ | `content-build map` | `--tree` reports an `error`-severity finding (a place with no parent, a parent that does not resolve, a cycle), a named place is not one, the renderer fails, or the corpus cannot be indexed — never on a warning |
290
290
  | `content-build docs item-fields --check` | the generated page differs from what is committed |
291
291
  | `content-build package compile` | pack JSON generation reported any `error`-severity finding (checked, and the compile refused, before any pack is written), or a compiled pack fails its Scene/Level integrity check |
292
292
  | `content-build deps fetch` | the fetch itself throws — it emits no diagnostics |
@@ -370,9 +370,10 @@ position together:
370
370
  ```
371
371
 
372
372
  The accepted sizes are `auto`, `small`, `medium`, `large`, `xlarge`, and
373
- `full-width`. Omitting `size:` means `auto`. The build checks the name, but
374
- `size:` does not change the displayed dimensions on the website, in Foundry, or
375
- in the book. See [Images](content-format.md#images) for the rendering rules.
373
+ `full-width`. Omitting `size:` means `auto`, which uses the image's natural
374
+ dimensions within the available measure. The other names set maximum display
375
+ widths for the website, Foundry, and book. See
376
+ [Images](content-format.md#images) for the surface sizes and layout rules.
376
377
 
377
378
  ## Step 6 — check the tree
378
379
 
@@ -738,6 +739,29 @@ compiles to a page, so there is no document to address.
738
739
  The index is derived and disposable; it is written under `build/` and rebuilt
739
740
  from the tree every time.
740
741
 
742
+ ### Calendar files for a setting module
743
+
744
+ A content package with `lore` notes of `subType: calendar` can emit Foundry
745
+ calendar definitions after the content index is built:
746
+
747
+ ```console
748
+ $ npx package-build calendars --calendaria-version 1.4.2
749
+ ✅ 5 calendars emitted (10 files).
750
+ ```
751
+
752
+ The command writes `build/calendars/<shortcode>.json` and
753
+ `build/calendars/<shortcode>.calendaria.json` for each calendar. The first is a
754
+ calendar definition; the second wraps the same definition for import through
755
+ Calendaria's settings. The version argument names the Calendaria import format
756
+ the package targets. The envelope uses a fixed `exportedAt` sentinel because
757
+ these files are reproducible build artifacts. A package with no calendars gets
758
+ an empty `build/calendars/` directory.
759
+
760
+ To ship the files in a Foundry package, add
761
+ `{ from: build/calendars, to: calendars }` to `packageBuild.assets` and run the
762
+ calendar command before `package-build assets`. Running the command requires
763
+ the current content index; regenerate it after changing calendar notes.
764
+
741
765
  ## Step 11 — the release archive
742
766
 
743
767
  ```console
@@ -100,6 +100,7 @@ import { locateFrontmatterKey } from "./retired-fields.mjs";
100
100
  // present before this pass reads `data:` for anything else — see
101
101
  // `engine/being-age.mjs`.
102
102
  import { applyComputedBeingAge, presentAmongRecords } from "./being-age.mjs";
103
+ import { reckoningContext } from "./reckoning-markers.mjs";
103
104
 
104
105
  /**
105
106
  * The tallies one pass accumulates while walking the tree.
@@ -1078,6 +1079,7 @@ export class BasePackCompiler {
1078
1079
  // to have the body anyway. So the read is one this pass was already
1079
1080
  // making; what it no longer does is decide for itself which files to
1080
1081
  // make it over.
1082
+ const dates = reckoningContext({ notes: this.corpus.records });
1081
1083
  for (const record of this.corpus.records) {
1082
1084
  if (!isNoteRecord(record)) continue;
1083
1085
  const absPath = noteFile(this.contentBase, record);
@@ -1091,7 +1093,7 @@ export class BasePackCompiler {
1091
1093
  stats.skippedOther++;
1092
1094
  continue;
1093
1095
  }
1094
- applyComputedBeingAge(fm, this.worldPresent);
1096
+ applyComputedBeingAge(fm, this.worldPresent, dates);
1095
1097
  // The retired frontmatter fields, refused before `selects` so a
1096
1098
  // note is answered whichever pass would have claimed it — and
1097
1099
  // whatever the declared value says.
@@ -36,17 +36,10 @@
36
36
  *
37
37
  * A year subtracted from a year is wrong whenever the birthday has not yet
38
38
  * happened in the present's year — the common shape, not the exception. So
39
- * {@link computeAge} compares `born`'s month and day against `present`'s,
40
- * **ordinally**, and drops a year where born's has not yet arrived. That
41
- * ordinal comparison needs no month-length table: two dates written in one
42
- * calendar order the same way whether the comparison counts days-of-year
43
- * through the calendar's month lengths or simply reads `(month, day)` as a
44
- * pair, because a calendar's months keep the same lengths every year — this
45
- * package's calendars carry no leap rule (`calendar-notes.mjs`'s
46
- * `years.leapYear` is always `null`). So the comparison is calendar-agnostic
47
- * by construction, and correct for a twelve-month calendar of thirty-day
48
- * months, a ten-month calendar, or none at all — a bare `born` with no era
49
- * names no calendar to consult in the first place.
39
+ * {@link computeAge} compares the two dates' canonical day ordinals where an
40
+ * era marker supplies them. The birthday that still lies ahead in the current
41
+ * year does not count as a completed year. An unmarked slash date has no
42
+ * calendar to convert, so it uses its written month and day as its ordinal.
50
43
  *
51
44
  * ## `~` follows the convention {@link module:engine/note-dates} sets
52
45
  *
@@ -65,6 +58,7 @@
65
58
  import { worldInvariants } from "./calendar-notes.mjs";
66
59
  import { positionOfFrontmatterPath } from "./diagnostics.mjs";
67
60
  import { parseNoteDate } from "./note-dates.mjs";
61
+ import { reckoningContext } from "./reckoning-markers.mjs";
68
62
  // `born`'s retired spelling is still read during its retirement window — see
69
63
  // `docs/content-format.md`'s "`birthday:` is the retired spelling of `born:`"
70
64
  // — so a being on the old spelling still computes an age rather than reading
@@ -124,23 +118,21 @@ export function parseAgeMagnitude(raw) {
124
118
  * @returns {number|null} The age, or `null` where `born` is `unknown` or
125
119
  * absent, where no present is declared, or where either date fails to parse.
126
120
  */
127
- export function computeAge(bornRaw, presentRaw) {
121
+ export function computeAge(bornRaw, presentRaw, dateContext = {}) {
128
122
  if (bornRaw === undefined || bornRaw === null) return null;
129
123
  if (presentRaw === undefined || presentRaw === null) return null;
130
124
 
131
- const born = parseNoteDate(bornRaw, { allowUnknown: true }).date;
125
+ const born = parseNoteDate(bornRaw, { ...dateContext, allowUnknown: true }).date;
132
126
  if (!born || !born.known || born.canonicalYear == null) return null;
133
127
 
134
- const present = parseNoteDate(presentRaw, { allowUnknown: false }).date;
128
+ const present = parseNoteDate(presentRaw, { ...dateContext, allowUnknown: false }).date;
135
129
  if (!present || !present.known || present.canonicalYear == null) return null;
136
130
 
137
- // Ordinal position within the year, day of year or not: see the module
138
- // doc for why `(month, day)` alone orders correctly here. Missing month or
139
- // day reads as the year's first day — the earliest a birthday could fall —
140
- // so a `born` written to less precision than `present` never counts as
141
- // "not yet happened".
142
- const bornOrdinal = (born.month ?? 1) * 100 + (born.day ?? 1);
143
- const presentOrdinal = (present.month ?? 1) * 100 + (present.day ?? 1);
131
+ // A resolved marker supplies a canonical day. An unmarked slash date uses
132
+ // its written month and day. Missing parts read as the first day that the
133
+ // value can name.
134
+ const bornOrdinal = born.canonicalDay ?? (born.month ?? 1) * 100 + (born.day ?? 1);
135
+ const presentOrdinal = present.canonicalDay ?? (present.month ?? 1) * 100 + (present.day ?? 1);
144
136
  const notYetHappened = bornOrdinal > presentOrdinal;
145
137
 
146
138
  return present.canonicalYear - born.canonicalYear - (notYetHappened ? 1 : 0);
@@ -163,7 +155,7 @@ export function computeAge(bornRaw, presentRaw) {
163
155
  * `undefined`/`null` where it states none.
164
156
  * @returns {void}
165
157
  */
166
- export function applyComputedBeingAge(fm, presentRaw) {
158
+ export function applyComputedBeingAge(fm, presentRaw, dateContext = {}) {
167
159
  if (!isBeingType(fm?.type)) return;
168
160
  const data = dataOf(fm);
169
161
  if (!data) return;
@@ -179,7 +171,7 @@ export function applyComputedBeingAge(fm, presentRaw) {
179
171
  }
180
172
 
181
173
  const born = readAliasedField(fm, "born", { inData: true });
182
- const computed = computeAge(born, presentRaw);
174
+ const computed = computeAge(born, presentRaw, dateContext);
183
175
  if (computed !== null) data.age = computed;
184
176
  }
185
177
 
@@ -251,7 +243,7 @@ export function checkBeingAge(note, { index } = {}) {
251
243
  if (!index) return [];
252
244
 
253
245
  const present = worldInvariants(index).present;
254
- const computed = computeAge(bornRaw, present);
246
+ const computed = computeAge(bornRaw, present, reckoningContext(index));
255
247
  if (computed === null) return [];
256
248
 
257
249
  const authored = parseAgeMagnitude(ageRaw);