diff --git a/README.md b/README.md index 96e5ea1..2aef32c 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,8 @@ changing underneath a running server. | **Bestiary** | 21 statblocks across folklore, horror and the far side | | **Eras** | modern, antique, future, anomalous — the agency is old and never abolishes anything | | **Icons** | 292, all generated from `tools/icons.mjs`; no external art | -| **Compendia** | pregens, NPCs, skills, weapons, armour, gear, vehicles, powers, tables, rules, a starter case | +| **Starter scenario** | **PAPER WINDOW**, a six-player introductory case, importable as a single Adventure | +| **Compendia** | pregens, NPCs, skills, weapons, armour, gear, vehicles, powers, tables, rules, the starter | | **Rules journal** | 12 pages, generated from `rules.mjs` so the book cannot drift from the code | @@ -179,7 +180,7 @@ The build runs all three guards before it writes anything: ``` -check-rules: OK — 87 rules, 15 files scanned, 3 aliases + 41 constant sets checked, 275 formulas verified +check-rules: OK — 87 rules, 16 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, 926 keys, no leaf/branch collisions check-behaviour: OK — 28 behavioural tests diff --git a/docs/REVIEW_LOG.md b/docs/REVIEW_LOG.md index 951dec3..bfd2793 100644 --- a/docs/REVIEW_LOG.md +++ b/docs/REVIEW_LOG.md @@ -4901,3 +4901,43 @@ should sit *above* executable evidence rather than instead of it. Every numbered that can be reproduced without a browser now has a test. Those that cannot — anything inside the generators, the sheets or the contact engine — are named in the test file's header as out of its reach, so the suite does not look more complete than it is. + +## PAPER WINDOW — the starter scenario, as a Foundry Adventure + +Codex's six-player introductory case is now in the system as an importable scenario +module. **Adventure**, not a journal: a scenario that arrives as nine loose documents a +GM has to assemble is a scenario that gets read rather than run. + +`ringbrp.starter` contains one Adventure holding: + +* the **GM scenario** in twelve pages, in running order; +* the **seven player handouts** as their own journal at ownership OBSERVER, so they can + be revealed one at a time without the GM changing permissions on seven pages before + the session — the GM journal stays hidden; +* the **six pregens**, built through the same `buildActor` path the shipped pregens + use, so they arrive with full skill lists, kit, hit points and linked tokens; +* the **cast** — Evelyn Vale, both Lorna Vales, Gareth Moss and the Cancellation Man — + as statblocks a GM can drag onto a scene; +* the **Borrowed Authority** as a real credential item with scope, Trust 2 and its three + red flags, rather than a paragraph in the brief; +* the **case file** already at Containment 8, Resources 8, Clearance 6 with its four + phases loaded at the scenario's stated difficulties and modifiers. + +**The pregens are fixed, not rolled.** Ratings sit at the middle of each band rather +than being generated, because a pregen that varies between installs is not a pregen and +six tables running the starter should see the same six agents. Verified after a live +import: Nia carries the team's highest Tradecraft at 53 as the text requires, Callum's +riot shield is **slung** so raising it is still the player's decision, and the three +probationers show the trade/induction split the scenario is built to teach — Elin +Medicine 58 against an induction of 26, Owen Electronics 58, Sennen Sleight of Hand 58. + +The expansion from a posting or trade into a full sheet reuses the register and the +induction rule rather than restating either, so the pregens cannot drift from the +generator: change `TRADE_BANDS` and the starter's doctor changes with it. + +**Verified by importing it rather than by building it.** The pack builds, the Adventure +is well formed, the import completes with no errors, the case roster resolves through +stable keys after import (raw ids do not survive one), the handouts come in +player-visible and the GM text does not, and **phase one runs straight out of the box** — +Nia leading the cordon on Tradecraft 53. Then everything imported was deleted again, +which the pack does not notice. diff --git a/packs/armour/000003.log b/packs/armour/000003.log deleted file mode 100644 index 9b6d27d..0000000 Binary files a/packs/armour/000003.log and /dev/null differ diff --git a/packs/armour/000005.ldb b/packs/armour/000005.ldb new file mode 100644 index 0000000..c982555 Binary files /dev/null and b/packs/armour/000005.ldb differ diff --git a/packs/armour/000006.log b/packs/armour/000006.log new file mode 100644 index 0000000..1a1bb27 Binary files /dev/null and b/packs/armour/000006.log differ diff --git a/packs/armour/CURRENT b/packs/armour/CURRENT index 1a84852..cacca75 100644 --- a/packs/armour/CURRENT +++ b/packs/armour/CURRENT @@ -1 +1 @@ -MANIFEST-000002 +MANIFEST-000004 diff --git a/packs/armour/LOG b/packs/armour/LOG index da64381..ff37f12 100644 --- a/packs/armour/LOG +++ b/packs/armour/LOG @@ -1 +1,5 @@ -2026/08/11-22:56:53.172229 172e3b000 Delete type=3 #1 +2026/08/11-23:23:08.314417 1742cf000 Recovering log #3 +2026/08/11-23:23:08.314570 1742cf000 Level-0 table #5: started +2026/08/11-23:23:08.314789 1742cf000 Level-0 table #5: 4591 bytes OK +2026/08/11-23:23:08.315089 1742cf000 Delete type=0 #3 +2026/08/11-23:23:08.315117 1742cf000 Delete type=3 #2 diff --git a/packs/armour/LOG.old b/packs/armour/LOG.old new file mode 100644 index 0000000..507c1cb --- /dev/null +++ b/packs/armour/LOG.old @@ -0,0 +1 @@ +2026/08/11-23:22:30.827615 17117f000 Delete type=3 #1 diff --git a/packs/armour/MANIFEST-000002 b/packs/armour/MANIFEST-000002 deleted file mode 100644 index bbbc585..0000000 Binary files a/packs/armour/MANIFEST-000002 and /dev/null differ diff --git a/packs/armour/MANIFEST-000004 b/packs/armour/MANIFEST-000004 new file mode 100644 index 0000000..9c05997 Binary files /dev/null and b/packs/armour/MANIFEST-000004 differ diff --git a/packs/expedition/000003.log b/packs/expedition/000003.log deleted file mode 100644 index 263295b..0000000 Binary files a/packs/expedition/000003.log and /dev/null differ diff --git a/packs/expedition/000005.ldb b/packs/expedition/000005.ldb new file mode 100644 index 0000000..4ff460d Binary files /dev/null and b/packs/expedition/000005.ldb differ diff --git a/packs/expedition/000006.log b/packs/expedition/000006.log new file mode 100644 index 0000000..268b187 Binary files /dev/null and b/packs/expedition/000006.log differ diff --git a/packs/expedition/CURRENT b/packs/expedition/CURRENT index 1a84852..cacca75 100644 --- a/packs/expedition/CURRENT +++ b/packs/expedition/CURRENT @@ -1 +1 @@ -MANIFEST-000002 +MANIFEST-000004 diff --git a/packs/expedition/LOG b/packs/expedition/LOG index 32e7b2f..c83cac5 100644 --- a/packs/expedition/LOG +++ b/packs/expedition/LOG @@ -1 +1,5 @@ -2026/08/11-22:56:53.204356 173647000 Delete type=3 #1 +2026/08/11-23:23:08.332681 174adb000 Recovering log #3 +2026/08/11-23:23:08.332779 174adb000 Level-0 table #5: started +2026/08/11-23:23:08.332918 174adb000 Level-0 table #5: 4129 bytes OK +2026/08/11-23:23:08.333190 174adb000 Delete type=0 #3 +2026/08/11-23:23:08.333236 174adb000 Delete type=3 #2 diff --git a/packs/expedition/LOG.old b/packs/expedition/LOG.old new file mode 100644 index 0000000..a6bf6d4 --- /dev/null +++ b/packs/expedition/LOG.old @@ -0,0 +1 @@ +2026/08/11-23:22:30.870148 17198b000 Delete type=3 #1 diff --git a/packs/expedition/MANIFEST-000002 b/packs/expedition/MANIFEST-000002 deleted file mode 100644 index bbbc585..0000000 Binary files a/packs/expedition/MANIFEST-000002 and /dev/null differ diff --git a/packs/expedition/MANIFEST-000004 b/packs/expedition/MANIFEST-000004 new file mode 100644 index 0000000..47fbcd4 Binary files /dev/null and b/packs/expedition/MANIFEST-000004 differ diff --git a/packs/gear/000003.log b/packs/gear/000003.log deleted file mode 100644 index 629ba6d..0000000 Binary files a/packs/gear/000003.log and /dev/null differ diff --git a/packs/gear/000005.ldb b/packs/gear/000005.ldb new file mode 100644 index 0000000..d9bac03 Binary files /dev/null and b/packs/gear/000005.ldb differ diff --git a/packs/gear/000006.log b/packs/gear/000006.log new file mode 100644 index 0000000..d1deb9b Binary files /dev/null and b/packs/gear/000006.log differ diff --git a/packs/gear/CURRENT b/packs/gear/CURRENT index 1a84852..cacca75 100644 --- a/packs/gear/CURRENT +++ b/packs/gear/CURRENT @@ -1 +1 @@ -MANIFEST-000002 +MANIFEST-000004 diff --git a/packs/gear/LOG b/packs/gear/LOG index a4c7f1f..e3e88c3 100644 --- a/packs/gear/LOG +++ b/packs/gear/LOG @@ -1 +1,5 @@ -2026/08/11-22:56:53.174127 172e3b000 Delete type=3 #1 +2026/08/11-23:23:08.316151 174adb000 Recovering log #3 +2026/08/11-23:23:08.316320 174adb000 Level-0 table #5: started +2026/08/11-23:23:08.316628 174adb000 Level-0 table #5: 31614 bytes OK +2026/08/11-23:23:08.316965 174adb000 Delete type=0 #3 +2026/08/11-23:23:08.317018 174adb000 Delete type=3 #2 diff --git a/packs/gear/LOG.old b/packs/gear/LOG.old new file mode 100644 index 0000000..61717b2 --- /dev/null +++ b/packs/gear/LOG.old @@ -0,0 +1 @@ +2026/08/11-23:22:30.829491 170973000 Delete type=3 #1 diff --git a/packs/gear/MANIFEST-000002 b/packs/gear/MANIFEST-000002 deleted file mode 100644 index bbbc585..0000000 Binary files a/packs/gear/MANIFEST-000002 and /dev/null differ diff --git a/packs/gear/MANIFEST-000004 b/packs/gear/MANIFEST-000004 new file mode 100644 index 0000000..f7cd73c Binary files /dev/null and b/packs/gear/MANIFEST-000004 differ diff --git a/packs/npcs/000003.log b/packs/npcs/000003.log deleted file mode 100644 index 09b2bf6..0000000 Binary files a/packs/npcs/000003.log and /dev/null differ diff --git a/packs/npcs/000005.ldb b/packs/npcs/000005.ldb new file mode 100644 index 0000000..29f69de Binary files /dev/null and b/packs/npcs/000005.ldb differ diff --git a/packs/npcs/000006.log b/packs/npcs/000006.log new file mode 100644 index 0000000..ac2ef10 Binary files /dev/null and b/packs/npcs/000006.log differ diff --git a/packs/npcs/CURRENT b/packs/npcs/CURRENT index 1a84852..cacca75 100644 --- a/packs/npcs/CURRENT +++ b/packs/npcs/CURRENT @@ -1 +1 @@ -MANIFEST-000002 +MANIFEST-000004 diff --git a/packs/npcs/LOG b/packs/npcs/LOG index f2ab953..aefc8a1 100644 --- a/packs/npcs/LOG +++ b/packs/npcs/LOG @@ -1 +1,5 @@ -2026/08/11-22:56:53.190267 173e53000 Delete type=3 #1 +2026/08/11-23:23:08.298312 174adb000 Recovering log #3 +2026/08/11-23:23:08.300367 174adb000 Level-0 table #5: started +2026/08/11-23:23:08.301985 174adb000 Level-0 table #5: 225945 bytes OK +2026/08/11-23:23:08.302352 174adb000 Delete type=0 #3 +2026/08/11-23:23:08.302416 174adb000 Delete type=3 #2 diff --git a/packs/npcs/LOG.old b/packs/npcs/LOG.old new file mode 100644 index 0000000..78abf31 --- /dev/null +++ b/packs/npcs/LOG.old @@ -0,0 +1 @@ +2026/08/11-23:22:30.845061 17198b000 Delete type=3 #1 diff --git a/packs/npcs/MANIFEST-000002 b/packs/npcs/MANIFEST-000002 deleted file mode 100644 index bbbc585..0000000 Binary files a/packs/npcs/MANIFEST-000002 and /dev/null differ diff --git a/packs/npcs/MANIFEST-000004 b/packs/npcs/MANIFEST-000004 new file mode 100644 index 0000000..116e469 Binary files /dev/null and b/packs/npcs/MANIFEST-000004 differ diff --git a/packs/powers/000003.log b/packs/powers/000003.log deleted file mode 100644 index d723043..0000000 Binary files a/packs/powers/000003.log and /dev/null differ diff --git a/packs/powers/000005.ldb b/packs/powers/000005.ldb new file mode 100644 index 0000000..7f540c6 Binary files /dev/null and b/packs/powers/000005.ldb differ diff --git a/packs/powers/000006.log b/packs/powers/000006.log new file mode 100644 index 0000000..43e306f Binary files /dev/null and b/packs/powers/000006.log differ diff --git a/packs/powers/CURRENT b/packs/powers/CURRENT index 1a84852..cacca75 100644 --- a/packs/powers/CURRENT +++ b/packs/powers/CURRENT @@ -1 +1 @@ -MANIFEST-000002 +MANIFEST-000004 diff --git a/packs/powers/LOG b/packs/powers/LOG index ee5c32b..0a51aaa 100644 --- a/packs/powers/LOG +++ b/packs/powers/LOG @@ -1 +1,5 @@ -2026/08/11-22:56:53.198465 172e3b000 Delete type=3 #1 +2026/08/11-23:23:08.309911 1742cf000 Recovering log #3 +2026/08/11-23:23:08.310322 1742cf000 Level-0 table #5: started +2026/08/11-23:23:08.310473 1742cf000 Level-0 table #5: 3295 bytes OK +2026/08/11-23:23:08.310750 1742cf000 Delete type=0 #3 +2026/08/11-23:23:08.310794 1742cf000 Delete type=3 #2 diff --git a/packs/powers/LOG.old b/packs/powers/LOG.old new file mode 100644 index 0000000..0099045 --- /dev/null +++ b/packs/powers/LOG.old @@ -0,0 +1 @@ +2026/08/11-23:22:30.862697 17198b000 Delete type=3 #1 diff --git a/packs/powers/MANIFEST-000002 b/packs/powers/MANIFEST-000002 deleted file mode 100644 index bbbc585..0000000 Binary files a/packs/powers/MANIFEST-000002 and /dev/null differ diff --git a/packs/powers/MANIFEST-000004 b/packs/powers/MANIFEST-000004 new file mode 100644 index 0000000..20de082 Binary files /dev/null and b/packs/powers/MANIFEST-000004 differ diff --git a/packs/pregens/000003.log b/packs/pregens/000003.log deleted file mode 100644 index 712264f..0000000 Binary files a/packs/pregens/000003.log and /dev/null differ diff --git a/packs/pregens/000005.ldb b/packs/pregens/000005.ldb new file mode 100644 index 0000000..ef0a53e Binary files /dev/null and b/packs/pregens/000005.ldb differ diff --git a/packs/pregens/000006.log b/packs/pregens/000006.log new file mode 100644 index 0000000..ddbae86 Binary files /dev/null and b/packs/pregens/000006.log differ diff --git a/packs/pregens/CURRENT b/packs/pregens/CURRENT index 1a84852..cacca75 100644 --- a/packs/pregens/CURRENT +++ b/packs/pregens/CURRENT @@ -1 +1 @@ -MANIFEST-000002 +MANIFEST-000004 diff --git a/packs/pregens/LOG b/packs/pregens/LOG index 4910f8d..83ac41a 100644 --- a/packs/pregens/LOG +++ b/packs/pregens/LOG @@ -1 +1,5 @@ -2026/08/11-22:56:53.180591 173647000 Delete type=3 #1 +2026/08/11-23:23:08.294604 174adb000 Recovering log #3 +2026/08/11-23:23:08.295663 174adb000 Level-0 table #5: started +2026/08/11-23:23:08.296163 174adb000 Level-0 table #5: 58792 bytes OK +2026/08/11-23:23:08.296520 174adb000 Delete type=0 #3 +2026/08/11-23:23:08.296610 174adb000 Delete type=3 #2 diff --git a/packs/pregens/LOG.old b/packs/pregens/LOG.old new file mode 100644 index 0000000..46f8755 --- /dev/null +++ b/packs/pregens/LOG.old @@ -0,0 +1 @@ +2026/08/11-23:22:30.835678 17198b000 Delete type=3 #1 diff --git a/packs/pregens/MANIFEST-000002 b/packs/pregens/MANIFEST-000002 deleted file mode 100644 index bbbc585..0000000 Binary files a/packs/pregens/MANIFEST-000002 and /dev/null differ diff --git a/packs/pregens/MANIFEST-000004 b/packs/pregens/MANIFEST-000004 new file mode 100644 index 0000000..fb2adca Binary files /dev/null and b/packs/pregens/MANIFEST-000004 differ diff --git a/packs/rules/000003.log b/packs/rules/000003.log deleted file mode 100644 index 7bc4ddf..0000000 Binary files a/packs/rules/000003.log and /dev/null differ diff --git a/packs/rules/000005.ldb b/packs/rules/000005.ldb new file mode 100644 index 0000000..aff8ef7 Binary files /dev/null and b/packs/rules/000005.ldb differ diff --git a/packs/rules/000006.log b/packs/rules/000006.log new file mode 100644 index 0000000..f3c7033 Binary files /dev/null and b/packs/rules/000006.log differ diff --git a/packs/rules/CURRENT b/packs/rules/CURRENT index 1a84852..cacca75 100644 --- a/packs/rules/CURRENT +++ b/packs/rules/CURRENT @@ -1 +1 @@ -MANIFEST-000002 +MANIFEST-000004 diff --git a/packs/rules/LOG b/packs/rules/LOG index 516c822..f872108 100644 --- a/packs/rules/LOG +++ b/packs/rules/LOG @@ -1 +1,5 @@ -2026/08/11-22:56:53.202660 17465f000 Delete type=3 #1 +2026/08/11-23:23:08.320015 175af3000 Recovering log #3 +2026/08/11-23:23:08.320173 175af3000 Level-0 table #5: started +2026/08/11-23:23:08.320528 175af3000 Level-0 table #5: 33474 bytes OK +2026/08/11-23:23:08.320923 175af3000 Delete type=0 #3 +2026/08/11-23:23:08.320978 175af3000 Delete type=3 #2 diff --git a/packs/rules/LOG.old b/packs/rules/LOG.old new file mode 100644 index 0000000..06c8aac --- /dev/null +++ b/packs/rules/LOG.old @@ -0,0 +1 @@ +2026/08/11-23:22:30.868156 17117f000 Delete type=3 #1 diff --git a/packs/rules/MANIFEST-000002 b/packs/rules/MANIFEST-000002 deleted file mode 100644 index bbbc585..0000000 Binary files a/packs/rules/MANIFEST-000002 and /dev/null differ diff --git a/packs/rules/MANIFEST-000004 b/packs/rules/MANIFEST-000004 new file mode 100644 index 0000000..a877353 Binary files /dev/null and b/packs/rules/MANIFEST-000004 differ diff --git a/packs/skills/000003.log b/packs/skills/000003.log deleted file mode 100644 index 2a55440..0000000 Binary files a/packs/skills/000003.log and /dev/null differ diff --git a/packs/skills/000005.ldb b/packs/skills/000005.ldb new file mode 100644 index 0000000..eb948ea Binary files /dev/null and b/packs/skills/000005.ldb differ diff --git a/packs/skills/000006.log b/packs/skills/000006.log new file mode 100644 index 0000000..1989ee1 Binary files /dev/null and b/packs/skills/000006.log differ diff --git a/packs/skills/CURRENT b/packs/skills/CURRENT index 1a84852..cacca75 100644 --- a/packs/skills/CURRENT +++ b/packs/skills/CURRENT @@ -1 +1 @@ -MANIFEST-000002 +MANIFEST-000004 diff --git a/packs/skills/LOG b/packs/skills/LOG index e1f4f28..7f8b75b 100644 --- a/packs/skills/LOG +++ b/packs/skills/LOG @@ -1 +1,5 @@ -2026/08/11-22:56:53.165752 173647000 Delete type=3 #1 +2026/08/11-23:23:08.305494 1742cf000 Recovering log #3 +2026/08/11-23:23:08.305911 1742cf000 Level-0 table #5: started +2026/08/11-23:23:08.306128 1742cf000 Level-0 table #5: 7590 bytes OK +2026/08/11-23:23:08.306405 1742cf000 Delete type=0 #3 +2026/08/11-23:23:08.306467 1742cf000 Delete type=3 #2 diff --git a/packs/skills/LOG.old b/packs/skills/LOG.old new file mode 100644 index 0000000..1d811f3 --- /dev/null +++ b/packs/skills/LOG.old @@ -0,0 +1 @@ +2026/08/11-23:22:30.820649 172197000 Delete type=3 #1 diff --git a/packs/skills/MANIFEST-000002 b/packs/skills/MANIFEST-000002 deleted file mode 100644 index bbbc585..0000000 Binary files a/packs/skills/MANIFEST-000002 and /dev/null differ diff --git a/packs/skills/MANIFEST-000004 b/packs/skills/MANIFEST-000004 new file mode 100644 index 0000000..90efb97 Binary files /dev/null and b/packs/skills/MANIFEST-000004 differ diff --git a/packs/slice/000003.log b/packs/slice/000003.log deleted file mode 100644 index 4a86fe9..0000000 Binary files a/packs/slice/000003.log and /dev/null differ diff --git a/packs/slice/000005.ldb b/packs/slice/000005.ldb new file mode 100644 index 0000000..4851c77 Binary files /dev/null and b/packs/slice/000005.ldb differ diff --git a/packs/slice/000006.log b/packs/slice/000006.log new file mode 100644 index 0000000..c7d9b16 Binary files /dev/null and b/packs/slice/000006.log differ diff --git a/packs/slice/CURRENT b/packs/slice/CURRENT index 1a84852..cacca75 100644 --- a/packs/slice/CURRENT +++ b/packs/slice/CURRENT @@ -1 +1 @@ -MANIFEST-000002 +MANIFEST-000004 diff --git a/packs/slice/LOG b/packs/slice/LOG index b02a7ad..fc3115d 100644 --- a/packs/slice/LOG +++ b/packs/slice/LOG @@ -1 +1,5 @@ -2026/08/11-22:56:53.196772 17465f000 Delete type=3 #1 +2026/08/11-23:23:08.328983 1752e7000 Recovering log #3 +2026/08/11-23:23:08.329262 1752e7000 Level-0 table #5: started +2026/08/11-23:23:08.329521 1752e7000 Level-0 table #5: 16892 bytes OK +2026/08/11-23:23:08.329805 1752e7000 Delete type=0 #3 +2026/08/11-23:23:08.329838 1752e7000 Delete type=3 #2 diff --git a/packs/slice/LOG.old b/packs/slice/LOG.old new file mode 100644 index 0000000..82360c8 --- /dev/null +++ b/packs/slice/LOG.old @@ -0,0 +1 @@ +2026/08/11-23:22:30.860796 17117f000 Delete type=3 #1 diff --git a/packs/slice/MANIFEST-000002 b/packs/slice/MANIFEST-000002 deleted file mode 100644 index bbbc585..0000000 Binary files a/packs/slice/MANIFEST-000002 and /dev/null differ diff --git a/packs/slice/MANIFEST-000004 b/packs/slice/MANIFEST-000004 new file mode 100644 index 0000000..0552c3a Binary files /dev/null and b/packs/slice/MANIFEST-000004 differ diff --git a/packs/starter/000005.ldb b/packs/starter/000005.ldb new file mode 100644 index 0000000..f7db5b4 Binary files /dev/null and b/packs/starter/000005.ldb differ diff --git a/packs/starter/000006.log b/packs/starter/000006.log new file mode 100644 index 0000000..68669cb Binary files /dev/null and b/packs/starter/000006.log differ diff --git a/packs/starter/CURRENT b/packs/starter/CURRENT new file mode 100644 index 0000000..cacca75 --- /dev/null +++ b/packs/starter/CURRENT @@ -0,0 +1 @@ +MANIFEST-000004 diff --git a/packs/starter/LOG b/packs/starter/LOG new file mode 100644 index 0000000..d947226 --- /dev/null +++ b/packs/starter/LOG @@ -0,0 +1,5 @@ +2026/08/11-23:23:08.324024 1752e7000 Recovering log #3 +2026/08/11-23:23:08.325074 1752e7000 Level-0 table #5: started +2026/08/11-23:23:08.325775 1752e7000 Level-0 table #5: 89206 bytes OK +2026/08/11-23:23:08.326188 1752e7000 Delete type=0 #3 +2026/08/11-23:23:08.326248 1752e7000 Delete type=3 #2 diff --git a/packs/starter/LOG.old b/packs/starter/LOG.old new file mode 100644 index 0000000..48f3c56 --- /dev/null +++ b/packs/starter/LOG.old @@ -0,0 +1 @@ +2026/08/11-23:22:30.856844 17117f000 Delete type=3 #1 diff --git a/packs/starter/MANIFEST-000004 b/packs/starter/MANIFEST-000004 new file mode 100644 index 0000000..78b3fb2 Binary files /dev/null and b/packs/starter/MANIFEST-000004 differ diff --git a/packs/tables/000003.log b/packs/tables/000003.log deleted file mode 100644 index f125b7d..0000000 Binary files a/packs/tables/000003.log and /dev/null differ diff --git a/packs/tables/000005.ldb b/packs/tables/000005.ldb new file mode 100644 index 0000000..64baa1c Binary files /dev/null and b/packs/tables/000005.ldb differ diff --git a/packs/tables/000006.log b/packs/tables/000006.log new file mode 100644 index 0000000..d5e06f3 Binary files /dev/null and b/packs/tables/000006.log differ diff --git a/packs/tables/CURRENT b/packs/tables/CURRENT index 1a84852..cacca75 100644 --- a/packs/tables/CURRENT +++ b/packs/tables/CURRENT @@ -1 +1 @@ -MANIFEST-000002 +MANIFEST-000004 diff --git a/packs/tables/LOG b/packs/tables/LOG index 6f8f621..d53aad9 100644 --- a/packs/tables/LOG +++ b/packs/tables/LOG @@ -1 +1,5 @@ -2026/08/11-22:56:53.194787 17465f000 Delete type=3 #1 +2026/08/11-23:23:08.330955 1742cf000 Recovering log #3 +2026/08/11-23:23:08.331078 1742cf000 Level-0 table #5: started +2026/08/11-23:23:08.331226 1742cf000 Level-0 table #5: 3739 bytes OK +2026/08/11-23:23:08.331550 1742cf000 Delete type=0 #3 +2026/08/11-23:23:08.331583 1742cf000 Delete type=3 #2 diff --git a/packs/tables/LOG.old b/packs/tables/LOG.old new file mode 100644 index 0000000..2c994fb --- /dev/null +++ b/packs/tables/LOG.old @@ -0,0 +1 @@ +2026/08/11-23:22:30.849798 17117f000 Delete type=3 #1 diff --git a/packs/tables/MANIFEST-000002 b/packs/tables/MANIFEST-000002 deleted file mode 100644 index bbbc585..0000000 Binary files a/packs/tables/MANIFEST-000002 and /dev/null differ diff --git a/packs/tables/MANIFEST-000004 b/packs/tables/MANIFEST-000004 new file mode 100644 index 0000000..004ff30 Binary files /dev/null and b/packs/tables/MANIFEST-000004 differ diff --git a/packs/talents/000003.log b/packs/talents/000003.log deleted file mode 100644 index 443b7ea..0000000 Binary files a/packs/talents/000003.log and /dev/null differ diff --git a/packs/talents/000005.ldb b/packs/talents/000005.ldb new file mode 100644 index 0000000..fb653de Binary files /dev/null and b/packs/talents/000005.ldb differ diff --git a/packs/talents/000006.log b/packs/talents/000006.log new file mode 100644 index 0000000..29003f7 Binary files /dev/null and b/packs/talents/000006.log differ diff --git a/packs/talents/CURRENT b/packs/talents/CURRENT index 1a84852..cacca75 100644 --- a/packs/talents/CURRENT +++ b/packs/talents/CURRENT @@ -1 +1 @@ -MANIFEST-000002 +MANIFEST-000004 diff --git a/packs/talents/LOG b/packs/talents/LOG index cb1063d..80e25cf 100644 --- a/packs/talents/LOG +++ b/packs/talents/LOG @@ -1 +1,5 @@ -2026/08/11-22:56:53.168626 17465f000 Delete type=3 #1 +2026/08/11-23:23:08.307829 174adb000 Recovering log #3 +2026/08/11-23:23:08.308163 174adb000 Level-0 table #5: started +2026/08/11-23:23:08.308314 174adb000 Level-0 table #5: 5770 bytes OK +2026/08/11-23:23:08.308647 174adb000 Delete type=0 #3 +2026/08/11-23:23:08.308699 174adb000 Delete type=3 #2 diff --git a/packs/talents/LOG.old b/packs/talents/LOG.old new file mode 100644 index 0000000..54aea62 --- /dev/null +++ b/packs/talents/LOG.old @@ -0,0 +1 @@ +2026/08/11-23:22:30.823417 172197000 Delete type=3 #1 diff --git a/packs/talents/MANIFEST-000002 b/packs/talents/MANIFEST-000002 deleted file mode 100644 index bbbc585..0000000 Binary files a/packs/talents/MANIFEST-000002 and /dev/null differ diff --git a/packs/talents/MANIFEST-000004 b/packs/talents/MANIFEST-000004 new file mode 100644 index 0000000..f8c6b47 Binary files /dev/null and b/packs/talents/MANIFEST-000004 differ diff --git a/packs/vehicles/000003.log b/packs/vehicles/000003.log deleted file mode 100644 index 9950ce5..0000000 Binary files a/packs/vehicles/000003.log and /dev/null differ diff --git a/packs/vehicles/000005.ldb b/packs/vehicles/000005.ldb new file mode 100644 index 0000000..f6b2cd4 Binary files /dev/null and b/packs/vehicles/000005.ldb differ diff --git a/packs/vehicles/000006.log b/packs/vehicles/000006.log new file mode 100644 index 0000000..47800e2 Binary files /dev/null and b/packs/vehicles/000006.log differ diff --git a/packs/vehicles/CURRENT b/packs/vehicles/CURRENT index 1a84852..cacca75 100644 --- a/packs/vehicles/CURRENT +++ b/packs/vehicles/CURRENT @@ -1 +1 @@ -MANIFEST-000002 +MANIFEST-000004 diff --git a/packs/vehicles/LOG b/packs/vehicles/LOG index 1489aeb..58fcb45 100644 --- a/packs/vehicles/LOG +++ b/packs/vehicles/LOG @@ -1 +1,5 @@ -2026/08/11-22:56:53.176044 17465f000 Delete type=3 #1 +2026/08/11-23:23:08.318258 1752e7000 Recovering log #3 +2026/08/11-23:23:08.318423 1752e7000 Level-0 table #5: started +2026/08/11-23:23:08.318619 1752e7000 Level-0 table #5: 6939 bytes OK +2026/08/11-23:23:08.318944 1752e7000 Delete type=0 #3 +2026/08/11-23:23:08.319006 1752e7000 Delete type=3 #2 diff --git a/packs/vehicles/LOG.old b/packs/vehicles/LOG.old new file mode 100644 index 0000000..6ec0926 --- /dev/null +++ b/packs/vehicles/LOG.old @@ -0,0 +1 @@ +2026/08/11-23:22:30.831389 170973000 Delete type=3 #1 diff --git a/packs/vehicles/MANIFEST-000002 b/packs/vehicles/MANIFEST-000002 deleted file mode 100644 index bbbc585..0000000 Binary files a/packs/vehicles/MANIFEST-000002 and /dev/null differ diff --git a/packs/vehicles/MANIFEST-000004 b/packs/vehicles/MANIFEST-000004 new file mode 100644 index 0000000..d4d73b7 Binary files /dev/null and b/packs/vehicles/MANIFEST-000004 differ diff --git a/packs/weapons/000003.log b/packs/weapons/000003.log deleted file mode 100644 index e1488ed..0000000 Binary files a/packs/weapons/000003.log and /dev/null differ diff --git a/packs/weapons/000005.ldb b/packs/weapons/000005.ldb new file mode 100644 index 0000000..742beba Binary files /dev/null and b/packs/weapons/000005.ldb differ diff --git a/packs/weapons/000006.log b/packs/weapons/000006.log new file mode 100644 index 0000000..4d427a6 Binary files /dev/null and b/packs/weapons/000006.log differ diff --git a/packs/weapons/CURRENT b/packs/weapons/CURRENT index 1a84852..cacca75 100644 --- a/packs/weapons/CURRENT +++ b/packs/weapons/CURRENT @@ -1 +1 @@ -MANIFEST-000002 +MANIFEST-000004 diff --git a/packs/weapons/LOG b/packs/weapons/LOG index 1caa48d..f4adbf3 100644 --- a/packs/weapons/LOG +++ b/packs/weapons/LOG @@ -1 +1,5 @@ -2026/08/11-22:56:53.170534 173e53000 Delete type=3 #1 +2026/08/11-23:23:08.311931 174adb000 Recovering log #3 +2026/08/11-23:23:08.312527 174adb000 Level-0 table #5: started +2026/08/11-23:23:08.312793 174adb000 Level-0 table #5: 14767 bytes OK +2026/08/11-23:23:08.313217 174adb000 Delete type=0 #3 +2026/08/11-23:23:08.313275 174adb000 Delete type=3 #2 diff --git a/packs/weapons/LOG.old b/packs/weapons/LOG.old new file mode 100644 index 0000000..b9e9eaa --- /dev/null +++ b/packs/weapons/LOG.old @@ -0,0 +1 @@ +2026/08/11-23:22:30.825501 172197000 Delete type=3 #1 diff --git a/packs/weapons/MANIFEST-000002 b/packs/weapons/MANIFEST-000002 deleted file mode 100644 index bbbc585..0000000 Binary files a/packs/weapons/MANIFEST-000002 and /dev/null differ diff --git a/packs/weapons/MANIFEST-000004 b/packs/weapons/MANIFEST-000004 new file mode 100644 index 0000000..6a582af Binary files /dev/null and b/packs/weapons/MANIFEST-000004 differ diff --git a/system.json b/system.json index 67ca553..aec29a2 100644 --- a/system.json +++ b/system.json @@ -120,6 +120,13 @@ "type": "JournalEntry", "system": "ringbrp" }, + { + "name": "starter", + "label": "The Custodians · PAPER WINDOW (starter scenario)", + "path": "packs/starter", + "type": "Adventure", + "system": "ringbrp" + }, { "name": "slice", "label": "The Custodians · The Ilfracombe Crossing", diff --git a/tools/build-packs.mjs b/tools/build-packs.mjs index 5d83fa6..6565b1f 100644 --- a/tools/build-packs.mjs +++ b/tools/build-packs.mjs @@ -472,6 +472,161 @@ await writePack("armour", ARMOURS.map(a => { await writePack("tables", entries); } +// PAPER WINDOW — the starter scenario, shipped as an Adventure so the whole thing +// imports in one click rather than as nine loose documents a GM has to assemble. +{ + const S = await import("./scenario-starter.mjs"); + const { ROLES, TRADES, INDUCTION, TRADE_BANDS, STANDING_ISSUE, TIER_ISSUE, TIERS } + = await import("../postings.mjs"); + + // A posting or trade expanded into the shape buildActor wants. Ratings are FIXED at + // the middle of the band rather than rolled: every table that runs the starter should + // get the same six agents, and a pregen that varies between installs is not a pregen. + const mid = b => Math.round((b[0] + b[1]) / 2); + const expand = spec => { + const isTrade = !!spec.trade; + const R = isTrade ? TRADES[spec.trade] : ROLES[spec.role]; + if (!R) throw new Error(`starter: no ${isTrade ? "trade" : "role"} "${spec.trade ?? spec.role}"`); + const T = TIERS[spec.rank === "Probationary" ? "probationary" : "officer"]; + const coreV = isTrade ? mid(TRADE_BANDS.core) : mid(T.core); + const supportV = isTrade ? mid(TRADE_BANDS.support) : mid(T.support); + const inducV = mid(INDUCTION.bandByTier[spec.rank === "Probationary" ? "probationary" : "officer"]); + + const skills = []; + const seen = new Set(); + const addSkill = (id, val) => { + if (seen.has(id)) return; // induction never demotes a trade + seen.add(id); + const [fam, sp = ""] = id.split(":"); + skills.push({ fam, spec: sp, val }); + }; + for (const k of R.core) addSkill(k, coreV); + for (const k of R.support) addSkill(k, supportV); + if (isTrade) for (const k of INDUCTION.skills) addSkill(k, inducV); + addSkill("language:trade_cant", 55); + + // Kit: standing issue, the posting or trade's own, and induction for a trade. + const kitKeys = [...STANDING_ISSUE, ...R.kit, + ...(isTrade ? INDUCTION.kit : []), + ...(TIER_ISSUE[spec.rank === "Probationary" ? "probationary" : "officer"] ?? [])]; + const weapons = [], armour = [], gear = []; + for (const k of new Set(kitKeys)) { + if (WEAPON_LOOKUP.has(k)) weapons.push(k); + else if (ARMOUR_LOOKUP.has(k)) armour.push(k); + else if (GEAR_LOOKUP.has(k)) gear.push(k); + else throw new Error(`starter: ${spec.name} issued unknown item "${k}"`); + } + const bio = `

