diff --git a/README.md b/README.md index ecd202f..531adae 100644 --- a/README.md +++ b/README.md @@ -180,7 +180,7 @@ The build runs all three guards before it writes anything: ``` -check-rules: OK — 87 rules, 16 files scanned, 3 aliases + 41 constant sets checked, 275 formulas verified +check-rules: OK — 87 rules, 17 files scanned, 3 aliases + 41 constant sets checked, 275 formulas verified check-kits: OK — 25 roles, 10 trades, 218 catalogue items, every kit key resolves, every posting can use what it carries, every loadout distinct check-lang: OK — en.json, 927 keys, no leaf/branch collisions check-behaviour: OK — 38 behavioural tests diff --git a/docs/REVIEW_LOG.md b/docs/REVIEW_LOG.md index 5fd2a31..fa5154d 100644 --- a/docs/REVIEW_LOG.md +++ b/docs/REVIEW_LOG.md @@ -5070,3 +5070,63 @@ species bonus." Checked afterwards that every localisation key still referenced by the engine or a template exists; the six apparent misses are Handlebars concatenation prefixes rather than lookups. + +## The player primer, and the journals finally look like the game + +Ten pages a player reads before session one, in their own compendium at OBSERVER +ownership: nine of setting and one cheat sheet. Plus the branding that all four +journals in the system had been going without. + +**The voice was the first thing to fix.** The draft called the organisation "the +Bureau" throughout. Everything else in the game — twelve rules pages, the starter, 927 +localisation strings — calls it *the department* or *the agency*. One name, and it is +not Bureau. + +**Where the draft was thin, it got filled rather than padded.** The character section +now carries the seven principles as an ordered list a player can actually work from; +"what it costs" gets the real Coherence band table rather than a paragraph describing +one, and says what the anchor in the kit is for; and the recruitment page separates +*what continuity means in practice* from *what it is not*, because "it is not a +superpower" is the single most important sentence for a new player and it was buried. + +**The cheat sheet's numbers are computed, not typed.** Hit points, Major Wound, damage +modifier, the defence penalties, the range ladder and the Coherence bands all come out +of `rules.mjs` and `lang/en.json` at build time. A hand-typed cheat sheet would have +been the sixth time this project shipped a number that disagreed with the code — and it +would have been the one the players were holding. It closes on six questions new +players actually ask, including "am I going to die?" answered honestly. + +### R-250. The design tokens were never on `:root` + +The journals rendered as Foundry's default parchment, and chasing why took four wrong +guesses that are worth recording, because each one looked right. + +1. **The hook name.** v14's journal sheet is an ApplicationV2: the hook is + `renderJournalEntrySheet`, not `renderJournalSheet`. +2. **`element` is the node.** On AppV2 it is the root element, not a jQuery wrapper. + Reaching for `[0]` silently returns the first *child* — which is how the first + version appeared to work, put the class on a header button, and changed nothing. +3. **The page container.** There is no `.journal-page-content` in v14. The prose sits + directly inside `article.journal-entry-page`. +4. **And then the real one.** With the class on the right element and the selector + matching, `font-size`, `line-height` and `padding` from my rule were all applying — + and every colour was not. `--rb-deep` resolved to nothing. The palette is declared + on `.ringbrp-sheet, .ringbrp-chat, .ringbrp-window` and **never on `:root`**, so a + journal carrying a new class inherited no tokens at all. Every `var()` silently + became nothing, which CSS treats as "unset" rather than as an error. + +The tell was that the *numeric* properties from the same rule worked while the coloured +ones did not. A rule that half-applies is not a specificity problem, and I spent three +attempts treating it as one — including bumping the system version on a cache theory +that was wrong. + +Adding `.ringbrp-journal` to the token block fixed all of it at once. + +**Now branded:** the primer, the generated rulebook, the starter's GM text and its +handouts. Scoped by a flag, so another module's journals in the same world are +untouched. The contents rail is the yellow-on-navy of the sheet tabs, read-aloud gets a +spot-colour margin rule, diagrams are monospaced in a bordered block, and the cheat +sheet is a grid of cards so it can be skimmed under pressure rather than read. + +Version bumped to 0.9.0 — which, incidentally, is what makes Foundry re-fetch the +stylesheet for anyone updating. diff --git a/package.json b/package.json index 5a06a4a..7e3276f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ringbrp", - "version": "0.8.0", - "description": "The Custodians \u2014 a Basic Roleplaying game of crossings, for Foundry VTT", + "version": "0.9.0", + "description": "The Custodians — a Basic Roleplaying game of crossings, for Foundry VTT", "main": "ringbrp.mjs", "scripts": { "build": "node tools/build-packs.mjs && node tools/update-readme.mjs", diff --git a/packs/armour/LOG b/packs/armour/LOG index b150376..64122b1 100644 --- a/packs/armour/LOG +++ b/packs/armour/LOG @@ -1,5 +1,5 @@ -2026/08/12-00:06:16.855265 1754e3000 Recovering log #3 -2026/08/12-00:06:16.855669 1754e3000 Level-0 table #5: started -2026/08/12-00:06:16.855836 1754e3000 Level-0 table #5: 4591 bytes OK -2026/08/12-00:06:16.856081 1754e3000 Delete type=0 #3 -2026/08/12-00:06:16.856115 1754e3000 Delete type=3 #2 +2026/08/12-10:15:09.925114 17200b000 Recovering log #3 +2026/08/12-10:15:09.925148 17200b000 Level-0 table #5: started +2026/08/12-10:15:09.925283 17200b000 Level-0 table #5: 4591 bytes OK +2026/08/12-10:15:09.925570 17200b000 Delete type=0 #3 +2026/08/12-10:15:09.925615 17200b000 Delete type=3 #2 diff --git a/packs/armour/LOG.old b/packs/armour/LOG.old index 76d96d2..359bf9b 100644 --- a/packs/armour/LOG.old +++ b/packs/armour/LOG.old @@ -1 +1 @@ -2026/08/12-00:05:23.690846 171b57000 Delete type=3 #1 +2026/08/12-10:14:43.782384 173eef000 Delete type=3 #1 diff --git a/packs/expedition/LOG b/packs/expedition/LOG index 2720361..d6d3173 100644 --- a/packs/expedition/LOG +++ b/packs/expedition/LOG @@ -1,5 +1,5 @@ -2026/08/12-00:06:16.873912 1754e3000 Recovering log #3 -2026/08/12-00:06:16.874024 1754e3000 Level-0 table #5: started -2026/08/12-00:06:16.874152 1754e3000 Level-0 table #5: 4129 bytes OK -2026/08/12-00:06:16.874423 1754e3000 Delete type=0 #3 -2026/08/12-00:06:16.874466 1754e3000 Delete type=3 #2 +2026/08/12-10:15:09.941304 173023000 Recovering log #3 +2026/08/12-10:15:09.941329 173023000 Level-0 table #5: started +2026/08/12-10:15:09.941465 173023000 Level-0 table #5: 4129 bytes OK +2026/08/12-10:15:09.941748 173023000 Delete type=0 #3 +2026/08/12-10:15:09.941781 173023000 Delete type=3 #2 diff --git a/packs/expedition/LOG.old b/packs/expedition/LOG.old index 0d1e32e..c189aa3 100644 --- a/packs/expedition/LOG.old +++ b/packs/expedition/LOG.old @@ -1 +1 @@ -2026/08/12-00:05:23.732633 17134b000 Delete type=3 #1 +2026/08/12-10:14:43.827491 1726cb000 Delete type=3 #1 diff --git a/packs/gear/LOG b/packs/gear/LOG index d212fb2..90124f5 100644 --- a/packs/gear/LOG +++ b/packs/gear/LOG @@ -1,5 +1,5 @@ -2026/08/12-00:06:16.857059 174cd7000 Recovering log #3 -2026/08/12-00:06:16.857573 174cd7000 Level-0 table #5: started -2026/08/12-00:06:16.857903 174cd7000 Level-0 table #5: 31614 bytes OK -2026/08/12-00:06:16.858145 174cd7000 Delete type=0 #3 -2026/08/12-00:06:16.858196 174cd7000 Delete type=3 #2 +2026/08/12-10:15:09.926306 173023000 Recovering log #3 +2026/08/12-10:15:09.926421 173023000 Level-0 table #5: started +2026/08/12-10:15:09.926814 173023000 Level-0 table #5: 31614 bytes OK +2026/08/12-10:15:09.927163 173023000 Delete type=0 #3 +2026/08/12-10:15:09.927207 173023000 Delete type=3 #2 diff --git a/packs/gear/LOG.old b/packs/gear/LOG.old index c8def97..c88d7d9 100644 --- a/packs/gear/LOG.old +++ b/packs/gear/LOG.old @@ -1 +1 @@ -2026/08/12-00:05:23.692998 170b3f000 Delete type=3 #1 +2026/08/12-10:14:43.784415 173eef000 Delete type=3 #1 diff --git a/packs/npcs/LOG b/packs/npcs/LOG index b7730b8..2b7df33 100644 --- a/packs/npcs/LOG +++ b/packs/npcs/LOG @@ -1,5 +1,5 @@ -2026/08/12-00:06:16.840426 174cd7000 Recovering log #3 -2026/08/12-00:06:16.842114 174cd7000 Level-0 table #5: started -2026/08/12-00:06:16.843749 174cd7000 Level-0 table #5: 225945 bytes OK -2026/08/12-00:06:16.844107 174cd7000 Delete type=0 #3 -2026/08/12-00:06:16.844201 174cd7000 Delete type=3 #2 +2026/08/12-10:15:09.914246 173023000 Recovering log #3 +2026/08/12-10:15:09.915233 173023000 Level-0 table #5: started +2026/08/12-10:15:09.916771 173023000 Level-0 table #5: 225945 bytes OK +2026/08/12-10:15:09.917162 173023000 Delete type=0 #3 +2026/08/12-10:15:09.917219 173023000 Delete type=3 #2 diff --git a/packs/npcs/LOG.old b/packs/npcs/LOG.old index 0fc6a18..1b98b67 100644 --- a/packs/npcs/LOG.old +++ b/packs/npcs/LOG.old @@ -1 +1 @@ -2026/08/12-00:05:23.708940 170b3f000 Delete type=3 #1 +2026/08/12-10:14:43.800859 1726cb000 Delete type=3 #1 diff --git a/packs/powers/LOG b/packs/powers/LOG index 5981400..9cbef14 100644 --- a/packs/powers/LOG +++ b/packs/powers/LOG @@ -1,5 +1,5 @@ -2026/08/12-00:06:16.850448 1754e3000 Recovering log #3 -2026/08/12-00:06:16.850937 1754e3000 Level-0 table #5: started -2026/08/12-00:06:16.851096 1754e3000 Level-0 table #5: 3295 bytes OK -2026/08/12-00:06:16.851406 1754e3000 Delete type=0 #3 -2026/08/12-00:06:16.851447 1754e3000 Delete type=3 #2 +2026/08/12-10:15:09.922245 17200b000 Recovering log #3 +2026/08/12-10:15:09.922265 17200b000 Level-0 table #5: started +2026/08/12-10:15:09.922421 17200b000 Level-0 table #5: 3295 bytes OK +2026/08/12-10:15:09.922681 17200b000 Delete type=0 #3 +2026/08/12-10:15:09.922713 17200b000 Delete type=3 #2 diff --git a/packs/powers/LOG.old b/packs/powers/LOG.old index 2107343..ced2e69 100644 --- a/packs/powers/LOG.old +++ b/packs/powers/LOG.old @@ -1 +1 @@ -2026/08/12-00:05:23.725493 171b57000 Delete type=3 #1 +2026/08/12-10:14:43.821081 1726cb000 Delete type=3 #1 diff --git a/packs/pregens/LOG b/packs/pregens/LOG index 50fb7ad..42ee2a9 100644 --- a/packs/pregens/LOG +++ b/packs/pregens/LOG @@ -1,5 +1,5 @@ -2026/08/12-00:06:16.836787 174cd7000 Recovering log #3 -2026/08/12-00:06:16.837779 174cd7000 Level-0 table #5: started -2026/08/12-00:06:16.838453 174cd7000 Level-0 table #5: 58792 bytes OK -2026/08/12-00:06:16.838760 174cd7000 Delete type=0 #3 -2026/08/12-00:06:16.838812 174cd7000 Delete type=3 #2 +2026/08/12-10:15:09.911544 173023000 Recovering log #3 +2026/08/12-10:15:09.911759 173023000 Level-0 table #5: started +2026/08/12-10:15:09.912426 173023000 Level-0 table #5: 58792 bytes OK +2026/08/12-10:15:09.912801 173023000 Delete type=0 #3 +2026/08/12-10:15:09.912846 173023000 Delete type=3 #2 diff --git a/packs/pregens/LOG.old b/packs/pregens/LOG.old index 967ffa0..2c2de79 100644 --- a/packs/pregens/LOG.old +++ b/packs/pregens/LOG.old @@ -1 +1 @@ -2026/08/12-00:05:23.699399 170b3f000 Delete type=3 #1 +2026/08/12-10:14:43.791044 173eef000 Delete type=3 #1 diff --git a/packs/primer/000005.ldb b/packs/primer/000005.ldb new file mode 100644 index 0000000..669e7b1 Binary files /dev/null and b/packs/primer/000005.ldb differ diff --git a/packs/primer/000006.log b/packs/primer/000006.log new file mode 100644 index 0000000..daa37bb Binary files /dev/null and b/packs/primer/000006.log differ diff --git a/packs/primer/CURRENT b/packs/primer/CURRENT new file mode 100644 index 0000000..cacca75 --- /dev/null +++ b/packs/primer/CURRENT @@ -0,0 +1 @@ +MANIFEST-000004 diff --git a/packs/primer/LOG b/packs/primer/LOG new file mode 100644 index 0000000..a50ac4a --- /dev/null +++ b/packs/primer/LOG @@ -0,0 +1,5 @@ +2026/08/12-10:15:09.929600 17382f000 Recovering log #3 +2026/08/12-10:15:09.929643 17382f000 Level-0 table #5: started +2026/08/12-10:15:09.929903 17382f000 Level-0 table #5: 21613 bytes OK +2026/08/12-10:15:09.930174 17382f000 Delete type=0 #3 +2026/08/12-10:15:09.930216 17382f000 Delete type=3 #2 diff --git a/packs/primer/LOG.old b/packs/primer/LOG.old new file mode 100644 index 0000000..6de6949 --- /dev/null +++ b/packs/primer/LOG.old @@ -0,0 +1 @@ +2026/08/12-10:14:43.817678 1726cb000 Delete type=3 #1 diff --git a/packs/primer/MANIFEST-000004 b/packs/primer/MANIFEST-000004 new file mode 100644 index 0000000..2ce4ec2 Binary files /dev/null and b/packs/primer/MANIFEST-000004 differ diff --git a/packs/rules/000005.ldb b/packs/rules/000005.ldb index e88a6f7..c8f9c7d 100644 Binary files a/packs/rules/000005.ldb and b/packs/rules/000005.ldb differ diff --git a/packs/rules/000006.log b/packs/rules/000006.log index 2d0501f..7b08998 100644 Binary files a/packs/rules/000006.log and b/packs/rules/000006.log differ diff --git a/packs/rules/LOG b/packs/rules/LOG index cb74d2a..e4d9c12 100644 --- a/packs/rules/LOG +++ b/packs/rules/LOG @@ -1,5 +1,5 @@ -2026/08/12-00:06:16.861054 174cd7000 Recovering log #3 -2026/08/12-00:06:16.861635 174cd7000 Level-0 table #5: started -2026/08/12-00:06:16.861956 174cd7000 Level-0 table #5: 33473 bytes OK -2026/08/12-00:06:16.862206 174cd7000 Delete type=0 #3 -2026/08/12-00:06:16.862237 174cd7000 Delete type=3 #2 +2026/08/12-10:15:09.930884 17200b000 Recovering log #3 +2026/08/12-10:15:09.930957 17200b000 Level-0 table #5: started +2026/08/12-10:15:09.931274 17200b000 Level-0 table #5: 33484 bytes OK +2026/08/12-10:15:09.931485 17200b000 Delete type=0 #3 +2026/08/12-10:15:09.931513 17200b000 Delete type=3 #2 diff --git a/packs/rules/LOG.old b/packs/rules/LOG.old index 3d704dc..9568411 100644 --- a/packs/rules/LOG.old +++ b/packs/rules/LOG.old @@ -1 +1 @@ -2026/08/12-00:05:23.729349 17134b000 Delete type=3 #1 +2026/08/12-10:14:43.825718 1726cb000 Delete type=3 #1 diff --git a/packs/rules/MANIFEST-000004 b/packs/rules/MANIFEST-000004 index f38776b..15f0607 100644 Binary files a/packs/rules/MANIFEST-000004 and b/packs/rules/MANIFEST-000004 differ diff --git a/packs/skills/LOG b/packs/skills/LOG index 7db8a54..d732c09 100644 --- a/packs/skills/LOG +++ b/packs/skills/LOG @@ -1,5 +1,5 @@ -2026/08/12-00:06:16.846928 1744cb000 Recovering log #3 -2026/08/12-00:06:16.847099 1744cb000 Level-0 table #5: started -2026/08/12-00:06:16.847302 1744cb000 Level-0 table #5: 7590 bytes OK -2026/08/12-00:06:16.847547 1744cb000 Delete type=0 #3 -2026/08/12-00:06:16.847579 1744cb000 Delete type=3 #2 +2026/08/12-10:15:09.920048 17200b000 Recovering log #3 +2026/08/12-10:15:09.920101 17200b000 Level-0 table #5: started +2026/08/12-10:15:09.920292 17200b000 Level-0 table #5: 7590 bytes OK +2026/08/12-10:15:09.920533 17200b000 Delete type=0 #3 +2026/08/12-10:15:09.920569 17200b000 Delete type=3 #2 diff --git a/packs/skills/LOG.old b/packs/skills/LOG.old index 98129dd..a1432a0 100644 --- a/packs/skills/LOG.old +++ b/packs/skills/LOG.old @@ -1 +1 @@ -2026/08/12-00:05:23.685076 170b3f000 Delete type=3 #1 +2026/08/12-10:14:43.775951 172ed7000 Delete type=3 #1 diff --git a/packs/slice/000005.ldb b/packs/slice/000005.ldb index 4851c77..ac4df01 100644 Binary files a/packs/slice/000005.ldb and b/packs/slice/000005.ldb differ diff --git a/packs/slice/000006.log b/packs/slice/000006.log index c7d9b16..c25ac75 100644 Binary files a/packs/slice/000006.log and b/packs/slice/000006.log differ diff --git a/packs/slice/LOG b/packs/slice/LOG index f2b4b09..671c6e4 100644 --- a/packs/slice/LOG +++ b/packs/slice/LOG @@ -1,5 +1,5 @@ -2026/08/12-00:06:16.870148 1744cb000 Recovering log #3 -2026/08/12-00:06:16.870587 1744cb000 Level-0 table #5: started -2026/08/12-00:06:16.870864 1744cb000 Level-0 table #5: 16892 bytes OK -2026/08/12-00:06:16.871211 1744cb000 Delete type=0 #3 -2026/08/12-00:06:16.871266 1744cb000 Delete type=3 #2 +2026/08/12-10:15:09.938481 172817000 Recovering log #3 +2026/08/12-10:15:09.938536 172817000 Level-0 table #5: started +2026/08/12-10:15:09.938839 172817000 Level-0 table #5: 16902 bytes OK +2026/08/12-10:15:09.939182 172817000 Delete type=0 #3 +2026/08/12-10:15:09.939235 172817000 Delete type=3 #2 diff --git a/packs/slice/LOG.old b/packs/slice/LOG.old index e1cb79d..e24df3f 100644 --- a/packs/slice/LOG.old +++ b/packs/slice/LOG.old @@ -1 +1 @@ -2026/08/12-00:05:23.723650 170b3f000 Delete type=3 #1 +2026/08/12-10:14:43.819552 1736e3000 Delete type=3 #1 diff --git a/packs/slice/MANIFEST-000004 b/packs/slice/MANIFEST-000004 index 0552c3a..0966a50 100644 Binary files a/packs/slice/MANIFEST-000004 and b/packs/slice/MANIFEST-000004 differ diff --git a/packs/starter/000005.ldb b/packs/starter/000005.ldb index 5c410fe..c0f9d9b 100644 Binary files a/packs/starter/000005.ldb and b/packs/starter/000005.ldb differ diff --git a/packs/starter/000006.log b/packs/starter/000006.log index 6c9b64b..aaf167c 100644 Binary files a/packs/starter/000006.log and b/packs/starter/000006.log differ diff --git a/packs/starter/LOG b/packs/starter/LOG index fa2bb45..23a3421 100644 --- a/packs/starter/LOG +++ b/packs/starter/LOG @@ -1,5 +1,5 @@ -2026/08/12-00:06:16.864877 1744cb000 Recovering log #3 -2026/08/12-00:06:16.865947 1744cb000 Level-0 table #5: started -2026/08/12-00:06:16.866787 1744cb000 Level-0 table #5: 82274 bytes OK -2026/08/12-00:06:16.867102 1744cb000 Delete type=0 #3 -2026/08/12-00:06:16.867176 1744cb000 Delete type=3 #2 +2026/08/12-10:15:09.933901 172817000 Recovering log #3 +2026/08/12-10:15:09.934300 172817000 Level-0 table #5: started +2026/08/12-10:15:09.935025 172817000 Level-0 table #5: 82299 bytes OK +2026/08/12-10:15:09.935279 172817000 Delete type=0 #3 +2026/08/12-10:15:09.935312 172817000 Delete type=3 #2 diff --git a/packs/starter/LOG.old b/packs/starter/LOG.old index 4e5bd27..b8e48b1 100644 --- a/packs/starter/LOG.old +++ b/packs/starter/LOG.old @@ -1 +1 @@ -2026/08/12-00:05:23.720012 170333000 Delete type=3 #1 +2026/08/12-10:14:43.811578 1736e3000 Delete type=3 #1 diff --git a/packs/starter/MANIFEST-000004 b/packs/starter/MANIFEST-000004 index b8a7dce..87687fc 100644 Binary files a/packs/starter/MANIFEST-000004 and b/packs/starter/MANIFEST-000004 differ diff --git a/packs/tables/LOG b/packs/tables/LOG index 8c1f830..c2e3d86 100644 --- a/packs/tables/LOG +++ b/packs/tables/LOG @@ -1,5 +1,5 @@ -2026/08/12-00:06:16.872191 173cbf000 Recovering log #3 -2026/08/12-00:06:16.872466 173cbf000 Level-0 table #5: started -2026/08/12-00:06:16.872601 173cbf000 Level-0 table #5: 3739 bytes OK -2026/08/12-00:06:16.872838 173cbf000 Delete type=0 #3 -2026/08/12-00:06:16.872870 173cbf000 Delete type=3 #2 +2026/08/12-10:15:09.940141 17200b000 Recovering log #3 +2026/08/12-10:15:09.940161 17200b000 Level-0 table #5: started +2026/08/12-10:15:09.940281 17200b000 Level-0 table #5: 3739 bytes OK +2026/08/12-10:15:09.940565 17200b000 Delete type=0 #3 +2026/08/12-10:15:09.940598 17200b000 Delete type=3 #2 diff --git a/packs/tables/LOG.old b/packs/tables/LOG.old index e0e34e8..869365b 100644 --- a/packs/tables/LOG.old +++ b/packs/tables/LOG.old @@ -1 +1 @@ -2026/08/12-00:05:23.713770 17134b000 Delete type=3 #1 +2026/08/12-10:14:43.805210 1726cb000 Delete type=3 #1 diff --git a/packs/talents/LOG b/packs/talents/LOG index 25ac20e..1889c72 100644 --- a/packs/talents/LOG +++ b/packs/talents/LOG @@ -1,5 +1,5 @@ -2026/08/12-00:06:16.848619 173cbf000 Recovering log #3 -2026/08/12-00:06:16.848765 173cbf000 Level-0 table #5: started -2026/08/12-00:06:16.848948 173cbf000 Level-0 table #5: 5770 bytes OK -2026/08/12-00:06:16.849247 173cbf000 Delete type=0 #3 -2026/08/12-00:06:16.849307 173cbf000 Delete type=3 #2 +2026/08/12-10:15:09.921227 173023000 Recovering log #3 +2026/08/12-10:15:09.921256 173023000 Level-0 table #5: started +2026/08/12-10:15:09.921405 173023000 Level-0 table #5: 5770 bytes OK +2026/08/12-10:15:09.921672 173023000 Delete type=0 #3 +2026/08/12-10:15:09.921715 173023000 Delete type=3 #2 diff --git a/packs/talents/LOG.old b/packs/talents/LOG.old index fb93ea3..7ba195c 100644 --- a/packs/talents/LOG.old +++ b/packs/talents/LOG.old @@ -1 +1 @@ -2026/08/12-00:05:23.687537 17134b000 Delete type=3 #1 +2026/08/12-10:14:43.779038 173eef000 Delete type=3 #1 diff --git a/packs/vehicles/LOG b/packs/vehicles/LOG index 0565ef4..247f394 100644 --- a/packs/vehicles/LOG +++ b/packs/vehicles/LOG @@ -1,5 +1,5 @@ -2026/08/12-00:06:16.859394 1754e3000 Recovering log #3 -2026/08/12-00:06:16.859688 1754e3000 Level-0 table #5: started -2026/08/12-00:06:16.859874 1754e3000 Level-0 table #5: 6939 bytes OK -2026/08/12-00:06:16.860159 1754e3000 Delete type=0 #3 -2026/08/12-00:06:16.860190 1754e3000 Delete type=3 #2 +2026/08/12-10:15:09.928207 172817000 Recovering log #3 +2026/08/12-10:15:09.928248 172817000 Level-0 table #5: started +2026/08/12-10:15:09.928429 172817000 Level-0 table #5: 6939 bytes OK +2026/08/12-10:15:09.928761 172817000 Delete type=0 #3 +2026/08/12-10:15:09.928810 172817000 Delete type=3 #2 diff --git a/packs/vehicles/LOG.old b/packs/vehicles/LOG.old index 876771f..8fbd016 100644 --- a/packs/vehicles/LOG.old +++ b/packs/vehicles/LOG.old @@ -1 +1 @@ -2026/08/12-00:05:23.694993 17134b000 Delete type=3 #1 +2026/08/12-10:14:43.786318 173eef000 Delete type=3 #1 diff --git a/packs/weapons/LOG b/packs/weapons/LOG index 7557d5c..7b5de13 100644 --- a/packs/weapons/LOG +++ b/packs/weapons/LOG @@ -1,5 +1,5 @@ -2026/08/12-00:06:16.852833 173cbf000 Recovering log #3 -2026/08/12-00:06:16.853536 173cbf000 Level-0 table #5: started -2026/08/12-00:06:16.853822 173cbf000 Level-0 table #5: 14767 bytes OK -2026/08/12-00:06:16.854096 173cbf000 Delete type=0 #3 -2026/08/12-00:06:16.854134 173cbf000 Delete type=3 #2 +2026/08/12-10:15:09.923574 17382f000 Recovering log #3 +2026/08/12-10:15:09.923739 17382f000 Level-0 table #5: started +2026/08/12-10:15:09.924049 17382f000 Level-0 table #5: 14767 bytes OK +2026/08/12-10:15:09.924348 17382f000 Delete type=0 #3 +2026/08/12-10:15:09.924394 17382f000 Delete type=3 #2 diff --git a/packs/weapons/LOG.old b/packs/weapons/LOG.old index e8dcdd7..2d24c97 100644 --- a/packs/weapons/LOG.old +++ b/packs/weapons/LOG.old @@ -1 +1 @@ -2026/08/12-00:05:23.689273 17134b000 Delete type=3 #1 +2026/08/12-10:14:43.780736 1726cb000 Delete type=3 #1 diff --git a/ringbrp.mjs b/ringbrp.mjs index 83a8c66..f3a0e47 100644 --- a/ringbrp.mjs +++ b/ringbrp.mjs @@ -6259,6 +6259,38 @@ async function migrateFittedGear() { return fixed; } +/** + * Dress this system's journals in the system's own paper. + * + * Twelve pages of generated rulebook, a player primer and a starter scenario were all + * rendering as Foundry's default parchment — the only part of the game that did not + * look like the game. Scoped by a flag rather than applied to every JournalSheet, so a + * module's journals in the same world are left exactly as they were. + */ +/** + * Dress this system's journals in the system's own paper. + * + * Twelve pages of generated rulebook, a ten-page player primer and a starter scenario + * were all rendering as Foundry's default parchment — the only part of the game that + * did not look like the game. Scoped by a flag rather than applied to every journal, so + * a module's entries in the same world are left exactly as they were. + * + * v14 note: the sheet is an ApplicationV2, so the hook is `renderJournalEntrySheet` and + * `element` is the root node itself rather than a jQuery wrapper. Reaching for `[0]` on + * it silently returns the first CHILD — which is how the first version of this appeared + * to work, added the class to a header button, and changed nothing. + */ +const rbBrandJournal = (doc, el) => { + const mine = doc?.flags?.ringbrp?.brand + || (typeof doc?.pack === "string" && doc.pack.startsWith("ringbrp.")) + || doc?.compendium?.metadata?.packageName === "ringbrp"; + const node = el instanceof HTMLElement ? el : (el?.[0] ?? null); + if (mine && node?.classList) node.classList.add("ringbrp-journal"); +}; +Hooks.on("renderJournalEntrySheet", (app, el) => rbBrandJournal(app?.document, el)); +Hooks.on("renderJournalEntryPageSheet", (app, el) => + rbBrandJournal(app?.document?.parent, el)); + Hooks.once("ready", async () => { console.log("RingBRP | ready"); if (!game.user.isGM) return; diff --git a/styles/ringbrp.css b/styles/ringbrp.css index 4b88fdd..9b3e015 100644 --- a/styles/ringbrp.css +++ b/styles/ringbrp.css @@ -36,6 +36,7 @@ .ringbrp-sheet, .ringbrp-chat, +.ringbrp-journal, .ringbrp-window { /* sampled from the 1984 Explorer Book cover */ --rb-void: #0c125b; @@ -423,6 +424,154 @@ .ringbrp-sheet .rb-equip:hover { color: var(--rb-white); } .ringbrp-sheet .rb-equip.on { color: var(--rb-yellow); } +/* ============================================================ + JOURNALS — the department's own paper + ============================================================ + Twelve pages of rulebook and a player primer were rendering as Foundry's default + parchment, which is the one place in the system that did not look like the system. + Scoped to .ringbrp-journal, applied by a hook to this system's entries only, so + other modules' journals are left alone. */ + +.ringbrp-journal .window-content, +.ringbrp-journal .journal-entry-content, +.ringbrp-journal .journal-entry-pages { background: var(--rb-deep); } + +/* The contents rail: this is how a player moves through ten pages. */ +.ringbrp-journal .journal-sidebar { + background: var(--rb-void); border-right: 1px solid var(--rb-line); +} +.ringbrp-journal .journal-sidebar .toc li.page { + font-family: var(--rb-display); font-size: 10px; letter-spacing: .05em; + text-transform: uppercase; color: var(--rb-muted); + border-bottom: 1px solid var(--rb-hairline); +} +.ringbrp-journal .journal-sidebar .toc li.page:hover { color: var(--rb-white); } +.ringbrp-journal .journal-sidebar .toc li.page.active, +.ringbrp-journal .journal-sidebar .toc li.page.readied { + color: var(--rb-void); background: var(--rb-yellow); +} +.ringbrp-journal .journal-header .title { + font-family: var(--rb-display); color: var(--rb-yellow); + background: transparent; border: none; +} + +/* A page. v14 puts the prose straight inside article.journal-entry-page — there is no + .journal-page-content wrapper, which is why the first version of this styled nothing. */ +.ringbrp-journal .journal-entry-page { + background: var(--rb-deep); + color: var(--rb-white); + font-family: var(--rb-body); + font-size: 14px; + line-height: 1.55; + padding: 4px 18px 28px; +} + +.ringbrp-journal .journal-entry-page h1 { + font-family: var(--rb-display); font-size: 19px; letter-spacing: .06em; + text-transform: uppercase; color: var(--rb-yellow); + border: none; border-bottom: 2px solid var(--rb-yellow); padding-bottom: 8px; + margin: 0 0 18px; +} +.ringbrp-journal .journal-entry-page h2 { + font-family: var(--rb-display); font-size: 13px; letter-spacing: .1em; + text-transform: uppercase; color: var(--rb-yellow); + border: none; border-bottom: 1px solid var(--rb-hairline); padding-bottom: 5px; + margin: 26px 0 12px; +} +.ringbrp-journal .journal-entry-page h3 { + font-family: var(--rb-display); font-size: 11px; letter-spacing: .09em; + text-transform: uppercase; color: var(--rb-white); border: none; margin: 20px 0 8px; +} +.ringbrp-journal .journal-entry-page p { margin: 0 0 12px; } +.ringbrp-journal .journal-entry-page strong { color: var(--rb-yellow); font-weight: 700; } +.ringbrp-journal .journal-entry-page em { color: var(--rb-muted); } +.ringbrp-journal .journal-entry-page a { color: var(--rb-yellow); } + +.ringbrp-journal .journal-entry-page ul, +.ringbrp-journal .journal-entry-page ol { margin: 0 0 14px; padding-left: 22px; } +.ringbrp-journal .journal-entry-page li { margin-bottom: 5px; } +.ringbrp-journal .journal-entry-page ul { list-style: none; padding-left: 16px; } +.ringbrp-journal .journal-entry-page ul > li:before { + content: "\25aa"; color: var(--rb-yellow); margin-left: -14px; margin-right: 8px; +} +.ringbrp-journal .journal-entry-page ol > li::marker { color: var(--rb-yellow); font-weight: 700; } + +.ringbrp-journal .journal-entry-page table { + width: 100%; border-collapse: collapse; margin: 0 0 16px; + background: var(--rb-void); border: 1px solid var(--rb-line); +} +.ringbrp-journal .journal-entry-page th { + font-family: var(--rb-display); font-size: 10px; letter-spacing: .08em; + text-transform: uppercase; text-align: left; color: var(--rb-void); + background: var(--rb-yellow); padding: 6px 9px; border: none; +} +.ringbrp-journal .journal-entry-page td { + padding: 6px 9px; border-top: 1px solid var(--rb-hairline); vertical-align: top; +} +.ringbrp-journal .journal-entry-page tbody tr:nth-child(even) { background: var(--rb-panel); } + +/* Read-aloud: the one thing a GM says in somebody else's voice. */ +.ringbrp-journal .rb-readaloud { + margin: 0 0 18px; padding: 12px 16px; + background: var(--rb-void); + border: none; border-left: 3px solid var(--rb-yellow); + color: var(--rb-white); font-size: 15px; line-height: 1.6; +} +.ringbrp-journal .rb-readaloud strong, +.ringbrp-journal .rb-readaloud em { color: var(--rb-yellow); } + +/* Maps and diagrams are typed, not drawn. */ +.ringbrp-journal .rb-map { + background: var(--rb-void); border: 1px solid var(--rb-line); + color: var(--rb-muted); font-family: var(--rb-mono); font-size: 11px; + line-height: 1.45; padding: 12px 14px; margin: 0 0 16px; + overflow-x: auto; white-space: pre; +} + +/* A departmental form: the stamped block a primer page opens with. */ +.ringbrp-journal .rb-form { + background: var(--rb-void); border: 1px solid var(--rb-line); + border-top: 3px solid var(--rb-yellow); + padding: 12px 14px; margin: 0 0 20px; + font-family: var(--rb-mono); font-size: 11px; line-height: 1.7; + color: var(--rb-muted); text-transform: uppercase; letter-spacing: .04em; +} +.ringbrp-journal .rb-form b { color: var(--rb-white); font-weight: 700; } +.ringbrp-journal .rb-stamp { + display: inline-block; border: 2px solid var(--rb-danger); color: var(--rb-danger); + font-family: var(--rb-display); font-size: 11px; letter-spacing: .14em; + text-transform: uppercase; padding: 4px 10px; transform: rotate(-2deg); + margin: 4px 0 14px; +} +/* The line a page lands on. */ +.ringbrp-journal .rb-closer { + font-family: var(--rb-display); font-size: 13px; letter-spacing: .05em; + color: var(--rb-yellow); text-align: center; + border-top: 1px solid var(--rb-hairline); padding-top: 16px; margin-top: 22px; +} +.ringbrp-journal .rb-two { columns: 260px 2; column-gap: 26px; } +.ringbrp-journal .rb-two > * { break-inside: avoid; } + +/* The cheat sheet: a card per rule, so it can be skimmed under pressure. */ +.ringbrp-journal .rb-cards { + display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 12px; margin: 0 0 18px; +} +.ringbrp-journal .rb-card { + background: var(--rb-void); border: 1px solid var(--rb-line); + border-left: 3px solid var(--rb-yellow); padding: 10px 12px; +} +.ringbrp-journal .rb-card h4 { + font-family: var(--rb-display); font-size: 10px; letter-spacing: .09em; + text-transform: uppercase; color: var(--rb-yellow); margin: 0 0 6px; border: none; +} +.ringbrp-journal .rb-card p { margin: 0 0 6px; font-size: 13px; } +.ringbrp-journal .rb-card p:last-child { margin-bottom: 0; } +.ringbrp-journal .rb-big-number { + font-family: var(--rb-display); font-size: 22px; color: var(--rb-white); + line-height: 1.15; display: block; margin-bottom: 6px; +} + /* ============================================================ Shields — armour with a switch, so the switch has to be visible ============================================================ */ diff --git a/system.json b/system.json index b94b754..1278139 100644 --- a/system.json +++ b/system.json @@ -2,7 +2,7 @@ "id": "ringbrp", "title": "The Custodians", "description": "A Basic Roleplaying game of agents who cross the crossing to contain what should not be there — and who come back a little less from here each time.", - "version": "0.8.0", + "version": "0.9.0", "compatibility": { "minimum": 13, "verified": "14.364" @@ -113,6 +113,13 @@ "type": "Item", "system": "ringbrp" }, + { + "name": "primer", + "label": "The Custodians · Read this first (players)", + "path": "packs/primer", + "type": "JournalEntry", + "system": "ringbrp" + }, { "name": "rules", "label": "The Custodians · Rules", diff --git a/tools/build-packs.mjs b/tools/build-packs.mjs index 3c40e0e..8b757e4 100644 --- a/tools/build-packs.mjs +++ b/tools/build-packs.mjs @@ -622,6 +622,7 @@ await writePack("armour", ARMOURS.map(a => { // Handouts are for handing out. A GM who has to change ownership on seven pages // before the session starts will simply read them aloud instead. handouts.doc.ownership = { default: 2 }; + handouts.doc.flags = { ringbrp: { brand: true } }; for (const p of handouts.pages) p.ownership = { default: 2 }; const advId = idFor("adventure:black_platform"); @@ -656,10 +657,25 @@ await writePack("armour", ARMOURS.map(a => { + `${S.STARTER_PAGES.length} GM pages, ${S.STARTER_HANDOUTS.length} handouts`); } +// The player primer. Its own pack, OBSERVER by default, because it is the one thing +// in the system written to be handed to players before anybody has read a rule. +{ + const { PRIMER_PAGES } = await import("./primer-text.mjs"); + const j = journalDoc("The Custodians — Read this first", "primer", PRIMER_PAGES); + j.doc.ownership = { default: 2 }; + j.doc.flags = { ringbrp: { brand: true } }; + for (const pg of j.pages) pg.ownership = { default: 2 }; + const entries = [[`!journal!${j.id}`, j.doc]]; + for (const pg of j.pages) entries.push([`!journal.pages!${j.id}.${pg._id}`, pg]); + await writePack("primer", entries); + console.log(` primer ${PRIMER_PAGES.length} pages, player-visible`); +} + // the slice journal { const { SLICE_PAGES } = await import("./slice-text.mjs"); const j = journalDoc("The Ilfracombe Crossing", "slice", SLICE_PAGES); + j.doc.flags = { ringbrp: { brand: true } }; const entries = [[`!journal!${j.id}`, j.doc]]; for (const p of j.pages) entries.push([`!journal.pages!${j.id}.${p._id}`, p]); await writePack("slice", entries); @@ -693,6 +709,7 @@ await writePack("armour", ARMOURS.map(a => { { const { RULES_PAGES } = await import("./rules-text.mjs"); const j = journalDoc("The Custodians — Rules", "rules", RULES_PAGES); + j.doc.flags = { ringbrp: { brand: true } }; const entries = [[`!journal!${j.id}`, j.doc]]; for (const p of j.pages) entries.push([`!journal.pages!${j.id}.${p._id}`, p]); await writePack("rules", entries); diff --git a/tools/primer-text.mjs b/tools/primer-text.mjs new file mode 100644 index 0000000..4b3c9dd --- /dev/null +++ b/tools/primer-text.mjs @@ -0,0 +1,502 @@ +/** + * THE PLAYER PRIMER — what a new player reads before session one. + * + * Written to be handed out, not studied. Eight pages of setting and one cheat sheet, + * and the cheat sheet's numbers are computed from rules.mjs at build time so the card + * a player holds cannot say one thing while the engine does another. That failure has + * happened five times in this project; a hand-typed cheat sheet would have been the + * sixth, and it would have been the one the players were looking at. + * + * On the voice: the source draft called the organisation "the Bureau". Everything else + * in the game — twelve rules pages, the starter, every localisation string — calls it + * "the department" or "the agency", so that is what it is called here. One name. + */ +// TRADE_BANDS and INDUCTION are register data, not rules — they live in postings.mjs. +import { TRADE_BANDS, INDUCTION } from "../postings.mjs"; +import { + hitPointsFor, majorWoundFor, damageModifierFor, defencePenaltyFor, + COHERENCE_BANDS, RANGE_LADDER, CIRCUMSTANCE +} from "../rules.mjs"; +import { readFile } from "node:fs/promises"; + +// Labels come from the language file, the same way the rulebook generator reads them, +// so the primer prints what the sheet prints rather than a prettified lang key. +const LANG = JSON.parse(await readFile(new URL("../lang/en.json", import.meta.url), "utf8")); +const L = key => LANG[key] ?? key; + +const P = s => `