${R.label}, ${spec.rank}. ${R.blurb}

` + + `

Your job in this case. ${spec.spotlight}

` + + `

Your complication. ${spec.complication}

` + + `

${spec.note}

`; + return { ...spec, skills, weapons, armour, gear, bio }; + }; + + const actorDocs = [], itemDocsFor = new Map(); + for (const spec of S.STARTER_TEAM) { + const { actorDoc, itemDocs } = buildActor(expand(spec), "character"); + actorDoc.items = itemDocs; // Adventures embed full documents + actorDocs.push(actorDoc); + } + for (const spec of S.STARTER_CAST) { + const { actorDoc, itemDocs } = buildActor(spec, "npc"); + actorDoc.items = itemDocs; + actorDocs.push(actorDoc); + } + + // The credential, as an item rather than a paragraph in the brief. + const A = S.STARTER_AUTHORITY; + const authId = idFor(`item:${A.key}`); + const authority = { + _id: authId, name: A.name, type: "authority", img: IC("cover_id") ?? ICON.gear, + system: { + description: A.description, + holderName: A.holderName, holderRole: A.holderRole, issuer: A.issuer, + scope: A.scope, scopeNote: A.scopeNote, + trust: { value: A.trust, max: 4 }, + expires: "", redFlags: A.redFlags, dossier: [], + establishesRemit: false, timesWidened: 0, mergeSteps: 0, + burned: false, suspect: false, aiPosture: "", notes: "" + }, + effects: [], ...BASE(), flags: { ringbrp: { key: A.key } } + }; + + // The case file, already at the scenario's opening state. + const C = S.STARTER_CASE; + const caseId = idFor(`actor:${C.key}`); + const caseDoc = { + _id: caseId, name: C.name, type: "expedition", img: ICON.caseFile ?? ICON.actor, + system: { + integrityMode: "abstract", + integrity: { value: C.containment, max: 10 }, + supply: { value: C.resources, max: 10 }, + power: { value: C.clearance, max: 10 }, + assets: [], members: S.STARTER_TEAM.map(t => t.key), + journey: { + destinationId: "rookery_heath", destinationName: C.destination, + destinationKnowledge: "briefed", currentLeg: 0, elapsedDays: 0, + siteTableId: "sites", hazardTableId: "hazards", returnClock: 0, + legs: C.legs.map((l, i) => ({ + index: i, terrain: l.label, mode: "foot", kindId: l.kindId, + difficulty: l.difficulty, modifier: l.mod, days: 0, + explored: true, resolved: false, forcedEncounterId: "", + costSupply: 1, costPower: 0 + })) + }, + standings: [{ groupId: "returned_clerks", value: 0, impression: 0, encounters: 0, + knownTaboos: [], knownDesires: [], pidginEstablished: false, obligations: [] }], + across: false, crossingModifier: 0, salvageEra: "modern", + notes: C.notes + }, + items: [], effects: [], + prototypeToken: { name: C.name, displayName: 20, actorLink: true, width: 1, height: 1, + texture: { src: ICON.actor }, sight: { enabled: false }, disposition: 1, + bar1: { attribute: "integrity" }, bar2: { attribute: "supply" }, flags: {} }, + ...BASE() + }; + actorDocs.push(caseDoc); + + const gmJournal = journalDoc("PAPER WINDOW — Run the case", "starter_gm", S.STARTER_PAGES); + const handouts = journalDoc("PAPER WINDOW — Player handouts", "starter_handouts", S.STARTER_HANDOUTS); + // 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 }; + for (const p of handouts.pages) p.ownership = { default: 2 }; + + const advId = idFor("adventure:paper_window"); + const adventure = { + _id: advId, + name: "PAPER WINDOW — The Dead Letter Office", + img: IC("actor_case") ?? ICON.caseFile ?? ICON.actor, + caption: "A six-player starter case. Four to five hours.", + description: + "

Six brown envelopes with a 1987 postmark, from an office that closed in " + + "1998. Open one and the person it is addressed to begins disappearing from the " + + "record — not dying, just no longer filed.

" + + "

The complete starter: GM scenario, seven player handouts, six pregenerated " + + "agents with their scenario jobs and complications, the cast, the Borrowed " + + "Authority as a usable credential, and the case file already set to Containment 8, " + + "Resources 8, Clearance 6 with its four phases loaded.

" + + "

It teaches the game while it is being played: Borrowed Authority at the " + + "cordon, investigation in the cage, crossing at Window Six, READ / SIGNAL / OFFER " + + "with the night shift, one containable fight, and a closing scene that needs all " + + "six players.

", + actors: actorDocs, + items: [authority], + journal: [ + { ...gmJournal.doc, pages: gmJournal.pages }, + { ...handouts.doc, pages: handouts.pages } + ], + scenes: [], combats: [], tables: [], macros: [], cards: [], playlists: [], folders: [], + sort: 0, ...BASE() + }; + + await writePack("starter", [[`!adventures!${advId}`, adventure]]); + console.log(` starter PAPER WINDOW — ${actorDocs.length} actors, ` + + `${S.STARTER_PAGES.length} GM pages, ${S.STARTER_HANDOUTS.length} handouts`); +} + // the slice journal { const { SLICE_PAGES } = await import("./slice-text.mjs"); diff --git a/tools/scenario-starter.mjs b/tools/scenario-starter.mjs new file mode 100644 index 0000000..7511e91 --- /dev/null +++ b/tools/scenario-starter.mjs @@ -0,0 +1,696 @@ +/** + * PAPER WINDOW — the starter scenario, as data. + * + * Shipped as a Foundry ADVENTURE so the whole thing imports in one click: the GM + * text, the seven player handouts as separate pages a GM can reveal one at a time, + * the six pregens with their scenario functions and complications already written + * into their biographies, the cast, the case file with its tracks and phases already + * set, and the Borrowed Authority as a real item rather than a paragraph. + * + * The handouts are their own journal with default ownership OBSERVER, because a + * handout a GM has to copy out of the scenario text is a handout that does not get + * used. The GM journal stays hidden. + */ + +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}
`; + +/* ---------------------------------------------------------------- */ +/* The GM scenario */ +/* ---------------------------------------------------------------- */ + +export const STARTER_PAGES = [ + +{ name: "1 · Run this first", content: + P("PAPER WINDOW is a six-player introductory case for The Custodians. " + + "Four to five hours. Classification Near. Bureaucratic horror, " + + "investigation, negotiation and one containable fight.") + + P("It teaches the game while it is being played. Do not explain the " + + "subsystems in the briefing. Introduce each one the first time the agents need it.") + + TABLE(["Scene", "Introduces"], [ + ["Briefing", "the agency, the case tracks, cover and objectives"], + ["The cordon", "ordinary skill rolls and Borrowed Authority"], + ["The dead-letter cage", "investigation, fail-forward clues and the case clock"], + ["Window Six", "crossing and Coherence"], + ["The night shift", "READ, SIGNAL, OFFER and Standing"], + ["The Cancellation Man", "reaction lanes, cover, graded defences, armour, First Aid"], + ["Closing the register", "specialist teamwork, Containment and a real choice"], + ["Coming home", "extraction, return rolls, debrief and experience"] + ]) + + P("There is no clue that must be found and no roll that must succeed. " + + "A failed roll changes the cost, the danger or the clarity of the information. It " + + "never stops the scenario.") + + H(2, "The six-player spotlight rule") + + P("Every major scene names a spotlight agent. That player frames the " + + "first approach. Others assist, supply dossier items, handle consequences or propose " + + "alternatives. Do not solve another player's specialist scene with the Field " + + "Lead's phase roll. The purpose of a six-player starter is six protagonists, " + + "not six people present.") +}, + +{ name: "2 · The briefing", content: + READ("At 08:12 yesterday, six brown envelopes were delivered in Coventry. They bear " + + "valid first-class postage, a 1987 postmark and the cancellation mark of Rookery Heath " + + "Delivery Office, which closed in 1998.

" + + "Each envelope was addressed to a living person. Within an hour of opening one, the " + + "recipient disappeared from government databases. By evening, employers, banks and " + + "close relatives had begun forgetting them. The people themselves remain physically " + + "present.

" + + "One recipient, Lorna Vale, went to the closed delivery office shortly after midnight. " + + "She has not returned. At 03:17 the office's disconnected landline called the Cabinet " + + "Office switchboard. A woman asked whether the Department would be making its last " + + "collection.

" + + "The building is scheduled for demolition at sixteen hundred today.") + + H(2, "Orders") + + OL(["Enter Rookery Heath Delivery Office under the supplied cover.", + "Recover Lorna Vale and the six original letters.", + "Identify the source of the cancellation mark.", + "Stop further deliveries.", + "Leave no publicly intelligible incident."]) + + P("Cover: licensed asbestos and records-remediation team acting for " + + "the demolition contractor.") + + P("The brief omits: the agency attended the same building after a fire " + + "in 1987. The file has been deliberately removed from the current case papers.") + + H(2, "Starting state") + + P("Containment 8 · Resources 8 · Clearance 6 · Last Collection 0/4 · " + + "all agents at Coherence 10. The case file in this adventure is already set to these.") + + P("The six letters count as one secured exhibit once collected.") +}, + +{ name: "3 · The truth", content: + P("In 1987, Rookery Heath briefly overlapped a Near world in which the building was not " + + "a postal depot but the Civil Continuity Registry. In that world a " + + "person's civic existence depended on a deliverable name and address held in the " + + "nightly register.") + + P("The overlap happened during a fire. For eleven minutes both offices processed the " + + "same mail. Three hundred and seventeen people acquired valid entries in " + + "both worlds.") + + P("Custodian Malcolm Rusk contained it by declaring Window Six, the " + + "disputed register and the entire other office to be an undelivered agency " + + "consignment, and sealing the declaration in the dead-letter cage. As long as " + + "the building remained an unresolved delivery, the crossing stayed closed.") + + P("Demolition workers broke the exterior seal three days ago. The Registry's night " + + "shift resumed where it stopped in 1987. Its staff have spent thirty-nine years " + + "waiting to be relieved, and their automatic system has begun delivering notices " + + "that reconcile the disputed citizens by deleting one version from the record.") + + P("There are two Lorna Vales. Both are forty-two, both have a complete " + + "childhood, family and employment history. Neither is the original: the overlap " + + "produced two equally valid continuations of the same infant recorded in 1987.") + + P("The Registry staff are not invaders. They want a recognised authority to " + + "take custody of the disputed folio so they can close their shift. The briefed " + + "solution — destroying the other register — works, and erases 317 people who are as " + + "real as anyone on the team.") + + P("The clean solution is to accept the folio into agency custody as a " + + "pending inter-jurisdictional register, using the original case number, a " + + "working franking mechanism, the Registry's master stamp, a medical certification " + + "that the duplicates are living persons, and a serving officer's signature. " + + "It requires every member of the six-person team.") +}, + +{ name: "4 · Pressure — the Last Collection", content: + P("Advance the clock when a phase fumbles; when the team argues twenty minutes without " + + "acting; when a loud weapon is fired in the mundane building; when the Borrowed " + + "Authority is burned; when the team violates the clerks' taboo after learning it; " + + "or when passive investigation needs turning into a decision.") + + P("Do not advance it merely because the players are investigating carefully.") + + TABLE(["Clock", "Event"], [ + ["1 — Sorting", + "The dead conveyor starts. Every letter in the building rotates so its address faces Window Six."], + ["2 — Forwarded", + "One affected recipient's spouse can no longer remember their face. Containment −1."], + ["3 — At the gate", + "The demolition manager arrives with workers and police support. The cover has about thirty minutes left. Clearance −1 if the team demands overt agency intervention."], + ["4 — Last Collection", + "Every disputed notice is delivered at once. Window Six locks open, the two offices begin occupying the same space, and the demolition crew witnesses it. Containment −3. The clean settlement is still possible; all remaining specialist tasks become Difficult."] + ]) + + P("If the clock would pass 4, reduce Clearance or Containment instead, according to " + + "whether the cause was disobedience or public exposure.") +}, + +{ name: "5 · Phase one — the demolition cordon", content: + P("A low concrete building behind temporary fencing. Half the red sign has been removed, " + + "leaving DELIVE OFF over the entrance. Rainwater lies everywhere " + + "except on the front step.") + + P("Gareth Moss, site manager, is competent, worried about his schedule " + + "and eager to transfer responsibility. He has opened no letter. He accepts Imogen " + + "Price's credential if the team behaves like a survey crew.") + + H(2, "Teach: Borrowed Authority") + + P("Ask the group how they use the identity without breaking its red flags. Signing in " + + "satisfies the first. A six-person team satisfies the second. Photographing each " + + "opened door satisfies the third and creates useful evidence.") + + P("Routine entry costs 1 Trust and needs no roll. Demanding that demolition stop is a " + + "stretch, or Clearance spent on overt agency authority.") + + H(2, "Field Lead spotlight — choose the posture") + + UL(["Quiet: demolition continues outside. +20% Stealth inside; Last " + + "Collection advances if the crew reaches Window Six.", + "Official: spend 1 Clearance to suspend demolition two hours. No " + + "public clock pressure, but the agency knows the team went beyond the brief.", + "Borrowed: stretch Price's scope for a temporary asbestos exclusion. " + + "Success buys two hours free; failure buys thirty minutes; a fumble burns the credential."]) + + H(2, "What Gareth gives without a roll") + + P("If treated as a professional rather than a witness to be managed:") + + UL(["The cage door was found open three days ago, although its lock was rusted shut.", + "Lorna Vale entered using a staff key bearing the old crown logo.", + "A landline rings inside every day at 15:42.", + "The demolition plan contains no Window Six."]) +}, + +{ name: "6 · Phase two — the dead-letter cage", content: + P("Sorting frames, sacks hardened by damp, a secure wire cage. Electricity disconnected. " + + "The fluorescent lights come on when nobody is looking directly at them.") + + P("Give the group the plan and ask where everyone goes. Keep all six active by cutting " + + "between simultaneous searches.") + + P("Every discovery is automatic when the agents search the right place. " + + "A success adds interpretation or advantage; a failure gives the clue with a cost; a " + + "fumble also advances Last Collection.") + + H(2, "A · The 1987 agency carbon — Registry or Research") + + P("In the manager's desk, a carbon headed R-143 / UNDELIVERABLE MATERIAL.") + + UL(["Success: “pending collection” is the containment, not " + + "ordinary wording. Record case number R-143 for the clean ending.", + "Failure: found and read, but removing it tears the line with the " + + "signing officer's name. Resources −1 to authenticate later.", + "Special/critical: the page numbers jump from 14 to 22. The agency " + + "knew, and removed the intervening report."]) + + H(2, "B · The six letters — Doctor") + + P("Five wait in numbered pigeonholes; the sixth is opened and addressed to Lorna Vale. " + + "The ink contains human proteins. The paper fibres carry the addressee's own " + + "mitochondrial markers.") + + UL(["Success: these are not messages about the recipients, they " + + "are partial physical records of them. Destroying one may injure or erase " + + "its addressee. +20% when certifying the disputed people later.", + "Failure: same conclusion, after a sample makes one recipient briefly " + + "lose their surname. Witnessed paradox: −1 Coherence to the doctor.", + "Critical: the doctor's own future death notice is medically genuine " + + "and has not yet been written."]) + + H(2, "C · Window Six — Engineer") + + P("Behind a sorting frame, a service window absent from the building plan. Roller shutter " + + "painted shut. The brass franking machine beside it has no power connection and is warm.") + + UL(["Success: the window is a regulator whose state is expressed through " + + "postal procedure, and it is repairable. Record franker diagnosed: " + + "+20% on the later repair.", + "Failure: learns how it works by opening the shutter two centimetres. " + + "Everyone hears rain falling upward. Containment −1.", + "Fumble: as failure, and the machine stamps the engineer's hand with " + + "tomorrow's date. Witnessed paradox."]) + + H(2, "D · The master key and the impossible fingerprints — Criminal") + + P("Two mechanisms: an ordinary warded lock, and a bolt that moves only while the landline " + + "is ringing. Sleight of Hand opens the first. The second yields to calling the " + + "disconnected number from the agency handset, waiting for 15:42, or Gareth's staff key.") + + UL(["Success: in without marking the lock, and the old crown staff key.", + "Failure: in, with visible damage — 1 Trust or 1 Clearance to suppress it.", + "Fumble: the cage closes with the criminal inside as the landline rings. " + + "They may answer. A woman says, “Collection is overdue, Mr Roake.” " + + "Advance Last Collection."]) + + P("Inside the cage are the criminal's fingerprints under the identity Arthur " + + "Roake, postal porter, deceased 19 September 1987.") + + H(2, "Opening Window Six") + + P("The disconnected telephone rings once. A woman asks: “Collection or delivery?”") + + P("“Collection” opens the shutter safely. “Delivery” " + + "opens it violently and advances Last Collection. Any careful reading of R-143 shows the " + + "consignment is pending collection.") + + P("Mark the case Across. Going out is a decision, not a roll — the " + + "Transposition roll happens coming home. The mundane shutter closes behind the last " + + "agent and can still be opened from either side.") +}, + +{ name: "7 · Phase three — the night shift", content: + P("The Near office is clean, warm and fully staffed. Six clerks in 1987 uniforms work " + + "beneath green lamps. None has aged. Pneumatic tubes carry sealed envelopes into a " + + "ceiling with no space above it.") + + P("At Window Six stands Evelyn Vale, night supervisor. Behind her is " + + "Lorna Vale, the missing recipient — and another Lorna Vale " + + "in Registry uniform. They are physically identical.") + + P("Seeing the two Lornas is a witnessed paradox. It is not meeting " + + "yourself unless an agent meets their own duplicate.") + + H(2, "READ") + + P("Insight, Anthropology or relevant professional observation. Facts in order:") + + OL(["Taboo: never open, read aloud or name the addressee of mail that is " + + "not yours. A sealed identity belongs to its recipient.", + "Desire: they want a recognised authority to relieve them and receipt " + + "the disputed folio. Thirty-nine years on the same shift.", + "Critical extra: they are frightened of the Cancellation Man, which is " + + "not a person but the procedure that destroys records that cannot be reconciled."]) + + H(2, "SIGNAL") + + P("They speak English; the difficulty is procedural. A correct signal gives name, " + + "department and case reference from the public side of the counter: " + + "“Amira Bell, Departmental Custody, attending R-143 for collection.”") + + UL(["With R-143 — Easy.", + "Without it — Average.", + "From behind the counter — Difficult, and it violates their working boundary."]) + + H(2, "OFFER") + + P("A signed receipt and a promise to take the folio into recognised custody, passed " + + "through Window Six in exchange for it.") + + UL(["Correct form — Easy Bargain.", + "A promise to “look into it” — Average.", + "Demanding surrender or threatening destruction — Difficult."]) + + P("At +2 or better Evelyn explains the clean closure and releases the " + + "folio once the six requirements are met. At +1 she explains only that " + + "the register needs custody and certification. At 0 she permits " + + "inspection but will not release it. At negative, she activates the " + + "Cancellation Man.") + + H(2, "The two Lornas") + + P("Same childhood until the 1987 fire, different lives after. The mundane Lorna has a " + + "partner and works for the council. Registry Lorna has worked this shift for " + + "thirty-nine subjective years and remembers a daughter born in the Near world.") + + P("Neither wants the other erased. Asked which is real, each answers: " + + "“Both, until you file otherwise.”") + + P("Any successful Medicine or Biology roll establishes that both are living humans and " + + "neither is a constructed copy. Failure leaves it medically apparent but costs the " + + "later certification its +20%.") +}, + +{ name: "8 · The Cancellation Man", content: + P("Trigger when the team tries to remove or destroy the folio without an " + + "agreement; when someone knowingly violates the taboo; when Standing reaches −1; or when " + + "the table needs the promised physical tutorial before the final act.") + + P("The lights extinguish from the back of the room toward the team. A tall figure unfolds " + + "from returned envelopes, red twine and black cancellation ink. Every address " + + "on it belongs to someone currently in the room.") + + P("It wants the disputed folio, not casualties. It moves toward the " + + "carrier and ignores other agents unless they obstruct it.") + + H(2, "In Foundry") + + P("Use the Cancellation Man actor in this adventure, or any Dangerous " + + "adversary. It withdraws at 2 hit points, when restrained, or when presented with a " + + "properly bagged and receipted folio. For a gentler first session, drop its attack by " + + "10% and start it a full move away.") + + H(2, "Terrain") + + UL(["Sorting frames — solid partial cover.", + "The service counter — blocks movement, not attacks.", + "Closing a wire cage around it — one action, and Sleight of Hand, Repair (Mechanical) or Athletics.", + "The Containment Officer can interpose a raised riot shield between it and the carrier."]) + + H(2, "Teach combat in this order") + + OL(["Roll reaction lanes.", + "Ask each player for movement and one significant action.", + "Explain range, cover or shield modifiers only when they apply.", + "When attacked, demonstrate Dodge or Parry and the escalating defence penalty.", + "If a wound penetrates, resolve location, armour and any Major Wound.", + "Give the doctor an immediate First Aid decision if anyone bleeds or goes down."]) + + H(2, "Nonviolent containment — always available") + + UL(["Bag the folio and attach a provisional receipt: Bargain or Status, Average. Chain of " + + "Custody is exactly this expertise.", + "Lock it in the dead-letter cage: the criminal works the lock, Containment holds it off.", + "Repair the franking machine and stamp the receipt early: the engineer acts, another agent covers."]) + + P("Killing it is temporary. It reforms at the next clock advance. " + + "Containing it teaches the intended lesson: violence buys time, procedure closes the case.") +}, + +{ name: "9 · Phase four — closing the register", content: + P("Put six blank lines on the table, one per contribution. Tell the players the case can " + + "now be closed. Do not prescribe which ending.") + + H(2, "The clean settlement") + + P("Enter the folio into agency custody as R-143-A, Pending Inter-Jurisdictional " + + "Register. This decides nothing about which world owns the 317 names. It " + + "establishes a third recognised custodian and lets the Registry close its shift without " + + "deleting anyone.") + + TABLE(["#", "Requirement", "Who", "Roll", "On failure"], [ + ["1", "Establish authority", "Field Lead", "Spend 1 Clearance, or Status/Tradecraft to justify it", + "The Clearance is spent anyway. Fumble also marks the lead for internal investigation."], + ["2", "Hold containment", "Containment", "Athletics, Shield or a practical plan", + "Containment −1; the work continues. A critical prevents the next clock advance."], + ["3", "Write the settlement", "Settlements", "Bargain, Persuade or Knowledge — Easy at impression +2", + "A future obligation: the Registry staff are technically seconded, and may later demand pay, pension and extraction."], + ["4", "Certify the living", "Doctor", "Medicine, +20% if the letters and both Lornas were examined", + "Still certified, but the doctor is personally responsible for reviewing all 317 cases."], + ["5", "Repair and frank", "Engineer", "Repair (Electronics) or Engineering, +20% if diagnosed", + "Resources −1. Fumble works but dates the receipt one year in the future — a campaign hook."], + ["6", "Recover the master stamp", "Criminal", "Sleight of Hand, from the public side of the counter", + "Obtained, but the drawer breaks: impression −1. Fumble opens a second drawer holding an undelivered letter addressed to the criminal."] + ]) + + P("When all six are on the form, the Field Lead reads the case number aloud and Evelyn " + + "stamps the other copy. Every clock in the Near office moves from 15:42 to " + + "15:43.") + + P("The clerks put on coats. Some cry. None of them knows what weather waits outside. The " + + "Near Lorna asks the mundane Lorna whether she would like to meet her daughter. It is " + + "not a trick and does not have to be resolved tonight.") + + P("Result: Containment +1; Clearance −1 if the team openly defied orders; " + + "Standing with the Returned Clerks +1. All six affected recipients begin returning to " + + "records over the next day.") + + H(2, "The other endings") + + UL(["Destroy the Near register. The agency's implied preference. The six " + + "mundane recipients are restored; the Registry staff, Near Lorna and 311 others become " + + "unrecorded and then physically absent. Watching it is a witnessed paradox; doing it " + + "knowingly is also broke local law. Containment +2, Clearance +1. At debrief nobody " + + "uses the word “people” unless a player does first.", + "Let the Registry supersede our record. The mundane six are progressively " + + "forgotten and their Near counterparts take their addresses, jobs and relationships. The " + + "team recovers Lorna — the other Lorna. Containment unchanged, Clearance −2. Grave, and " + + "not a game over.", + "Seal it again. Recreate Rusk's 1987 containment. Containment +1; the case " + + "is contained, not closed; the clock resets to 1 and starts again when the " + + "building is next disturbed. Somebody must stop the demolition permanently.", + "Fail to choose. At Last Collection 4 the buildings overlap and the crew " + + "sees the Registry. The team can still escape. Close on sirens arriving at a building " + + "that officially employs everyone standing outside it."]) +}, + +{ name: "10 · Extraction and debrief", content: + P("Use the normal return procedure and Transposition rolls in every ending. A clean " + + "settlement leaves the window clock at zero and adds no case-specific penalty.") + + UL(["Another agent may aim the return for someone else.", + "Failure gets an agent home near the target; it never strands them.", + "A fumble costs Coherence and may grant a Quiet Art.", + "PAPER WINDOW is Near: no classification penalty."]) + + P("When everyone is home, close the case and resolve experience.") + + H(2, "Debrief — ask each player one") + + OL(["Field Lead: at what moment did this stop being the case in your orders?", + "Containment: what did you protect that the department would rather was destroyed?", + "Settlements: which sentence in the agreement do you expect to regret?", + "Doctor: are the two Lornas the same person? Answer medically, then answer honestly.", + "Engineer: did you repair Window Six, or teach it a new procedure?", + "Criminal: what was written on the letter you left sealed?"]) + + READ("Whose name did you knowingly leave off the form?") + + P("On a clean settlement each agent recovers the normal debrief Coherence. If the team " + + "destroyed or superseded a register, ask the question before recovery.") +}, + +{ name: "11 · GM quick reference", content: + H(2, "Cast") + + TABLE(["Who", "Wants", "Lies about"], [ + ["Gareth Moss, site manager", + "a signed instruction and someone else liable for the delay", + "claiming he never looked through Window Six"], + ["Lorna Vale, mundane", + "her life and the truth, both", "having no curiosity about the other Lorna's daughter"], + ["Lorna Vale, Registry clerk", + "the shift to end without taking her existence with it", + "never having sent a letter to the mundane world before"], + ["Evelyn Vale, night supervisor", + "a valid receipt, more than rescue", + "nothing — but she answers only the question procedurally put to her"], + ["The Cancellation Man", + "unresolved records deleted or returned", + "it cannot bargain, but it obeys valid packaging and receipts"] + ]) + + H(2, "Clue map") + + TABLE(["Conclusion", "Evidence routes"], [ + ["The agency knew", "R-143 carbon; missing page numbers; the lead's impossible cover employment"], + ["Window Six is the crossing", "engineer's diagnosis; absent building plan; upward rain"], + ["Letters rewrite people", "doctor's analysis; recipients forgotten; Registry explanation"], + ["The clerks are trapped, not invading", "READ; frozen clocks; relief rota; Evelyn's offer"], + ["Both Lornas are real", "medical exam; separate post-1987 memories; the disputed folio"], + ["Clean closure is possible", "R-143 wording; contact at +1/+2; repaired franking mechanism"] + ]) + + H(2, "Fail-forward costs") + + P("When uncertain, charge one of these rather than withholding information — and never " + + "more than one major cost for a single failed roll unless it is a fumble.") + + UL(["Resources −1", "Containment −1", "Clearance −1", "Advance Last Collection", + "Apply a listed Coherence occurrence", "Reduce impression by 1", + "Attach a future obligation to the successful outcome"]) + + H(2, "Time management") + + TABLE(["Real time", "Target position"], [ + ["0:00–0:20", "Briefing and character introductions"], + ["0:20–1:00", "Cordon and Borrowed Authority"], + ["1:00–2:00", "Investigation and Window Six"], + ["2:00–3:00", "Contact and the two Lornas"], + ["3:00–3:40", "The Cancellation Man"], + ["3:40–4:30", "Closure, extraction and debrief"] + ]) + + P("Short on time: let the cage open automatically when the telephone rings, and run the " + + "Cancellation Man as a three-round containment challenge rather than combat to zero.") + + H(2, "Spotlight checklist") + + UL(["Lead chose the posture and signed the outcome.", + "Containment raised the shield, protected someone or secured the folio.", + "Settlements completed READ/SIGNAL/OFFER or drafted the receipt.", + "Doctor examined the letters or the Lornas, and had a First Aid opportunity.", + "Engineer diagnosed and repaired Window Six.", + "Criminal entered the cage and recovered the master stamp."]) + + P("If a box is still empty approaching the final scene, move that contribution into that " + + "player's path.") +}, + +{ name: "12 · Continuing the campaign", content: + P("On the clean ending the agency holds the R-143-A Disputed Folio. It " + + "generates cases without imposing a metaplot:") + + UL(["A name appears in the folio before the team meets its owner.", + "The future date from a fumbled repair arrives.", + "The Returned Clerks submit thirty-nine years of back-pay claims.", + "Near Lorna asks the team to extract her daughter.", + "A senior officer orders the folio destroyed, and supplies perfect paperwork.", + "Malcolm Rusk signs a new collection receipt, having died in 1994."]) + + P("Precedent: R-143-A recognises duplicate civic identities as living " + + "persons pending adjudication. Trust 3 on a clean settlement, 1 if " + + "merely resealed. Red flag: every invocation adds another name to the " + + "folio. Unresolved: the agency accepted custody and never promised a " + + "decision date.") + + READ("A firearm can stop the thing coming down the corridor, but only a correctly worded " + + "record can make the corridor stop existing.") +} +]; + +/* ---------------------------------------------------------------- */ +/* Player handouts — their own journal, visible to players */ +/* ---------------------------------------------------------------- */ + +export const STARTER_HANDOUTS = [ + +{ name: "1 · Operational brief", content: + P("DEPARTMENTAL CUSTODY SERVICE
" + + "CASE: PW-01 / ROOKERY HEATH
" + + "CLASSIFICATION: NEAR / LOCAL AUTHORITY EXPOSURE
" + + "REPORT: 08:30, Gate 2, Rookery Heath Delivery Office, Coventry") + + H(2, "Situation") + + P("Six letters bearing valid 1987 postmarks were delivered yesterday from an office " + + "closed in 1998. Opening a letter causes the addressee to disappear progressively " + + "from public and private records.") + + P("One recipient, Lorna Vale, entered the delivery office at 00:41 and " + + "has not returned.") + + P("The disconnected office landline called the Cabinet Office at 03:17. The caller asked " + + "whether the Department would be making its last collection.") + + P("Demolition is scheduled for 16:00.") + + H(2, "Orders") + + OL(["Enter under asbestos and records-remediation cover.", + "Recover Lorna Vale and the six original letters.", + "Identify the cancellation source.", + "Stop further deliveries.", + "Leave no publicly intelligible incident."]) + + H(2, "Starting state") + + P("Containment 8 · Resources 8 · Clearance 6") +}, + +{ name: "2 · Borrowed Authority — Imogen Price", content: + P("IMOGEN PRICE
Senior Asbestos Surveyor
" + + "Halcyon Environmental Compliance Ltd") + + P("Scope: routine site inspection
Trust: 2") + + H(2, "Observed working habits") + + UL(["Signs in at every controlled site.", + "Never conducts an inspection alone.", + "Photographs every opened door."]) + + P("Inside scope the credential works. One tier beyond scope requires a stretch. " + + "A fumble burns it.") +}, + +{ name: "3 · Recovered carbon copy", content: + P("DEPARTMENTAL CUSTODY SERVICE
" + + "R-143 / UNDELIVERABLE MATERIAL
19 SEPTEMBER 1987") + + P("Window Six and contents entered into temporary agency custody, pending collection.") + + P("Consignment must not be:") + + UL(["opened;", "destroyed;", "redirected; or", "returned to sender."]) + + P("Status remains PENDING COLLECTION until receipted by a serving officer.") + + P("Pages attached: 14, 22.") +}, + +{ name: "4 · The opened letter", content: + P("CIVIL CONTINUITY REGISTRY
NOTICE OF REDIRECTION") + + P("Dear Ms Lorna Vale,") + + P("The Registry regrets that the name and civic person presently delivered to:") + + P("14 MERE ROAD, COVENTRY") + + P("has previously and continuously been delivered to:") + + P("14 MERE ROAD, COVENTRY") + + P("This duplication cannot continue after the Last Collection.") + + P("Unless an authorised custodian accepts the disputed entry, one delivery will be " + + "cancelled without further notice.") + + P("Please do not destroy this notice. This notice contains material belonging to " + + "the addressee.") +}, + +{ name: "5 · Window Six procedure card", content: + H(2, "WINDOW SIX") + + OL(["State COLLECTION or DELIVERY.", + "Give name, department and case reference.", + "Remain on the public side of the counter.", + "Do not open or read mail not addressed to you.", + "Every consignment removed requires a receipt.", + "The master copy receives the crown date stamp."]) + + P("UNRECEIPTED MATERIAL WILL BE CANCELLED.") +}, + +{ name: "6 · R-143-A receipt", content: + P("DEPARTMENTAL CUSTODY SERVICE
" + + "R-143-A / PENDING INTER-JURISDICTIONAL REGISTER") + + P("The undersigned accept temporary custody of the disputed folio and the living civic " + + "identities recorded within it, without determining final ownership, origin or " + + "jurisdiction.") + + TABLE(["Requirement", "Name / finding", "Signature"], [ + ["Serving officer accepting responsibility", "", ""], + ["Containment established", "", ""], + ["Terms agreed with transferring authority", "", ""], + ["Living status medically certified", "", ""], + ["Window Six repaired and current date applied", "", ""], + ["Crown master stamp recovered", "", ""] + ]) + + P("Case status: pending adjudication
" + + "Collection date: ______________
" + + "Crown stamp: ______________") +}, + +{ name: "7 · Debrief slip", content: + P("Complete one sentence before speaking to the debriefing officer.") + + UL(["The case stopped matching our orders when ____________________.", + "The person or thing I protected was ____________________.", + "The fact I would remove from the official report is ____________________.", + "The name we knowingly left off the form was ____________________."]) +} +]; + +/* ---------------------------------------------------------------- */ +/* The team, the cast, the case file and the credential */ +/* ---------------------------------------------------------------- */ + +/** + * The six pregens. Built through the same buildActor path the shipped pregens use, so + * they arrive with skills, kit, hit points and a token exactly like any other agent. + * Ratings are fixed rather than rolled so every table gets the same starter. + */ +export const STARTER_TEAM = [ + { key: "pw_nia", name: "Nia Mercer", role: "lead", rank: "Field Officer", + chosenStyle: "reflex", species: "baseline", age: { chronological: 36, physiological: 36 }, + ch: { str: 11, siz: 12, con: 13, int: 14, pow: 13, dex: 15, cha: 13, edu: 12 }, + talents: ["first_through", "read_room"], + spotlight: "Keep the operation coherent, lead the case phases and sign for the final decision.", + complication: "Nia's cover identity says she worked a Christmas shift at this office in 1987. She was born in 1989.", + note: "Give Nia the highest Tradecraft on the team. Being in charge means deciding who acts, not making every roll." }, + + { key: "pw_callum", name: "Callum Pike", role: "heavy", rank: "Field Officer", + chosenStyle: "reflex", species: "baseline", age: { chronological: 34, physiological: 34 }, + ch: { str: 16, siz: 15, con: 15, int: 11, pow: 11, dex: 13, cha: 10, edu: 10 }, + talents: ["hard_landing", "first_through"], + spotlight: "Hold the corridor, protect the folio carrier and restrain rather than destroy.", + complication: "The Cancellation Man wears Callum's handwriting on several of its envelopes.", + note: "Callum starts with the riot shield SLUNG. Raising it is a real decision: torso and arms improve, and his own shooting takes -20%." }, + + { key: "pw_amira", name: "Amira Bell", role: "settlements", rank: "Field Officer", + chosenStyle: "deliberate", species: "baseline", age: { chronological: 41, physiological: 41 }, + ch: { str: 10, siz: 11, con: 12, int: 15, pow: 14, dex: 11, cha: 16, edu: 15 }, + talents: ["read_room", "debriefer"], + spotlight: "Understand what the clerks need and make an arrangement reality will accept.", + complication: "Amira knows the agency can lawfully abandon everyone on the other side. Her orders strongly imply that it should.", + note: "Amira runs READ, SIGNAL and OFFER. The correct offer is a receipt, not a bribe." }, + + { key: "pw_elin", name: "Dr Elin Shaw", trade: "doctor", rank: "Probationary", + chosenStyle: "deliberate", species: "baseline", age: { chronological: 31, physiological: 31 }, + ch: { str: 10, siz: 11, con: 11, int: 16, pow: 12, dex: 12, cha: 13, edu: 17 }, + talents: ["triage", "unblinking"], + spotlight: "Establish that both Lorna Vales are living people, treat injuries and certify the disputed names.", + complication: "One delivered letter is a correctly formatted notice of Elin's death, dated nine years from now.", + note: "A proper doctor and a new Custodian. Her Medicine is excellent and her induction — Tradecraft, Anomaly Lore, Dodge, Pistol — is not." }, + + { key: "pw_owen", name: "Owen Dey", trade: "engineer", rank: "Probationary", + chosenStyle: "reflex", species: "baseline", age: { chronological: 29, physiological: 29 }, + ch: { str: 12, siz: 12, con: 12, int: 17, pow: 11, dex: 14, cha: 10, edu: 15 }, + talents: ["hard_landing", "requisition"], + spotlight: "Diagnose Window Six, repair its franking mechanism and keep the crossing stable.", + complication: "The mechanism has been repaired before, using Owen's preferred soldering pattern.", + note: "Diagnosing the franker early gives +20% on the repair that closes the case." }, + + { key: "pw_sennen", name: "Sennen Roake", trade: "criminal", rank: "Probationary", + chosenStyle: "reflex", species: "baseline", age: { chronological: 27, physiological: 27 }, + ch: { str: 11, siz: 10, con: 12, int: 13, pow: 11, dex: 17, cha: 14, edu: 9 }, + talents: ["cover_work", "clean_scene"], + spotlight: "Enter the cage without breaking its rules and recover the master date stamp.", + complication: "The mundane police database recognises Sennen's fingerprints under the name of a postal worker who died in the 1987 fire.", + note: "The cage is the tutorial for doing a criminal thing procedurally: the drawer opens from the public side, and that matters." } +]; + +/** The cast, as statblocks a GM can drag onto a scene. */ +export const STARTER_CAST = [ + { key: "pw_evelyn", name: "Evelyn Vale", role: "FOLKLORE. Night supervisor, Civil Continuity Registry", + chosenStyle: "deliberate", naturalArmour: 0, species: "baseline", + ch: { str: 10, siz: 11, con: 12, int: 15, pow: 16, dex: 11, cha: 15, edu: 16 }, + skills: [{ fam: "insight", spec: "", val: 75 }, { fam: "status", spec: "", val: 70 }, + { fam: "bargain", spec: "", val: 65 }, { fam: "knowledge", spec: "", val: 70 }, + { fam: "listen", spec: "", val: 60 }, { fam: "persuade", spec: "", val: 55 }], + weapons: ["punch"], + tactics: "Formal, tired and exact. She wants a valid receipt more than she wants rescue, and she has wanted it for thirty-nine years. She does not lie; she answers only the question procedurally put to her, which is not the same thing and the players will notice. POWER: THE COUNTER. Anything said from behind her counter by somebody who does not work there is treated as unsaid — no roll, no argument. Agents who work this out can use it deliberately." }, + + { key: "pw_lorna_m", name: "Lorna Vale (mundane)", role: "FOLKLORE. Council payroll officer, forty-two", + chosenStyle: "reflex", naturalArmour: 0, species: "baseline", + ch: { str: 10, siz: 11, con: 11, int: 12, pow: 11, dex: 11, cha: 12, edu: 12 }, + skills: [{ fam: "knowledge", spec: "", val: 45 }, { fam: "insight", spec: "", val: 40 }, + { fam: "status", spec: "", val: 35 }, { fam: "listen", spec: "", val: 40 }], + weapons: ["punch"], + tactics: "Angry about being treated as evidence. Wants her life AND the truth, and will not accept being handed either one instead of the other. She lies when she says she has no curiosity about the other Lorna's daughter." }, + + { key: "pw_lorna_r", name: "Lorna Vale (Registry)", role: "FOLKLORE. Registry clerk, thirty-nine years on one shift", + chosenStyle: "reflex", naturalArmour: 0, species: "baseline", + ch: { str: 10, siz: 11, con: 11, int: 12, pow: 13, dex: 11, cha: 12, edu: 13 }, + skills: [{ fam: "knowledge", spec: "", val: 55 }, { fam: "insight", spec: "", val: 50 }, + { fam: "listen", spec: "", val: 45 }, { fam: "research", spec: "", val: 50 }], + weapons: ["punch"], + tactics: "Wants the shift to end without taking her existence with it. Remembers a daughter born in the Near world. She lies when she says she has never sent a letter to the mundane world before. Asked which Lorna is real, either will answer: \"Both, until you file otherwise.\"" }, + + { key: "pw_gareth", name: "Gareth Moss", role: "FOLKLORE. Demolition site manager", + chosenStyle: "reflex", naturalArmour: 0, species: "baseline", + ch: { str: 13, siz: 13, con: 13, int: 12, pow: 10, dex: 11, cha: 12, edu: 11 }, + skills: [{ fam: "repair", spec: "mechanical", val: 55 }, { fam: "status", spec: "", val: 40 }, + { fam: "spot", spec: "", val: 40 }, { fam: "bargain", spec: "", val: 45 }], + weapons: ["punch"], + tactics: "Practical, not obstructive. Wants a signed instruction and somebody else liable for the delay. Treat him as a professional and he gives up everything he knows without a roll. He lies exactly once: when he says he never looked through Window Six." }, + + { key: "pw_cancellation", name: "The Cancellation Man", role: "HORROR. A process with a body", + chosenStyle: "reflex", naturalArmour: 4, species: "baseline", + ch: { str: 16, siz: 14, con: 16, int: 8, pow: 17, dex: 14, cha: 3, edu: 5 }, + skills: [{ fam: "brawl", spec: "", val: 65 }, { fam: "dodge", spec: "", val: 45 }, + { fam: "stealth", spec: "", val: 60 }, { fam: "spot", spec: "", val: 70 }, + { fam: "athletics", spec: "", val: 55 }], + weapons: ["grasp"], + tactics: "It unfolds from returned envelopes, red twine and black cancellation ink, and every address on it belongs to somebody in the room. It wants the DISPUTED FOLIO, not casualties: it moves to the carrier and ignores anyone who does not obstruct it. Rename its attack the CANCELLATION STAMP. POWER: UNRECEIPTED. Anything it takes that has no receipt attached is gone — not destroyed, never delivered. It withdraws at 2 hit points, when restrained, or the moment it is handed a properly bagged and receipted folio. Killing it is temporary; it reforms at the next clock advance. That is the lesson." } +]; + +/** The credential, as a real item rather than a paragraph in the brief. */ +export const STARTER_AUTHORITY = { + key: "pw_price", + name: "Imogen Price — Senior Asbestos Surveyor", + holderName: "Imogen Price", + holderRole: "Senior Asbestos Surveyor", + issuer: "Halcyon Environmental Compliance Ltd", + scope: "operate", + scopeNote: "Routine site inspection at a controlled demolition site.", + trust: 2, + redFlags: [ + "Price signs in at every controlled site.", + "Price never conducts an inspection alone.", + "Price photographs every opened door." + ], + description: + "

Inside its scope this works and costs 1 Trust: a surveyor walking a condemned " + + "building is not intrusion, it is Tuesday.

" + + "

Entering the sealed records cage or altering the demolition schedule is one " + + "tier beyond — roll, at −20% for each red flag broken.

" + + "

A fumble burns it: the real Imogen Price telephones the site manager.

" +}; + +/** The case file, already set to the scenario's opening state. */ +export const STARTER_CASE = { + key: "pw_case", + name: "PW-01 · Rookery Heath", + containment: 8, resources: 8, clearance: 6, + destination: "Rookery Heath Delivery Office, Coventry", + notes: + "

Last Collection clock 0 / 4. Advance it on a fumbled phase, twenty " + + "minutes of argument without action, a loud weapon fired in the mundane building, the " + + "credential burned, or the clerks' taboo violated after it has been learned.

" + + "

Not for careful investigation. Never for careful investigation.

", + legs: [ + { label: "Approach — the demolition cordon", kindId: "approach", difficulty: "average", mod: 0 }, + { label: "Investigation — the dead-letter cage", kindId: "cordon", difficulty: "average", mod: -5 }, + { label: "Containment — the night shift", kindId: "containment", difficulty: "difficult", mod: 0 }, + { label: "Extraction — closing the register", kindId: "extraction", difficulty: "average", mod: -15 } + ] +}; diff --git a/tools/update-readme.mjs b/tools/update-readme.mjs index e3e93bc..6c0d163 100644 --- a/tools/update-readme.mjs +++ b/tools/update-readme.mjs @@ -40,7 +40,8 @@ const box = `| | | | **Bestiary** | ${NPCS.length} statblocks across folklore, horror and the far side | | **Eras** | modern, antique, future, anomalous — the agency is old and never abolishes anything | | **Icons** | ${Object.keys(ICON_MAP).length}, all generated from \`tools/icons.mjs\`; no external art | -| **Compendia** | pregens, NPCs, skills, weapons, armour, gear, vehicles, powers, tables, rules, a starter case | +| **Starter scenario** | **PAPER WINDOW**, a six-player introductory case, importable as a single Adventure | +| **Compendia** | pregens, NPCs, skills, weapons, armour, gear, vehicles, powers, tables, rules, the starter | | **Rules journal** | ${RULES_PAGES.length} pages, generated from \`rules.mjs\` so the book cannot drift from the code |`; const replaceRegion = (src, marker, body) => {