${s}

`; +const H = (n, s) => `${s}`; +const UL = xs => ``; +const OL = xs => `
    ${xs.map(x => `
  1. ${x}
  2. `).join("")}
`; +const TABLE = (head, rows) => + `${head.map(h => ``).join("")}` + + rows.map(r => `${r.map(c => ``).join("")}`).join("") + + `
${h}
${c}
`; +const READ = s => `
${s}
`; +const FORM = s => `
${s}
`; +const STAMP = s => `
${s}
`; +const CLOSER = s => `

${s}

`; +const CARDS = xs => `
${xs.map(([t, b]) => + `

${t}

${b}
`).join("")}
`; +const TWO = s => `
${s}
`; +const pc = n => `${n > 0 ? "+" : ""}${n}%`; + +/* Worked from the same functions the sheet uses, so the primer cannot drift. */ +const EG = { con: 12, siz: 13, str: 12 }; +const egHp = hitPointsFor(EG.con, EG.siz); +const egMw = majorWoundFor(egHp); +const egDm = damageModifierFor(EG.str + EG.siz); + +export const PRIMER_PAGES = [ + +/* ---------------------------------------------------------------- */ +{ name: "1 · You live in the 2020s", content: + FORM("Departmental Custody Service · Induction pack
" + + "Issued to: new entrant · Retain for the duration of service
" + + "Classification: restricted — not for disclosure outside the department") + + P("You have a phone, an email address, a National Insurance number and opinions " + + "about the quickest way through a self-service checkout. You have had jobs, " + + "relationships, debts, disappointments and plans. You know the world is troubled " + + "and frequently absurd, but you have never had reason to believe it is " + + "unreal.") + + P("You are not a wizard, a superhero or a chosen anything. You " + + "cannot see ghosts merely because they are ghosts. Bullets hurt you. A locked door " + + "stays locked until somebody opens it. If something with too many teeth reaches " + + "you, your advantages are whatever you brought, whoever is standing beside you, " + + "and the chance that somebody made a plan.") + + P("Until recently you thought that was all there was.") + + CLOSER("Then the department found you.") +}, + +/* ---------------------------------------------------------------- */ +{ name: "2 · The problem", content: + P("Reality is not as single or as steady as people assume.") + + P("There are other versions of the world — places where a different decision was " + + "taken, where history bent another way, where life developed under other " + + "conditions, or where the rules themselves settled into a shape impossible here. " + + "Most stay separate. Occasionally two of them touch.") + + P("The department calls that contact a crossing.") + + P("A crossing might be a door that opens into the wrong building, a road that " + + "arrives in a town on no map, or a train running between stations in different " + + "histories. It might be no wider than the gap under a filing cabinet. It might " + + "last three seconds or stand open for generations.") + + H(2, "Crossings do not merely join places. They create contradictions.") + + TWO(UL([ + "Two people hold the same birth certificate.", + "A house stands on ground where its foundations were never laid.", + "A dead soldier comes back to a family that remembers burying him.", + "Rain obeying somebody else's physics falls on a field in Yorkshire.", + "A person goes on living while banks, government systems and their own relatives stop being able to remember them." + ])) + + H(2, "The record") + + P("The department's oldest surviving explanation is that " + + "reality keeps a record of what belongs where.") + + P("It is not a literal ledger held by a god or a machine. It is the closest language " + + "anyone has found for how the world behaves. People, places and events have an " + + "identity: they are recognised by other people, by physical history, and by the " + + "thousand systems around them.") + + P("A crossing introduces something the record cannot file. Sometimes " + + "reality rejects the intrusion. Sometimes it alters the intruder. Sometimes it " + + "alters everything around the intruder until the contradiction goes away. None of " + + "those is reliably safe.") + + P("The department uses the theory of crossings because it is useful, not because it " + + "has been proved. A door into another history is plainly a crossing. A dead woman " + + "appearing in mirrors, a family curse that outlives the family, or one dream shared " + + "by six strangers is less easily explained.") + + P("The old hands avoid the argument. Whatever a thing is, the questions are " + + "the same:") + + READ("What does it do? What makes it worse? What is holding it here? And how do we " + + "stop it reaching anybody else?") +}, + +/* ---------------------------------------------------------------- */ +{ name: "3 · Why the department exists", content: + P("Because crossings have always happened.") + + P("Its offices today are fluorescent-lit government rooms with grey carpet, " + + "computers and secure cabinets. Its predecessors worked out of royal " + + "observatories, monasteries, counting houses, naval offices and rooms behind " + + "temples. Some job titles are modern. Others have lasted centuries because nobody " + + "has ever been confident enough to say the old work has stopped being necessary.") + + P("Governments have renamed it, split it, hidden it inside other departments and " + + "occasionally tried to abolish it. The work carried on under every arrangement. " + + "An organisation can be closed. The next impossible doorway still has to be " + + "dealt with.") + + P("Nobody knows when it began. The archive holds Roman inventories of objects that " + + "cast shadows the wrong way, medieval payments to wardens of roads that did not " + + "always reach the same county, and eighteenth-century orders about a lighthouse " + + "visible only to ships already lost. The oldest documents refer to earlier " + + "custodians as though they were ancient.") + + P("So the department has reached an institutional conclusion: the job did not " + + "begin with us, and it will not end with us.") + + H(2, "What it cannot do") + + UL(["Permanently repair reality.", + "Find every crossing before somebody walks through one.", + "Destroy everything that arrives — many arrivals are people, and destruction can make a contradiction worse.", + "Tell the public, which would create panic, exploitation and, worst of all, millions of new records confirming that the anomaly exists."]) + + H(2, "What it does instead") + + UL(["Finds crossings and charts where they go.", + "Contains things that should not be here.", + "Recovers people who have gone somewhere else.", + "Negotiates with whoever lives on the other side.", + "Closes routes that have become dangerous.", + "Leaves alone the arrangements that are safer undisturbed.", + "Alters, suppresses or completes records until reality has one answer it can accept.", + "Comes back tomorrow, when yesterday's solution starts to fail."]) + + CLOSER("This is not a war that can be won. It is maintenance, on an impossible scale.") +}, + +/* ---------------------------------------------------------------- */ +{ name: "4 · What is out there", content: + P("The things the department deals with are not one species and share no single " + + "origin. Some are hostile. Some are animals behaving normally somewhere humans are " + + "unprepared. Some are people defending their homes. Some are processes with no " + + "intention at all. A few are mistakes left by the department's own earlier attempts " + + "at containment.") + + P("Agents use rough, unofficial categories. None of them is in the manual.") + + H(2, "Things that crossed") + + P("A predator following prey through a temporary opening. A growth spreading through " + + "concrete because concrete does not exist where it evolved. A person who crossed by " + + "accident and is dangerous only because our world is poison to them — or because " + + "their being here is erasing somebody else.") + + H(2, "Echoes, duplicates and revenants") + + P("A crossing sometimes repeats a person instead of moving them. Two adults with the " + + "same childhood, the same fingerprints and twenty years of different memories. A " + + "dead porter who walks the same platform every night. A rescue beacon transmitted " + + "by the teammate they watched die.") + + P("The department does not assume a duplicate is false or a revenant " + + "mindless. Working out what something is — and whether it can be talked " + + "to — is the job.") + + H(2, "Wrong places") + + P("A government office with a thirteenth floor reachable only during fire alarms. A " + + "village on two maps in different counties that physically moves each winter. A " + + "service tunnel that gets longer every time somebody is sent to measure it.") + + P("Wrong places are the dangerous ones, because people relax indoors. " + + "A room with a kettle, a desk and a fire-exit sign goes on feeling ordinary long " + + "after it has stopped obeying the rules.") + + H(2, "Objects and procedures") + + P("A brass date stamp that makes a document true from the date applied. A field " + + "medical unit that heals a wound by moving it to the nearest blood relative. A road " + + "sign that creates the place printed on it.") + + P("Much of this is transposed materiel. Agents are trained not to " + + "operate unfamiliar equipment merely because it has a switch.") + + H(2, "People with an interest") + + P("The department is not the only one that knows. Collectors buy things that should " + + "never be catalogued. Contractors called Cleaners remove evidence " + + "for employers whose names rarely survive an investigation. Rival offices bury " + + "incidents to protect budgets and careers. People living beside a stable crossing " + + "may depend on it for food, medicine or faith.") + + P("They use guns, vehicles, surveillance, lawyers, political weight and correct " + + "paperwork. They are usually more immediately dangerous than the " + + "anomaly.") + + H(2, "The dead who do not leave") + + P("The department does not use ghost as a classification, because the word " + + "assumes an answer. A haunting might be a surviving person, an impression left on a " + + "place, somebody displaced from a history where they did not die, or a pattern that " + + "has learned to do what the dead are expected to do.") + + P("The distinction matters. A surviving person can be reasoned with. " + + "A recording can only repeat. A predatory pattern may imitate grief because grief " + + "brings people close enough.") + + H(2, "Curses and inherited conditions") + + P("A curse is rarely a beam of hostile energy. It is usually a rule the world " + + "has started enforcing. The eldest child stays in the house. Every owner of " + + "a certain ring dies before their spouse. Nobody appointed keeper of a particular " + + "bridge leaves the county alive.") + + P("These last centuries because generations of observance have made them part of the " + + "record. Breaking the ritual may end it, release whatever it was holding down, or " + + "move the obligation onto whoever interfered.") + + P("This is where well-meaning agents do the most damage. An old practice can look " + + "cruel or absurd and still be the only thing standing under something worse.") + + H(2, "Ideas that travel") + + P("Not everything needs a body. A dream can spread between sleepers. A proof can " + + "describe a geometry that starts appearing around anyone who understands it. An " + + "image can gain detail with every copy until it shows the room the copy is being " + + "viewed in.") + + P("The department files these as cognitive anomalies, which does " + + "nothing whatever to make them safer. It is also why publicity is feared: attention " + + "does not only expose an anomaly, it can feed one, define one, or hold one steady.") + + H(2, "The things behind the stories") + + P("There is no official category for gods, because an official category implies " + + "somebody knows what is being categorised. Agents say mythos case " + + "for incidents that repeat the same shapes across cultures that never met: the " + + "sleeper under the water, the king whose face must not be seen, the black star, the " + + "messenger who arrives before the disaster.") + + P("Some intelligences appear to work on a scale that makes individual people hard to " + + "notice. Their smallest movements can produce religions, extinctions or changes in " + + "physical law.") + + P("They are not necessarily evil. A storm is not evil. That is little " + + "comfort to anyone underneath one.") + + P("These are not solved by learning a creature's statistics and shooting it. Agents " + + "break the ritual, clear the site, spoil the alignment, close the route, deal with " + + "an intermediary, or stop the thing becoming present enough to act. Sometimes the " + + "most that can be done is to reinforce an old containment and leave instructions " + + "for the next generation.") + + H(2, "UNRESOLVED") + + STAMP("Unresolved") + + P("Some files defeat every model. There are rooms that cannot be entered twice by the " + + "same route, photographs of agents who never existed, and one recurring Wednesday. " + + "There are procedures whose purpose has been forgotten, because every attempt to " + + "write the purpose down removes another person from the archive.") + + P("Unresolved does not mean harmless. It means somebody found a " + + "procedure that limits the damage without understanding why it works. A Night " + + "Warden checks the seal. A Registry Officer confirms the names. Somebody replaces " + + "the bowl of milk, winds the silent clock, reads the number over a secure line, or " + + "leaves one chair empty.") + + CLOSER("Some of these duties are older than the explanation for them.") +}, + +/* ---------------------------------------------------------------- */ +{ name: "5 · Why you were recruited", content: + P("Most people cannot work near crossings for long.") + + P("They look past contradictions. Their memories quietly update to match the latest " + + "version of events. They are violently sick, or they dissociate, or they accept an " + + "impossibility as though it had always been ordinary. This is usually protective. " + + "Human minds are very good at continuing to function.") + + P("Yours does something slightly different.") + + P("The department calls it continuity, and does not know what it is.") + + P("It runs in families without following any clean pattern of inheritance. Certain " + + "genetic markers turn up more often among agents and none of them predicts it. " + + "Older texts describe an unusually well-seated soul. A twentieth-century " + + "programme argued that consciousness leaves a persistent informational pattern. " + + "Another department maintains that continuity is simply social: people who are " + + "strongly remembered resist being rewritten.") + + P("All of those have been tested. None of them explains everybody.") + + H(2, "What it means in practice") + + UL(["When reality changes its account, you are more likely to remember the previous one.", + "You can go through a controlled crossing and come back recognisably yourself.", + "You can look at two mutually exclusive facts and know the contradiction is real."]) + + H(2, "What it is not") + + P("It is not a superpower. You are not immune to crossings. You can " + + "still be hurt, altered, duplicated, forgotten or killed. Continuity does not make " + + "you stronger, faster or braver, and it does not tell you what an anomaly is or how " + + "to stop it.") + + P("It means that when the impossible happens you have a better chance of noticing — " + + "and of carrying the memory home.") + + CLOSER("For the department, that is enough to build a career on.") +}, + +/* ---------------------------------------------------------------- */ +{ name: "6 · Who your character is", content: + P("A modern adult whose life was recognisable before the department turned up in it.") + + P("They might have been a doctor, an engineer, an academic, a police officer, a " + + "construction worker, an IT specialist, a scientist, a linguist, an entertainer or " + + "a criminal. They might already have served inside the department in one of its " + + "field postings. Either way, what they are good at comes from work, " + + "education and hard practice — not from destiny.") + + P("A doctor is valuable because they are a doctor. A burglar understands locks " + + "because they spent years opening them. A builder can tell when a building is " + + "carrying weight wrongly. A police officer can control a dangerous scene. An " + + "entertainer can hold a room that does not want to listen.") + + H(2, "The department adds six weeks") + + P("Induction: how to recognise a crossing, set a cordon, handle evidence, fire the " + + "issued weapon, survive first contact, and complete the forms that will very likely " + + "matter more than the gun.") + + P("Six weeks does not make anybody an expert. A surgeon recruited " + + "last year is still a far better surgeon than they are a Custodian. Your prior " + + "career sits at " + + `${TRADE_BANDS.core[0]}–${TRADE_BANDS.core[1]}%; what the department taught ` + + `you starts at ${INDUCTION.bandByTier.probationary[0]}–` + + `${INDUCTION.bandByTier.probationary[1]}% and climbs with service.`) + + P("That gap is deliberate, and it is often frightening.") + + H(2, "Why you stayed") + + P("You may have come willingly. You may have survived an incident and learned too " + + "much to be let go. You may have been offered a choice whose other option was not " + + "really a choice. The department might have lost a conviction, saved a relative, " + + "bought out a contract, or simply been the first organisation to believe you.") + + P("You were not picked for being fearless. You were picked because " + + "you can remember, because you have a useful human skill, and because there are " + + "never enough people for the work waiting tomorrow morning.") + + H(2, "Seven principles for building one") + + OL(["You are from the 2020s. Your references, assumptions, work and relationships are contemporary.", + "You had a life before this. Decide what you did well enough that the department needed it.", + "You are not superhuman. Training, teamwork, equipment and preparation keep you alive.", + "Continuity is not immunity. You notice changes better than most people. The changes can still happen to you.", + "You have a reason to stay. Duty, debt, curiosity, guilt, protection, belonging — or knowing nobody else will do it.", + "You need the team. No posting or trade holds every answer. The lowest-ranked person in the room is often the only real expert in it.", + "Decide what anchors you. Name a person, an object, a place or a habit that reminds you who you are when the records disagree."]) + + CLOSER("The department does not need heroes. It needs people who will turn up, look " + + "straight at the impossible, and still remember what they came to do.") +}, + +/* ---------------------------------------------------------------- */ +{ name: "7 · What the job is actually like", content: + P("An operation usually starts with an incomplete briefing.") + + P("Something has happened. A person has vanished without leaving. An impossible " + + "object has been recovered. An old containment has failed. Somebody has opened a " + + "route that was supposed to stay shut. You get an objective, equipment, a cover " + + "story, and as much authority as the department is willing to admit it sent.") + + P("Then you find out what the briefing got wrong.") + + P("You may go through records, talk to witnesses, cross into another version of the " + + "world, walk unfamiliar ground, make contact with whoever lives there, recover " + + "people or evidence, and contain what is immediately dangerous. Sometimes that is a " + + "negotiation. Sometimes it is engineering. Sometimes it is holding a corridor with a " + + "rifle and a shield while somebody else repairs the only way home.") + + P("Fighting is dangerous and often unavoidable, and killing is rarely the " + + "whole answer. Shoot the creature and the crossing that produced it is " + + "still open. Beat the Cleaner and the evidence they came for is still lying there. " + + "Cases close when there is an answer reality can live with.") + + H(2, "Then you come home") + + P("You are debriefed. Reports are compared. Equipment goes back, except what was " + + "lost, broken, or has started behaving differently. The department decides what " + + "officially happened. You get rest, if the schedule allows it.") + + CLOSER("Sooner or later, another file arrives.") +}, + +/* ---------------------------------------------------------------- */ +{ name: "8 · What it costs", content: + P("Crossing leaves a mark.") + + P("The department measures your connection to your own reality as " + + "Coherence. It is not sanity and it is not morality. " + + "It is how firmly the world still recognises that you belong here.") + + P("Watching a paradox, using transposed equipment, breaking the physics of another " + + "place, meeting another version of yourself, or coming back badly all cost you some.") + + H(2, "How it goes") + + TABLE(["Coherence", "Where you are", "What it costs you"], + COHERENCE_BANDS.map((b, i) => { + const hi = i === 0 ? 10 : COHERENCE_BANDS[i - 1].min - 1; + const eff = [b.all ? `${pc(b.all)} to everything` : "", + b.comm ? `${pc(b.comm)} to talking to people` : ""] + .filter(Boolean).join(", ") || "nothing yet"; + return [b.min === hi ? `${b.min}` : `${hi}–${b.min}`, + `${L(b.label)}`, eff]; + })) + + P("Early on it is small. People mishear your name. Automatic doors hesitate. You look " + + "very slightly misplaced in photographs. Later, records disagree about where you " + + "work, people you love remember conversations differently, and the way home gets " + + "harder to aim.") + + H(2, "Getting it back") + + P("Debriefing, time at home, and contact with people or things that remember you " + + "correctly. Recovery is slow, and some of it never entirely comes back.") + + P("This is what the anchor in your kit is for. It is one object that remembers " + + "you as you were, and it is the cheapest medicine the department issues.") + + H(2, "The Quiet Arts") + + P("Very occasionally a crossing leaves somebody able to commit a small impossibility " + + "on purpose. The department calls these the Quiet Arts.") + + P("They are not gifts. They cannot be chosen at recruitment. They always cost more of " + + "your connection to the world. Most Custodians never get one.") + + P("Those who do are not superheroes. They are injured people who have learned " + + "how to press on the injury.") +}, + +/* ---------------------------------------------------------------- */ +{ name: "9 · The whole game, in one page", content: + P("You play ordinary adults in the modern world, pulled into an old and quiet " + + "government department because you stay yourselves when reality goes wrong.") + + P("You investigate crossings, recover the lost, face hostile people and impossible " + + "things, negotiate with strangers, contain what can be contained, and decide what " + + "the official record will say happened. You carry modern tools, professional " + + "experience, an issued weapon and incomplete information.") + + P("You can fight. Violence alone rarely closes a case. You can come home, and every " + + "journey makes home a little less certain.") + + P("The department has done this for longer than any living government, and expects to " + + "carry on long after you.") + + READ("There will always be another file.
" + + "There will always be something here that should not be.
" + + "Your job is to go there and sort it out.") +}, + +/* ---------------------------------------------------------------- * + * The cheat sheet. Numbers computed, not typed. + * ---------------------------------------------------------------- */ +{ name: "10 · Cheat sheet — everything you need at the table", content: + FORM("Departmental Custody Service · Field aide-memoire
" + + "One page. Keep it beside you. You do not need to read anything else to play.") + + + H(2, "The only rule you must know") + + P("Roll 1d100. Get equal to or under your skill." + + "That is the whole engine. Everything else changes that number, or is a consequence " + + "of the result.") + + TABLE(["Roll", "Result", "What it means"], [ + ["01 to a twentieth of your skill", "Critical", "the best version of what you wanted"], + ["up to a fifth of your skill", "Special", "notably better than you asked for"], + ["up to your skill", "Success", "you do it"], + ["over your skill", "Failure", "you do not"], + ["the top 5% of the range", "Fumble", "you do it wrong, and it costs"] + ]) + + P("The sheet prints those four numbers next to every skill, so you " + + "never work them out. A skill of 60 criticals on 01–03, specials to 12, succeeds to " + + "60 and fumbles on 98–00.") + + P("Two rules beat everything: 96–99 never succeed, however good you " + + "are, and 00 is always a fumble. No skill is ever certain — and " + + "none is ever hopeless either, because the floor is 1%.") + + + H(2, "The three things that change your number") + + CARDS([ + ["1 · Difficulty", + P("Set by the GM before you roll.") + + P("Easy doubles your skill. Average leaves it " + + "alone. Difficult halves it.")], + ["2 · Circumstance", + P("The situation, up to " + pc(Math.max(...CIRCUMSTANCE.map(c => c.mod))) + + " in your favour or " + pc(Math.min(...CIRCUMSTANCE.map(c => c.mod))) + " against.") + + P("At most two apply — the best good one and the worst bad one — " + + "and they cap at ±40%.")], + ["3 · Your condition", + P("Wounds, what you are carrying and how far you have drifted.") + + P("All three are already on your sheet and applied for you.")] + ]) + + P("Difficulty first, then everything else. A skill can never drop below 1%.") + + + H(2, "Fighting") + + CARDS([ + ["Who goes first", + P("Reaction, re-rolled every round including the first. Your " + + "printed number plus a d6. Highest acts first.")], + ["Attacking", + P("Roll the weapon's skill. The dialog asks you the questions — range, cover, " + + "light, how you are firing — and does the arithmetic.")], + ["Defending", + P("The first defence each round is free. Each one after that is " + + pc(defencePenaltyFor(1)) + ", then " + pc(defencePenaltyFor(2)) + ".")], + ["Defences are graded", + P("A defence stops a blow of its own quality or worse. Against something better it " + + "only drags it down a step.") + + P("Only a critical stops a critical.")] + ]) + + TABLE(["Range", "To hit"], + RANGE_LADDER.map(r => [L(`RINGBRP.Range.${r.id}`), + r.mod === null ? "cannot be hit at all" : pc(r.mod)])) + + + H(2, "Getting hurt") + + CARDS([ + ["Hit points", + P("" + egHp + "" + + "(CON + SIZ) ÷ 2, rounded up. The example agent has CON " + EG.con + " and SIZ " + + EG.siz + ".")], + ["Major Wound", + P("" + egMw + "" + + "Half your hit points in one blow. Roll on the table and " + + "something specific and lasting happens.")], + ["Damage modifier", + P("" + egDm + "" + + "STR + SIZ on a ladder. Added to what you hit with.")], + ["Zero hit points", + P("You are dying, not dead. A clock starts, equal to your Major " + + "Wound number, and somebody can still reach you.")] + ]) + + P("Damage comes off your total and the part of you it hit. Armour " + + "only counts where it actually covers — a helmet does nothing for your legs.") + + + H(2, "Coherence") + + P("Ten down to zero. How firmly the world still agrees you belong here. It is not " + + "sanity. Spend it, lose it to paradoxes, get it back slowly at home.") + + P("Watch the band, not the number — the sheet tells you which one you " + + "are in and what it is costing you.") + + + H(2, "Six things new players ask") + + TABLE(["", ""], [ + ["Can I just shoot it?", + "Often, yes. It rarely closes the case — the crossing that produced it is still open."], + ["What do I roll?", + "Say what you are doing. The GM names the skill. It is on your sheet with its four numbers beside it."], + ["I failed. Now what?", + "The story continues. A failure costs time, resources, position or clarity — it does not stop the case."], + ["Can I help somebody?", + "Yes, and you should. Assisting is a real action, and most specialist scenes want a second pair of hands."], + ["My skill is 5%. Why?", + "You are untrained at it. That is honest, not a mistake. Ask whoever is trained."], + ["Am I going to die?", + "You can. You are much more likely to come home changed, out of pocket, and holding a form you do not want to sign."] + ]) + + CLOSER("Roll under. Ask questions. Take the shield.") +} +];