diff --git a/README.md b/README.md index b09ae2c..cf3be88 100644 --- a/README.md +++ b/README.md @@ -65,13 +65,13 @@ changing underneath a running server. | **Postings** | 25, each with a key characteristic, trained skills, talents and its own kit | | **Trades** | 10 ordinary jobs the department recruits from, plus induction | | **Catalogue** | 236 items — 123 gear, 62 weapons, 32 armour, 19 vehicles | -| **Skills / talents** | 59 skills, 19 talents | +| **Skills / talents** | 62 skills, 19 talents | | **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 | | **Starter scenario** | **BLACK PLATFORM**, a six-player action starter, 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 | +| **Rules journal** | 13 pages, generated from `rules.mjs` so the book cannot drift from the code | --- @@ -180,9 +180,9 @@ The build runs all five guards before it writes anything: ``` -check-rules: OK — 87 rules, 20 files scanned, 3 aliases + 41 constant sets checked, 275 formulas verified +check-rules: OK — 92 rules, 20 files scanned, 3 aliases + 45 constant sets checked, 290 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, 945 keys, no leaf/branch collisions -check-behaviour: OK — 46 behavioural tests +check-lang: OK — en.json, 976 keys, no leaf/branch collisions +check-behaviour: OK — 55 behavioural tests ``` diff --git a/lang/en.json b/lang/en.json index 6e20b38..ca76973 100644 --- a/lang/en.json +++ b/lang/en.json @@ -943,5 +943,36 @@ "RINGBRP.Item.ArtRating": "Rating", "RINGBRP.Item.PushCost": "Push cost", "RINGBRP.Item.PushRule": "What pushing does", - "RINGBRP.Item.Origin": "Where it came from" + "RINGBRP.Item.Origin": "Where it came from", + "RINGBRP.Panic.Label": "Panic", + "RINGBRP.Panic.Steady": "Steady", + "RINGBRP.Panic.SteadyNote": "You are all right. You are doing the job.", + "RINGBRP.Panic.Rattled": "Rattled", + "RINGBRP.Panic.RattledNote": "Your hands are not quite steady. No penalty — the first one is free.", + "RINGBRP.Panic.Shaken": "Shaken", + "RINGBRP.Panic.ShakenNote": "−5% to everything. You keep checking the way you came in.", + "RINGBRP.Panic.Losing": "Losing it", + "RINGBRP.Panic.LosingNote": "−10% to everything. You are talking too fast and not hearing answers.", + "RINGBRP.Panic.Overwhelmed": "Overwhelmed", + "RINGBRP.Panic.OverwhelmedNote": "−20% to everything. One more and you go.", + "RINGBRP.Panic.Broken": "Broken", + "RINGBRP.Panic.BrokenNote": "You freeze, run, or go at it. Choose one, take it, and come back at 3.", + "RINGBRP.Panic.Trig.WrongThing": "Something that should not be there", + "RINGBRP.Panic.Trig.Touched": "It touched you", + "RINGBRP.Panic.Trig.Body": "A body", + "RINGBRP.Panic.Trig.Known": "Somebody you knew", + "RINGBRP.Panic.Trig.Dark": "The lights went", + "RINGBRP.Panic.Trig.Wounded": "A major wound", + "RINGBRP.Panic.Trig.Named": "It said your name", + "RINGBRP.Panic.Relief.Steady": "Steady yourself (POW×5)", + "RINGBRP.Panic.Relief.Religion": "Religion — something older than the department", + "RINGBRP.Panic.Relief.Psychology": "Psychology — talk somebody else down", + "RINGBRP.Panic.Relief.Occult": "Occult — name it, out loud, for everyone who can hear", + "RINGBRP.Panic.Relief.Anchor": "Your anchor, once per case", + "RINGBRP.Panic.Gained": "{name} — panic {from} → {to}", + "RINGBRP.Panic.Eased": "{name} — panic {from} → {to}", + "RINGBRP.Panic.Breaks": "BREAKS — freeze, flee, or fixate. Choose, take it, come back at 3.", + "RINGBRP.Panic.NoChange": "No change.", + "RINGBRP.Flavour.Panicking": "panicking ({mod}%)", + "RINGBRP.Chat.ToEverything": "to everything" } diff --git a/packs/armour/000003.log b/packs/armour/000003.log new file mode 100644 index 0000000..9b6d27d Binary files /dev/null and b/packs/armour/000003.log differ diff --git a/packs/armour/000005.ldb b/packs/armour/000005.ldb deleted file mode 100644 index c982555..0000000 Binary files a/packs/armour/000005.ldb and /dev/null differ diff --git a/packs/armour/000006.log b/packs/armour/000006.log deleted file mode 100644 index 1a1bb27..0000000 Binary files a/packs/armour/000006.log and /dev/null differ diff --git a/packs/armour/CURRENT b/packs/armour/CURRENT index cacca75..1a84852 100644 --- a/packs/armour/CURRENT +++ b/packs/armour/CURRENT @@ -1 +1 @@ -MANIFEST-000004 +MANIFEST-000002 diff --git a/packs/armour/LOG b/packs/armour/LOG index c7e3f23..9679cb5 100644 --- a/packs/armour/LOG +++ b/packs/armour/LOG @@ -1,5 +1 @@ -2026/08/12-22:44:48.920895 172817000 Recovering log #3 -2026/08/12-22:44:48.921047 172817000 Level-0 table #5: started -2026/08/12-22:44:48.921223 172817000 Level-0 table #5: 4591 bytes OK -2026/08/12-22:44:48.921517 172817000 Delete type=0 #3 -2026/08/12-22:44:48.921554 172817000 Delete type=3 #2 +2026/08/12-22:54:48.028748 16ea07000 Delete type=3 #1 diff --git a/packs/armour/LOG.old b/packs/armour/LOG.old deleted file mode 100644 index 7792940..0000000 --- a/packs/armour/LOG.old +++ /dev/null @@ -1 +0,0 @@ -2026/08/12-22:44:33.287733 16f5db000 Delete type=3 #1 diff --git a/packs/armour/MANIFEST-000002 b/packs/armour/MANIFEST-000002 new file mode 100644 index 0000000..bbbc585 Binary files /dev/null and b/packs/armour/MANIFEST-000002 differ diff --git a/packs/armour/MANIFEST-000004 b/packs/armour/MANIFEST-000004 deleted file mode 100644 index 9c05997..0000000 Binary files a/packs/armour/MANIFEST-000004 and /dev/null differ diff --git a/packs/expedition/000003.log b/packs/expedition/000003.log new file mode 100644 index 0000000..263295b Binary files /dev/null and b/packs/expedition/000003.log differ diff --git a/packs/expedition/000005.ldb b/packs/expedition/000005.ldb deleted file mode 100644 index 4ff460d..0000000 Binary files a/packs/expedition/000005.ldb and /dev/null differ diff --git a/packs/expedition/000006.log b/packs/expedition/000006.log deleted file mode 100644 index 268b187..0000000 Binary files a/packs/expedition/000006.log and /dev/null differ diff --git a/packs/expedition/CURRENT b/packs/expedition/CURRENT index cacca75..1a84852 100644 --- a/packs/expedition/CURRENT +++ b/packs/expedition/CURRENT @@ -1 +1 @@ -MANIFEST-000004 +MANIFEST-000002 diff --git a/packs/expedition/LOG b/packs/expedition/LOG index 96ac7df..971eb80 100644 --- a/packs/expedition/LOG +++ b/packs/expedition/LOG @@ -1,5 +1 @@ -2026/08/12-22:44:48.946376 17382f000 Recovering log #3 -2026/08/12-22:44:48.946637 17382f000 Level-0 table #5: started -2026/08/12-22:44:48.946788 17382f000 Level-0 table #5: 4129 bytes OK -2026/08/12-22:44:48.947029 17382f000 Delete type=0 #3 -2026/08/12-22:44:48.947065 17382f000 Delete type=3 #2 +2026/08/12-22:54:48.107122 16f213000 Delete type=3 #1 diff --git a/packs/expedition/LOG.old b/packs/expedition/LOG.old deleted file mode 100644 index 7110bdd..0000000 --- a/packs/expedition/LOG.old +++ /dev/null @@ -1 +0,0 @@ -2026/08/12-22:44:33.362147 16edcf000 Delete type=3 #1 diff --git a/packs/expedition/MANIFEST-000002 b/packs/expedition/MANIFEST-000002 new file mode 100644 index 0000000..bbbc585 Binary files /dev/null and b/packs/expedition/MANIFEST-000002 differ diff --git a/packs/expedition/MANIFEST-000004 b/packs/expedition/MANIFEST-000004 deleted file mode 100644 index 47fbcd4..0000000 Binary files a/packs/expedition/MANIFEST-000004 and /dev/null differ diff --git a/packs/gear/000003.log b/packs/gear/000003.log new file mode 100644 index 0000000..629ba6d Binary files /dev/null and b/packs/gear/000003.log differ diff --git a/packs/gear/000005.ldb b/packs/gear/000005.ldb deleted file mode 100644 index d9bac03..0000000 Binary files a/packs/gear/000005.ldb and /dev/null differ diff --git a/packs/gear/000006.log b/packs/gear/000006.log deleted file mode 100644 index d1deb9b..0000000 Binary files a/packs/gear/000006.log and /dev/null differ diff --git a/packs/gear/CURRENT b/packs/gear/CURRENT index cacca75..1a84852 100644 --- a/packs/gear/CURRENT +++ b/packs/gear/CURRENT @@ -1 +1 @@ -MANIFEST-000004 +MANIFEST-000002 diff --git a/packs/gear/LOG b/packs/gear/LOG index 7aade72..d9b5688 100644 --- a/packs/gear/LOG +++ b/packs/gear/LOG @@ -1,5 +1 @@ -2026/08/12-22:44:48.922471 173023000 Recovering log #3 -2026/08/12-22:44:48.923346 173023000 Level-0 table #5: started -2026/08/12-22:44:48.923621 173023000 Level-0 table #5: 31614 bytes OK -2026/08/12-22:44:48.923809 173023000 Delete type=0 #3 -2026/08/12-22:44:48.923836 173023000 Delete type=3 #2 +2026/08/12-22:54:48.030538 16e1fb000 Delete type=3 #1 diff --git a/packs/gear/LOG.old b/packs/gear/LOG.old deleted file mode 100644 index 44758ca..0000000 --- a/packs/gear/LOG.old +++ /dev/null @@ -1 +0,0 @@ -2026/08/12-22:44:33.290003 16f5db000 Delete type=3 #1 diff --git a/packs/gear/MANIFEST-000002 b/packs/gear/MANIFEST-000002 new file mode 100644 index 0000000..bbbc585 Binary files /dev/null and b/packs/gear/MANIFEST-000002 differ diff --git a/packs/gear/MANIFEST-000004 b/packs/gear/MANIFEST-000004 deleted file mode 100644 index f7cd73c..0000000 Binary files a/packs/gear/MANIFEST-000004 and /dev/null differ diff --git a/packs/lastadmission/000003.log b/packs/lastadmission/000003.log new file mode 100644 index 0000000..bfabd98 Binary files /dev/null and b/packs/lastadmission/000003.log differ diff --git a/packs/lastadmission/000005.ldb b/packs/lastadmission/000005.ldb deleted file mode 100644 index 8b1bbb9..0000000 Binary files a/packs/lastadmission/000005.ldb and /dev/null differ diff --git a/packs/lastadmission/000006.log b/packs/lastadmission/000006.log deleted file mode 100644 index 670254b..0000000 Binary files a/packs/lastadmission/000006.log and /dev/null differ diff --git a/packs/lastadmission/CURRENT b/packs/lastadmission/CURRENT index cacca75..1a84852 100644 --- a/packs/lastadmission/CURRENT +++ b/packs/lastadmission/CURRENT @@ -1 +1 @@ -MANIFEST-000004 +MANIFEST-000002 diff --git a/packs/lastadmission/LOG b/packs/lastadmission/LOG index bc3d885..f41b510 100644 --- a/packs/lastadmission/LOG +++ b/packs/lastadmission/LOG @@ -1,5 +1 @@ -2026/08/12-22:44:48.936635 17200b000 Recovering log #3 -2026/08/12-22:44:48.937301 17200b000 Level-0 table #5: started -2026/08/12-22:44:48.937794 17200b000 Level-0 table #5: 45532 bytes OK -2026/08/12-22:44:48.938219 17200b000 Delete type=0 #3 -2026/08/12-22:44:48.938275 17200b000 Delete type=3 #2 +2026/08/12-22:54:48.091041 16ea07000 Delete type=3 #1 diff --git a/packs/lastadmission/LOG.old b/packs/lastadmission/LOG.old deleted file mode 100644 index 4a3b718..0000000 --- a/packs/lastadmission/LOG.old +++ /dev/null @@ -1 +0,0 @@ -2026/08/12-22:44:33.345401 16e5c3000 Delete type=3 #1 diff --git a/packs/lastadmission/MANIFEST-000002 b/packs/lastadmission/MANIFEST-000002 new file mode 100644 index 0000000..bbbc585 Binary files /dev/null and b/packs/lastadmission/MANIFEST-000002 differ diff --git a/packs/lastadmission/MANIFEST-000004 b/packs/lastadmission/MANIFEST-000004 deleted file mode 100644 index bd10fee..0000000 Binary files a/packs/lastadmission/MANIFEST-000004 and /dev/null differ diff --git a/packs/npcs/000003.log b/packs/npcs/000003.log new file mode 100644 index 0000000..535c9fd Binary files /dev/null and b/packs/npcs/000003.log differ diff --git a/packs/npcs/000005.ldb b/packs/npcs/000005.ldb deleted file mode 100644 index 29f69de..0000000 Binary files a/packs/npcs/000005.ldb and /dev/null differ diff --git a/packs/npcs/000006.log b/packs/npcs/000006.log deleted file mode 100644 index ac2ef10..0000000 Binary files a/packs/npcs/000006.log and /dev/null differ diff --git a/packs/npcs/CURRENT b/packs/npcs/CURRENT index cacca75..1a84852 100644 --- a/packs/npcs/CURRENT +++ b/packs/npcs/CURRENT @@ -1 +1 @@ -MANIFEST-000004 +MANIFEST-000002 diff --git a/packs/npcs/LOG b/packs/npcs/LOG index b5e9426..bfe79db 100644 --- a/packs/npcs/LOG +++ b/packs/npcs/LOG @@ -1,5 +1 @@ -2026/08/12-22:44:48.907196 173023000 Recovering log #3 -2026/08/12-22:44:48.908927 173023000 Level-0 table #5: started -2026/08/12-22:44:48.910392 173023000 Level-0 table #5: 225945 bytes OK -2026/08/12-22:44:48.910874 173023000 Delete type=0 #3 -2026/08/12-22:44:48.910934 173023000 Delete type=3 #2 +2026/08/12-22:54:48.072716 16f213000 Delete type=3 #1 diff --git a/packs/npcs/LOG.old b/packs/npcs/LOG.old deleted file mode 100644 index 4af5cd8..0000000 --- a/packs/npcs/LOG.old +++ /dev/null @@ -1 +0,0 @@ -2026/08/12-22:44:33.327015 16f5db000 Delete type=3 #1 diff --git a/packs/npcs/MANIFEST-000002 b/packs/npcs/MANIFEST-000002 new file mode 100644 index 0000000..bbbc585 Binary files /dev/null and b/packs/npcs/MANIFEST-000002 differ diff --git a/packs/npcs/MANIFEST-000004 b/packs/npcs/MANIFEST-000004 deleted file mode 100644 index 116e469..0000000 Binary files a/packs/npcs/MANIFEST-000004 and /dev/null differ diff --git a/packs/powers/000003.log b/packs/powers/000003.log new file mode 100644 index 0000000..d723043 Binary files /dev/null and b/packs/powers/000003.log differ diff --git a/packs/powers/000005.ldb b/packs/powers/000005.ldb deleted file mode 100644 index 7f540c6..0000000 Binary files a/packs/powers/000005.ldb and /dev/null differ diff --git a/packs/powers/000006.log b/packs/powers/000006.log deleted file mode 100644 index 43e306f..0000000 Binary files a/packs/powers/000006.log and /dev/null differ diff --git a/packs/powers/CURRENT b/packs/powers/CURRENT index cacca75..1a84852 100644 --- a/packs/powers/CURRENT +++ b/packs/powers/CURRENT @@ -1 +1 @@ -MANIFEST-000004 +MANIFEST-000002 diff --git a/packs/powers/LOG b/packs/powers/LOG index c9b9114..0181288 100644 --- a/packs/powers/LOG +++ b/packs/powers/LOG @@ -1,5 +1 @@ -2026/08/12-22:44:48.917345 172817000 Recovering log #3 -2026/08/12-22:44:48.917660 172817000 Level-0 table #5: started -2026/08/12-22:44:48.917794 172817000 Level-0 table #5: 3295 bytes OK -2026/08/12-22:44:48.918046 172817000 Delete type=0 #3 -2026/08/12-22:44:48.918081 172817000 Delete type=3 #2 +2026/08/12-22:54:48.101253 16ea07000 Delete type=3 #1 diff --git a/packs/powers/LOG.old b/packs/powers/LOG.old deleted file mode 100644 index e06ec38..0000000 --- a/packs/powers/LOG.old +++ /dev/null @@ -1 +0,0 @@ -2026/08/12-22:44:33.356982 16edcf000 Delete type=3 #1 diff --git a/packs/powers/MANIFEST-000002 b/packs/powers/MANIFEST-000002 new file mode 100644 index 0000000..bbbc585 Binary files /dev/null and b/packs/powers/MANIFEST-000002 differ diff --git a/packs/powers/MANIFEST-000004 b/packs/powers/MANIFEST-000004 deleted file mode 100644 index 20de082..0000000 Binary files a/packs/powers/MANIFEST-000004 and /dev/null differ diff --git a/packs/pregens/000003.log b/packs/pregens/000003.log new file mode 100644 index 0000000..5245fc9 Binary files /dev/null and b/packs/pregens/000003.log differ diff --git a/packs/pregens/000005.ldb b/packs/pregens/000005.ldb deleted file mode 100644 index 88aa716..0000000 Binary files a/packs/pregens/000005.ldb and /dev/null differ diff --git a/packs/pregens/000006.log b/packs/pregens/000006.log deleted file mode 100644 index ccd9db2..0000000 Binary files a/packs/pregens/000006.log and /dev/null differ diff --git a/packs/pregens/CURRENT b/packs/pregens/CURRENT index cacca75..1a84852 100644 --- a/packs/pregens/CURRENT +++ b/packs/pregens/CURRENT @@ -1 +1 @@ -MANIFEST-000004 +MANIFEST-000002 diff --git a/packs/pregens/LOG b/packs/pregens/LOG index f879377..3f4b771 100644 --- a/packs/pregens/LOG +++ b/packs/pregens/LOG @@ -1,5 +1 @@ -2026/08/12-22:44:48.896053 17200b000 Recovering log #3 -2026/08/12-22:44:48.898901 17200b000 Level-0 table #5: started -2026/08/12-22:44:48.902279 17200b000 Level-0 table #5: 545293 bytes OK -2026/08/12-22:44:48.902701 17200b000 Delete type=0 #3 -2026/08/12-22:44:48.902787 17200b000 Delete type=3 #2 +2026/08/12-22:54:48.053348 16f213000 Delete type=3 #1 diff --git a/packs/pregens/LOG.old b/packs/pregens/LOG.old deleted file mode 100644 index a390901..0000000 --- a/packs/pregens/LOG.old +++ /dev/null @@ -1 +0,0 @@ -2026/08/12-22:44:33.311753 16e5c3000 Delete type=3 #1 diff --git a/packs/pregens/MANIFEST-000002 b/packs/pregens/MANIFEST-000002 new file mode 100644 index 0000000..bbbc585 Binary files /dev/null and b/packs/pregens/MANIFEST-000002 differ diff --git a/packs/pregens/MANIFEST-000004 b/packs/pregens/MANIFEST-000004 deleted file mode 100644 index 7b13493..0000000 Binary files a/packs/pregens/MANIFEST-000004 and /dev/null differ diff --git a/packs/primer/000003.log b/packs/primer/000003.log new file mode 100644 index 0000000..fff1285 Binary files /dev/null and b/packs/primer/000003.log differ diff --git a/packs/primer/000005.ldb b/packs/primer/000005.ldb deleted file mode 100644 index 669e7b1..0000000 Binary files a/packs/primer/000005.ldb and /dev/null differ diff --git a/packs/primer/000006.log b/packs/primer/000006.log deleted file mode 100644 index daa37bb..0000000 Binary files a/packs/primer/000006.log and /dev/null differ diff --git a/packs/primer/CURRENT b/packs/primer/CURRENT index cacca75..1a84852 100644 --- a/packs/primer/CURRENT +++ b/packs/primer/CURRENT @@ -1 +1 @@ -MANIFEST-000004 +MANIFEST-000002 diff --git a/packs/primer/LOG b/packs/primer/LOG index 186daa8..90e6c20 100644 --- a/packs/primer/LOG +++ b/packs/primer/LOG @@ -1,5 +1 @@ -2026/08/12-22:44:48.925841 17382f000 Recovering log #3 -2026/08/12-22:44:48.926073 17382f000 Level-0 table #5: started -2026/08/12-22:44:48.926304 17382f000 Level-0 table #5: 21613 bytes OK -2026/08/12-22:44:48.926557 17382f000 Delete type=0 #3 -2026/08/12-22:44:48.926598 17382f000 Delete type=3 #2 +2026/08/12-22:54:48.098549 16ea07000 Delete type=3 #1 diff --git a/packs/primer/LOG.old b/packs/primer/LOG.old deleted file mode 100644 index 72124e1..0000000 --- a/packs/primer/LOG.old +++ /dev/null @@ -1 +0,0 @@ -2026/08/12-22:44:33.353318 16edcf000 Delete type=3 #1 diff --git a/packs/primer/MANIFEST-000002 b/packs/primer/MANIFEST-000002 new file mode 100644 index 0000000..bbbc585 Binary files /dev/null and b/packs/primer/MANIFEST-000002 differ diff --git a/packs/primer/MANIFEST-000004 b/packs/primer/MANIFEST-000004 deleted file mode 100644 index 2ce4ec2..0000000 Binary files a/packs/primer/MANIFEST-000004 and /dev/null differ diff --git a/packs/prologue/000003.log b/packs/prologue/000003.log new file mode 100644 index 0000000..122ec89 Binary files /dev/null and b/packs/prologue/000003.log differ diff --git a/packs/prologue/000005.ldb b/packs/prologue/000005.ldb deleted file mode 100644 index 03d2f7f..0000000 Binary files a/packs/prologue/000005.ldb and /dev/null differ diff --git a/packs/prologue/000006.log b/packs/prologue/000006.log deleted file mode 100644 index 2d03e81..0000000 Binary files a/packs/prologue/000006.log and /dev/null differ diff --git a/packs/prologue/CURRENT b/packs/prologue/CURRENT index cacca75..1a84852 100644 --- a/packs/prologue/CURRENT +++ b/packs/prologue/CURRENT @@ -1 +1 @@ -MANIFEST-000004 +MANIFEST-000002 diff --git a/packs/prologue/LOG b/packs/prologue/LOG index 44b46d1..7a65a61 100644 --- a/packs/prologue/LOG +++ b/packs/prologue/LOG @@ -1,5 +1 @@ -2026/08/12-22:44:48.939398 17200b000 Recovering log #3 -2026/08/12-22:44:48.940505 17200b000 Level-0 table #5: started -2026/08/12-22:44:48.941454 17200b000 Level-0 table #5: 43953 bytes OK -2026/08/12-22:44:48.942198 17200b000 Delete type=0 #3 -2026/08/12-22:44:48.942249 17200b000 Delete type=3 #2 +2026/08/12-22:54:48.095111 16f213000 Delete type=3 #1 diff --git a/packs/prologue/LOG.old b/packs/prologue/LOG.old deleted file mode 100644 index 200e76c..0000000 --- a/packs/prologue/LOG.old +++ /dev/null @@ -1 +0,0 @@ -2026/08/12-22:44:33.349699 16f5db000 Delete type=3 #1 diff --git a/packs/prologue/MANIFEST-000002 b/packs/prologue/MANIFEST-000002 new file mode 100644 index 0000000..bbbc585 Binary files /dev/null and b/packs/prologue/MANIFEST-000002 differ diff --git a/packs/prologue/MANIFEST-000004 b/packs/prologue/MANIFEST-000004 deleted file mode 100644 index 8e0eaa0..0000000 Binary files a/packs/prologue/MANIFEST-000004 and /dev/null differ diff --git a/packs/rules/000003.log b/packs/rules/000003.log new file mode 100644 index 0000000..a3b0e4b Binary files /dev/null and b/packs/rules/000003.log differ diff --git a/packs/rules/000005.ldb b/packs/rules/000005.ldb deleted file mode 100644 index c8f9c7d..0000000 Binary files a/packs/rules/000005.ldb and /dev/null differ diff --git a/packs/rules/000006.log b/packs/rules/000006.log deleted file mode 100644 index 7b08998..0000000 Binary files a/packs/rules/000006.log and /dev/null differ diff --git a/packs/rules/CURRENT b/packs/rules/CURRENT index cacca75..1a84852 100644 --- a/packs/rules/CURRENT +++ b/packs/rules/CURRENT @@ -1 +1 @@ -MANIFEST-000004 +MANIFEST-000002 diff --git a/packs/rules/LOG b/packs/rules/LOG index 9f296bb..c02b27a 100644 --- a/packs/rules/LOG +++ b/packs/rules/LOG @@ -1,5 +1 @@ -2026/08/12-22:44:48.927292 172817000 Recovering log #3 -2026/08/12-22:44:48.927647 172817000 Level-0 table #5: started -2026/08/12-22:44:48.928175 172817000 Level-0 table #5: 33484 bytes OK -2026/08/12-22:44:48.928522 172817000 Delete type=0 #3 -2026/08/12-22:44:48.928579 172817000 Delete type=3 #2 +2026/08/12-22:54:48.104875 16fa1f000 Delete type=3 #1 diff --git a/packs/rules/LOG.old b/packs/rules/LOG.old deleted file mode 100644 index c8e350f..0000000 --- a/packs/rules/LOG.old +++ /dev/null @@ -1 +0,0 @@ -2026/08/12-22:44:33.360431 16e5c3000 Delete type=3 #1 diff --git a/packs/rules/MANIFEST-000002 b/packs/rules/MANIFEST-000002 new file mode 100644 index 0000000..bbbc585 Binary files /dev/null and b/packs/rules/MANIFEST-000002 differ diff --git a/packs/rules/MANIFEST-000004 b/packs/rules/MANIFEST-000004 deleted file mode 100644 index 15f0607..0000000 Binary files a/packs/rules/MANIFEST-000004 and /dev/null differ diff --git a/packs/skills/000003.log b/packs/skills/000003.log new file mode 100644 index 0000000..d985ef5 Binary files /dev/null and b/packs/skills/000003.log differ diff --git a/packs/skills/000005.ldb b/packs/skills/000005.ldb deleted file mode 100644 index eb948ea..0000000 Binary files a/packs/skills/000005.ldb and /dev/null differ diff --git a/packs/skills/000006.log b/packs/skills/000006.log deleted file mode 100644 index 1989ee1..0000000 Binary files a/packs/skills/000006.log and /dev/null differ diff --git a/packs/skills/CURRENT b/packs/skills/CURRENT index cacca75..1a84852 100644 --- a/packs/skills/CURRENT +++ b/packs/skills/CURRENT @@ -1 +1 @@ -MANIFEST-000004 +MANIFEST-000002 diff --git a/packs/skills/LOG b/packs/skills/LOG index c52d119..6d830e7 100644 --- a/packs/skills/LOG +++ b/packs/skills/LOG @@ -1,5 +1 @@ -2026/08/12-22:44:48.913614 172817000 Recovering log #3 -2026/08/12-22:44:48.914288 172817000 Level-0 table #5: started -2026/08/12-22:44:48.914458 172817000 Level-0 table #5: 7590 bytes OK -2026/08/12-22:44:48.914739 172817000 Delete type=0 #3 -2026/08/12-22:44:48.914773 172817000 Delete type=3 #2 +2026/08/12-22:54:48.022621 16ea07000 Delete type=3 #1 diff --git a/packs/skills/LOG.old b/packs/skills/LOG.old deleted file mode 100644 index 6d0b24f..0000000 --- a/packs/skills/LOG.old +++ /dev/null @@ -1 +0,0 @@ -2026/08/12-22:44:33.281949 16edcf000 Delete type=3 #1 diff --git a/packs/skills/MANIFEST-000002 b/packs/skills/MANIFEST-000002 new file mode 100644 index 0000000..bbbc585 Binary files /dev/null and b/packs/skills/MANIFEST-000002 differ diff --git a/packs/skills/MANIFEST-000004 b/packs/skills/MANIFEST-000004 deleted file mode 100644 index 90efb97..0000000 Binary files a/packs/skills/MANIFEST-000004 and /dev/null differ diff --git a/packs/slice/000003.log b/packs/slice/000003.log new file mode 100644 index 0000000..39926e8 Binary files /dev/null and b/packs/slice/000003.log differ diff --git a/packs/slice/000005.ldb b/packs/slice/000005.ldb deleted file mode 100644 index ac4df01..0000000 Binary files a/packs/slice/000005.ldb and /dev/null differ diff --git a/packs/slice/000006.log b/packs/slice/000006.log deleted file mode 100644 index c25ac75..0000000 Binary files a/packs/slice/000006.log and /dev/null differ diff --git a/packs/slice/CURRENT b/packs/slice/CURRENT index cacca75..1a84852 100644 --- a/packs/slice/CURRENT +++ b/packs/slice/CURRENT @@ -1 +1 @@ -MANIFEST-000004 +MANIFEST-000002 diff --git a/packs/slice/LOG b/packs/slice/LOG index 7fbc5a6..79e0be8 100644 --- a/packs/slice/LOG +++ b/packs/slice/LOG @@ -1,5 +1 @@ -2026/08/12-22:44:48.943128 17200b000 Recovering log #3 -2026/08/12-22:44:48.943292 17200b000 Level-0 table #5: started -2026/08/12-22:44:48.943546 17200b000 Level-0 table #5: 16902 bytes OK -2026/08/12-22:44:48.943901 17200b000 Delete type=0 #3 -2026/08/12-22:44:48.943932 17200b000 Delete type=3 #2 +2026/08/12-22:54:48.100047 16e1fb000 Delete type=3 #1 diff --git a/packs/slice/LOG.old b/packs/slice/LOG.old deleted file mode 100644 index 67e4d93..0000000 --- a/packs/slice/LOG.old +++ /dev/null @@ -1 +0,0 @@ -2026/08/12-22:44:33.355279 16e5c3000 Delete type=3 #1 diff --git a/packs/slice/MANIFEST-000002 b/packs/slice/MANIFEST-000002 new file mode 100644 index 0000000..bbbc585 Binary files /dev/null and b/packs/slice/MANIFEST-000002 differ diff --git a/packs/slice/MANIFEST-000004 b/packs/slice/MANIFEST-000004 deleted file mode 100644 index 0966a50..0000000 Binary files a/packs/slice/MANIFEST-000004 and /dev/null differ diff --git a/packs/starter/000003.log b/packs/starter/000003.log new file mode 100644 index 0000000..5fb9735 Binary files /dev/null and b/packs/starter/000003.log differ diff --git a/packs/starter/000005.ldb b/packs/starter/000005.ldb deleted file mode 100644 index c0f9d9b..0000000 Binary files a/packs/starter/000005.ldb and /dev/null differ diff --git a/packs/starter/000006.log b/packs/starter/000006.log deleted file mode 100644 index aaf167c..0000000 Binary files a/packs/starter/000006.log and /dev/null differ diff --git a/packs/starter/CURRENT b/packs/starter/CURRENT index cacca75..1a84852 100644 --- a/packs/starter/CURRENT +++ b/packs/starter/CURRENT @@ -1 +1 @@ -MANIFEST-000004 +MANIFEST-000002 diff --git a/packs/starter/LOG b/packs/starter/LOG index fd4c1cc..e0190c8 100644 --- a/packs/starter/LOG +++ b/packs/starter/LOG @@ -1,5 +1 @@ -2026/08/12-22:44:48.931234 17200b000 Recovering log #3 -2026/08/12-22:44:48.932452 17200b000 Level-0 table #5: started -2026/08/12-22:44:48.933139 17200b000 Level-0 table #5: 82299 bytes OK -2026/08/12-22:44:48.933493 17200b000 Delete type=0 #3 -2026/08/12-22:44:48.933538 17200b000 Delete type=3 #2 +2026/08/12-22:54:48.085121 16ea07000 Delete type=3 #1 diff --git a/packs/starter/LOG.old b/packs/starter/LOG.old deleted file mode 100644 index c79d586..0000000 --- a/packs/starter/LOG.old +++ /dev/null @@ -1 +0,0 @@ -2026/08/12-22:44:33.337758 16f5db000 Delete type=3 #1 diff --git a/packs/starter/MANIFEST-000002 b/packs/starter/MANIFEST-000002 new file mode 100644 index 0000000..bbbc585 Binary files /dev/null and b/packs/starter/MANIFEST-000002 differ diff --git a/packs/starter/MANIFEST-000004 b/packs/starter/MANIFEST-000004 deleted file mode 100644 index 87687fc..0000000 Binary files a/packs/starter/MANIFEST-000004 and /dev/null differ diff --git a/packs/tables/000003.log b/packs/tables/000003.log new file mode 100644 index 0000000..f125b7d Binary files /dev/null and b/packs/tables/000003.log differ diff --git a/packs/tables/000005.ldb b/packs/tables/000005.ldb deleted file mode 100644 index 64baa1c..0000000 Binary files a/packs/tables/000005.ldb and /dev/null differ diff --git a/packs/tables/000006.log b/packs/tables/000006.log deleted file mode 100644 index d5e06f3..0000000 Binary files a/packs/tables/000006.log and /dev/null differ diff --git a/packs/tables/CURRENT b/packs/tables/CURRENT index cacca75..1a84852 100644 --- a/packs/tables/CURRENT +++ b/packs/tables/CURRENT @@ -1 +1 @@ -MANIFEST-000004 +MANIFEST-000002 diff --git a/packs/tables/LOG b/packs/tables/LOG index 937a7a9..6f35d78 100644 --- a/packs/tables/LOG +++ b/packs/tables/LOG @@ -1,5 +1 @@ -2026/08/12-22:44:48.944808 172817000 Recovering log #3 -2026/08/12-22:44:48.944973 172817000 Level-0 table #5: started -2026/08/12-22:44:48.945086 172817000 Level-0 table #5: 3739 bytes OK -2026/08/12-22:44:48.945317 172817000 Delete type=0 #3 -2026/08/12-22:44:48.945349 172817000 Delete type=3 #2 +2026/08/12-22:54:48.077691 16e1fb000 Delete type=3 #1 diff --git a/packs/tables/LOG.old b/packs/tables/LOG.old deleted file mode 100644 index d0fb875..0000000 --- a/packs/tables/LOG.old +++ /dev/null @@ -1 +0,0 @@ -2026/08/12-22:44:33.331276 16e5c3000 Delete type=3 #1 diff --git a/packs/tables/MANIFEST-000002 b/packs/tables/MANIFEST-000002 new file mode 100644 index 0000000..bbbc585 Binary files /dev/null and b/packs/tables/MANIFEST-000002 differ diff --git a/packs/tables/MANIFEST-000004 b/packs/tables/MANIFEST-000004 deleted file mode 100644 index 004ff30..0000000 Binary files a/packs/tables/MANIFEST-000004 and /dev/null differ diff --git a/packs/talents/000003.log b/packs/talents/000003.log new file mode 100644 index 0000000..443b7ea Binary files /dev/null and b/packs/talents/000003.log differ diff --git a/packs/talents/000005.ldb b/packs/talents/000005.ldb deleted file mode 100644 index fb653de..0000000 Binary files a/packs/talents/000005.ldb and /dev/null differ diff --git a/packs/talents/000006.log b/packs/talents/000006.log deleted file mode 100644 index 29003f7..0000000 Binary files a/packs/talents/000006.log and /dev/null differ diff --git a/packs/talents/CURRENT b/packs/talents/CURRENT index cacca75..1a84852 100644 --- a/packs/talents/CURRENT +++ b/packs/talents/CURRENT @@ -1 +1 @@ -MANIFEST-000004 +MANIFEST-000002 diff --git a/packs/talents/LOG b/packs/talents/LOG index 6204424..4ec26e8 100644 --- a/packs/talents/LOG +++ b/packs/talents/LOG @@ -1,5 +1 @@ -2026/08/12-22:44:48.915783 173023000 Recovering log #3 -2026/08/12-22:44:48.915922 173023000 Level-0 table #5: started -2026/08/12-22:44:48.916069 173023000 Level-0 table #5: 5770 bytes OK -2026/08/12-22:44:48.916313 173023000 Delete type=0 #3 -2026/08/12-22:44:48.916348 173023000 Delete type=3 #2 +2026/08/12-22:54:48.025116 16fa1f000 Delete type=3 #1 diff --git a/packs/talents/LOG.old b/packs/talents/LOG.old deleted file mode 100644 index f7759d7..0000000 --- a/packs/talents/LOG.old +++ /dev/null @@ -1 +0,0 @@ -2026/08/12-22:44:33.284515 16e5c3000 Delete type=3 #1 diff --git a/packs/talents/MANIFEST-000002 b/packs/talents/MANIFEST-000002 new file mode 100644 index 0000000..bbbc585 Binary files /dev/null and b/packs/talents/MANIFEST-000002 differ diff --git a/packs/talents/MANIFEST-000004 b/packs/talents/MANIFEST-000004 deleted file mode 100644 index f8c6b47..0000000 Binary files a/packs/talents/MANIFEST-000004 and /dev/null differ diff --git a/packs/vehicles/000003.log b/packs/vehicles/000003.log new file mode 100644 index 0000000..9950ce5 Binary files /dev/null and b/packs/vehicles/000003.log differ diff --git a/packs/vehicles/000005.ldb b/packs/vehicles/000005.ldb deleted file mode 100644 index f6b2cd4..0000000 Binary files a/packs/vehicles/000005.ldb and /dev/null differ diff --git a/packs/vehicles/000006.log b/packs/vehicles/000006.log deleted file mode 100644 index 47800e2..0000000 Binary files a/packs/vehicles/000006.log and /dev/null differ diff --git a/packs/vehicles/CURRENT b/packs/vehicles/CURRENT index cacca75..1a84852 100644 --- a/packs/vehicles/CURRENT +++ b/packs/vehicles/CURRENT @@ -1 +1 @@ -MANIFEST-000004 +MANIFEST-000002 diff --git a/packs/vehicles/LOG b/packs/vehicles/LOG index 8450cde..0da96d1 100644 --- a/packs/vehicles/LOG +++ b/packs/vehicles/LOG @@ -1,5 +1 @@ -2026/08/12-22:44:48.924550 17200b000 Recovering log #3 -2026/08/12-22:44:48.924684 17200b000 Level-0 table #5: started -2026/08/12-22:44:48.924837 17200b000 Level-0 table #5: 6939 bytes OK -2026/08/12-22:44:48.925120 17200b000 Delete type=0 #3 -2026/08/12-22:44:48.925163 17200b000 Delete type=3 #2 +2026/08/12-22:54:48.032886 16f213000 Delete type=3 #1 diff --git a/packs/vehicles/LOG.old b/packs/vehicles/LOG.old deleted file mode 100644 index 0e04bb5..0000000 --- a/packs/vehicles/LOG.old +++ /dev/null @@ -1 +0,0 @@ -2026/08/12-22:44:33.292038 16fde7000 Delete type=3 #1 diff --git a/packs/vehicles/MANIFEST-000002 b/packs/vehicles/MANIFEST-000002 new file mode 100644 index 0000000..bbbc585 Binary files /dev/null and b/packs/vehicles/MANIFEST-000002 differ diff --git a/packs/vehicles/MANIFEST-000004 b/packs/vehicles/MANIFEST-000004 deleted file mode 100644 index d4d73b7..0000000 Binary files a/packs/vehicles/MANIFEST-000004 and /dev/null differ diff --git a/packs/weapons/000003.log b/packs/weapons/000003.log new file mode 100644 index 0000000..e1488ed Binary files /dev/null and b/packs/weapons/000003.log differ diff --git a/packs/weapons/000005.ldb b/packs/weapons/000005.ldb deleted file mode 100644 index 742beba..0000000 Binary files a/packs/weapons/000005.ldb and /dev/null differ diff --git a/packs/weapons/000006.log b/packs/weapons/000006.log deleted file mode 100644 index 4d427a6..0000000 Binary files a/packs/weapons/000006.log and /dev/null differ diff --git a/packs/weapons/CURRENT b/packs/weapons/CURRENT index cacca75..1a84852 100644 --- a/packs/weapons/CURRENT +++ b/packs/weapons/CURRENT @@ -1 +1 @@ -MANIFEST-000004 +MANIFEST-000002 diff --git a/packs/weapons/LOG b/packs/weapons/LOG index 95ecea5..18b1cef 100644 --- a/packs/weapons/LOG +++ b/packs/weapons/LOG @@ -1,5 +1 @@ -2026/08/12-22:44:48.918709 173023000 Recovering log #3 -2026/08/12-22:44:48.919008 173023000 Level-0 table #5: started -2026/08/12-22:44:48.919255 173023000 Level-0 table #5: 14767 bytes OK -2026/08/12-22:44:48.919627 173023000 Delete type=0 #3 -2026/08/12-22:44:48.919674 173023000 Delete type=3 #2 +2026/08/12-22:54:48.027160 16f213000 Delete type=3 #1 diff --git a/packs/weapons/LOG.old b/packs/weapons/LOG.old deleted file mode 100644 index 8cdd7d9..0000000 --- a/packs/weapons/LOG.old +++ /dev/null @@ -1 +0,0 @@ -2026/08/12-22:44:33.286188 16e5c3000 Delete type=3 #1 diff --git a/packs/weapons/MANIFEST-000002 b/packs/weapons/MANIFEST-000002 new file mode 100644 index 0000000..bbbc585 Binary files /dev/null and b/packs/weapons/MANIFEST-000002 differ diff --git a/packs/weapons/MANIFEST-000004 b/packs/weapons/MANIFEST-000004 deleted file mode 100644 index 6a582af..0000000 Binary files a/packs/weapons/MANIFEST-000004 and /dev/null differ diff --git a/postings.mjs b/postings.mjs index 38bd71f..f1d5d40 100644 --- a/postings.mjs +++ b/postings.mjs @@ -60,7 +60,7 @@ export const ROLES = { registry: { label: "Registry Officer", blurb: "Keeps the record that a crossing is made against. Knows what the file says and, more usefully, what it used to say.", stat: "edu", - core: ["research","knowledge","anomaly_lore","language:own","computer_use","insight"], + core: ["occult","knowledge","anomaly_lore","language:own","computer_use","insight"], support: ["spot","art","first_aid","tradecraft","firearm:pistol"], talents: ["chain_custody","wrong_memory","debriefer","quorum"], kit: ["spectacles","notebook","handset","jacket","pistol_light", "carbon_book", "date_stamp", "seals"] }, @@ -82,7 +82,7 @@ export const ROLES = { interview: { label: "Interview Officer", blurb: "Sits with the witness afterwards. Patient, unhurried, and very hard to lie to twice.", stat: "cha", core: ["insight","persuade","listen","language:own","anthropology"], - support: ["fast_talk","spot","tradecraft","first_aid","firearm:pistol"], + support: ["psychology","spot","tradecraft","first_aid","firearm:pistol"], talents: ["debriefer","read_room","wrong_memory","unblinking"], kit: ["dictaphone","cover_id","notebook","jacket","pistol_light", "carbon_book", "vacuum_flask", "clipboard"] }, @@ -110,7 +110,7 @@ export const ROLES = { pathology: { label: "Pathology Officer", blurb: "Examines what is left. Establishes what a thing was, which is usually the question the case turns on.", stat: "int", core: ["medicine","science:biology","xenology:baseline","insight","research"], - support: ["first_aid","spot","anthropology","dodge","melee_weapon:knife"], + support: ["first_aid","spot","religion","dodge","melee_weapon:knife"], talents: ["triage","chain_custody","unblinking","clean_scene"], kit: ["chirurgeon_kit","gas_mask","envsuit","sample_case","dagger", "specimen_jars", "swab_kit", "barrier_gloves"] }, @@ -172,7 +172,7 @@ export const ROLES = { antiquities: { label: "Antiquities Officer", blurb: "An archaic posting the department never abolished. Old objects, older records, and the manners to ask about both.", stat: "edu", - core: ["knowledge","art","anomaly_lore","research","status"], + core: ["religion","art","anomaly_lore","research","status"], support: ["insight","spot","first_aid","melee_weapon:sword","firearm:pistol"], talents: ["chain_custody","wrong_memory","borrowed_hour","quorum"], kit: ["pocket_watch","tallow_candle","duelling_sword","brigandine","flintlock", "quill_ink", "sealing_wax", "sand_glass"] }, @@ -231,13 +231,13 @@ export const TRADES = { doctor: { label: "Doctor", blurb: "A&E, mostly. Used to people at their worst and to being the calmest thing in the room.", stat: "edu", core: ["medicine","first_aid","science:biology","insight","knowledge"], - support: ["research","status","persuade","listen","spot"], + support: ["research","psychology","persuade","listen","spot"], talents: ["triage","debriefer","unblinking","clean_scene"], kit: ["medkit","trauma_unit","analgesics","specimen_jars","swab_kit"] }, professor: { label: "Professor", blurb: "A chair in something narrow, and a reading list nobody else has finished. Argues for a living.", stat: "edu", - core: ["knowledge","research","anthropology","language:own","status"], + core: ["knowledge","research","occult","language:own","status"], support: ["insight","persuade","science:botany","listen","art"], talents: ["debriefer","read_room","requisition","wrong_memory"], kit: ["carbon_book","notebook","spectacles","clipboard","quill_ink"] }, diff --git a/ringbrp.mjs b/ringbrp.mjs index 50655aa..db91b1a 100644 --- a/ringbrp.mjs +++ b/ringbrp.mjs @@ -50,7 +50,9 @@ import { ARTS, powerCostFor, powerStartingRating, CROSSING, crossingArtPoolFor, crossingGrantsArt, crossingCostFor, REACTION, CIRCUMSTANCE, RANGE_LADDER, rangeBandFrom, MAJOR_WOUND_TABLE, - COHERENCE_BANDS, COHERENCE_COSTS, COHERENCE_RECOVERY, coherenceBandFrom + COHERENCE_BANDS, COHERENCE_COSTS, COHERENCE_RECOVERY, coherenceBandFrom, + PANIC_MAX, PANIC_BANDS, PANIC_TRIGGERS, PANIC_RELIEF, PANIC_BREAK, PANIC_AFTER_BREAK, + panicBandFrom, panicPenaltyFor, panicBreaksAt, panicAfter, panicReliefFor } from "./rules.mjs"; export { hitPointsFor, majorWoundFor, damageModifierFor, locationMaxHp, reflexRating, awarenessRating }; @@ -1030,6 +1032,17 @@ export async function skillRoll({ flavourLines = [...flavourLines, game.i18n.format("RINGBRP.Flavour.Encumbered", { mod: enc })]; } } + // Panic is the short track and it bites on everything, so it sits in the same stack + // the wound and encumbrance penalties already use — anything that computes a modifier + // and is not added here is a modifier nothing reads, which is this project's oldest + // way of shipping a rule that does not exist. + if (actor) { + const pan = panicPenaltyFor(actor.system?.panic?.value); + if (pan) { + situational = (Number(situational) || 0) + pan; + flavourLines = [...flavourLines, game.i18n.format("RINGBRP.Flavour.Panicking", { mod: pan })]; + } + } if (actor && !skipCoherence) { const coh = coherencePenaltyFor(actor.system?.coherence?.value, categoryId); if (coh) { @@ -1534,6 +1547,68 @@ export async function loseCoherence(actor, reasonKey, { amount = null, note = "" return bAfter; } +RINGBRP.panicBands = PANIC_BANDS; +RINGBRP.panicTriggers = PANIC_TRIGGERS; +RINGBRP.panicRelief = PANIC_RELIEF; + +export const panicBand = panicBandFrom; + +/** + * Take a point of panic. No roll: a die to find out whether you are frightened slows + * the scene and tells the player how to feel. The die is for getting rid of it. + */ +export async function gainPanic(actor, triggerKey, { amount = null, note = "" } = {}) { + if (!actor) return null; + const spec = PANIC_TRIGGERS[triggerKey]; + const cost = amount ?? spec?.cost ?? 1; + const before = Number(actor.system?.panic?.value ?? 0); + const after = panicAfter(before, cost); + const log = [...(actor.system?.panic?.log ?? []), { reason: triggerKey, cost, note }]; + await actor.update({ "system.panic.value": after, "system.panic.log": log }); + await panicCard(actor, before, after, game.i18n.localize(String(spec?.label ?? triggerKey ?? "")), note); + return panicBandFrom(after); +} + +/** + * Put a point back. `level` is the outcome of whatever was rolled — a special takes two + * — so every relief in PANIC_RELIEF runs through one function and cannot drift. + */ +export async function easePanic(actor, reliefKey, level = "success", { note = "" } = {}) { + if (!actor) return null; + const spec = PANIC_RELIEF[reliefKey]; + const back = panicReliefFor(level); + const before = Number(actor.system?.panic?.value ?? 0); + const after = panicAfter(before, -back); + await actor.update({ "system.panic.value": after }); + await panicCard(actor, before, after, game.i18n.localize(String(spec?.label ?? reliefKey ?? "")), note); + return panicBandFrom(after); +} + +/** Breaking spends the panic: you come back at 3, because it is not a rest. */ +export async function breakPanic(actor, choice = "freeze") { + if (!actor) return null; + const before = Number(actor.system?.panic?.value ?? 0); + const after = PANIC_AFTER_BREAK; + await actor.update({ "system.panic.value": after }); + await panicCard(actor, before, after, + game.i18n.localize("RINGBRP.Panic.Breaks"), + PANIC_BREAK.includes(choice) ? choice : "freeze"); + return panicBandFrom(after); +} + +async function panicCard(actor, before, after, reason, note) { + const band = panicBandFrom(after); + await ChatMessage.create({ + speaker: ChatMessage.getSpeaker({ actor }), + content: await rbRenderTemplate("systems/ringbrp/templates/chat-panic.hbs", { + actorName: actor.name, before, after, reason, note, + band, breaks: panicBreaksAt(after), penalty: panicPenaltyFor(after), + choices: PANIC_BREAK, + pips: Array.from({ length: PANIC_MAX }, (_, i) => i < after) + }) + }); +} + /** * Displacement. Reaching zero is not a slow slide to be nursed back; it is an * event. The character does not die — they go home to an Earth that is almost @@ -4754,6 +4829,9 @@ class RingBRPCharacterSheet extends ActorSheet { ctx.reaction = this._reactionRail(); ctx.readiness = this._readiness ?? { rating: 0 }; ctx.coherence = { band: coherenceBand(sys.coherence?.value) }; + ctx.panic = { band: panicBandFrom(sys.panic?.value), + penalty: panicPenaltyFor(sys.panic?.value), + pips: Array.from({ length: PANIC_MAX }, (_, i) => i < Number(sys.panic?.value ?? 0)) }; ctx.readinessSource = ctx.readiness.best >= ctx.readiness.intFloor ? game.i18n.localize("RINGBRP.React.ReadinessSkill") : game.i18n.localize("RINGBRP.React.ReadinessInt"); @@ -4847,6 +4925,18 @@ class RingBRPCharacterSheet extends ActorSheet { if (item) await useDeviceFunction(this.actor, item, btn.dataset.fn); this.render(false); })); + root.querySelectorAll("[data-action='panic-adjust']").forEach(el => + el.addEventListener("click", async ev => { + ev.preventDefault(); ev.stopPropagation(); + const delta = Number(ev.currentTarget.dataset.delta) || 0; + if (delta > 0) await gainPanic(this.actor, "wrongThing", { amount: 1 }); + else await easePanic(this.actor, "steadyYourself", "success"); + })); + root.querySelectorAll("[data-action='panic-break']").forEach(el => + el.addEventListener("click", async ev => { + ev.preventDefault(); ev.stopPropagation(); + await breakPanic(this.actor, ev.currentTarget.dataset.choice ?? "freeze"); + })); root.querySelectorAll("[data-action='coherence-adjust']").forEach(el => el.addEventListener("click", async ev => { ev.preventDefault(); ev.stopPropagation(); @@ -6085,6 +6175,7 @@ Hooks.once("init", () => { reactionLoadFrom, reactionPrintedFrom, readinessRating, percentileFaces, circumstanceTotal, coherenceBand, coherencePenaltyFor, woundPenaltyFor, loseCoherence, regainCoherence, displace, + panicBand, panicPenaltyFor, panicBreaksAt, gainPanic, easePanic, breakPanic, isAcross, setAcross, useTransposed, debrief, activeCaseFile, returnCrossing, caseMembers, earnedDiscoveries, deviceFunctionStatus, earnDiscovery, useDeviceFunction, pickContactActor, resolveContact, newEncounter, claimRewards, activeRewardEffects, diff --git a/rules.mjs b/rules.mjs index c581eb4..d4a97d4 100644 --- a/rules.mjs +++ b/rules.mjs @@ -263,6 +263,108 @@ export const COHERENCE_COSTS = { export const COHERENCE_RECOVERY = { debrief: 1, downtimeWeek: 1, anchorObject: 1 }; +/* ============================================================ + PANIC — the short track + ============================================================ + Coherence is the long one: 0-10, campaign-scale, what it costs to keep looking once + you know what you are looking at, recovered at a debrief. Panic is the other half and + they must not be confused at the table: + + Coherence what knowing has cost you weeks 0-10, down + Panic what is happening to you NOW minutes 0-5, up + + Panic goes UP. A trigger gives a point with no roll — asking for a die to find out + whether you are frightened is a die that slows the scene down and tells the player + how to feel. The roll is to get RID of it, which is the interesting direction and the + only one worth a check. + + One free point on purpose: at Panic 1 you are rattled and roll at no penalty. It costs + the table nothing and it means the first shock of a scene is colour rather than + arithmetic. + ============================================================ */ + +export const PANIC_MAX = 5; + +export const PANIC_BANDS = [ + { min: 5, id: "broken", label: "RINGBRP.Panic.Broken", all: -20, breaks: true, + note: "RINGBRP.Panic.BrokenNote" }, + { min: 4, id: "overwhelmed", label: "RINGBRP.Panic.Overwhelmed", all: -20, breaks: false, + note: "RINGBRP.Panic.OverwhelmedNote" }, + { min: 3, id: "losing", label: "RINGBRP.Panic.Losing", all: -10, breaks: false, + note: "RINGBRP.Panic.LosingNote" }, + { min: 2, id: "shaken", label: "RINGBRP.Panic.Shaken", all: -5, breaks: false, + note: "RINGBRP.Panic.ShakenNote" }, + { min: 1, id: "rattled", label: "RINGBRP.Panic.Rattled", all: 0, breaks: false, + note: "RINGBRP.Panic.RattledNote" }, + { min: 0, id: "steady", label: "RINGBRP.Panic.Steady", all: 0, breaks: false, + note: "RINGBRP.Panic.SteadyNote" } +]; + +/** What a point costs. One each, so a GM never has to weigh anything mid-scene. */ +export const PANIC_TRIGGERS = { + wrongThing: { cost: 1, label: "RINGBRP.Panic.Trig.WrongThing" }, + touchedByIt: { cost: 1, label: "RINGBRP.Panic.Trig.Touched" }, + aBody: { cost: 1, label: "RINGBRP.Panic.Trig.Body" }, + someoneYouKnew:{ cost: 2, label: "RINGBRP.Panic.Trig.Known" }, + theLightsWent: { cost: 1, label: "RINGBRP.Panic.Trig.Dark" }, + majorWound: { cost: 1, label: "RINGBRP.Panic.Trig.Wounded" }, + itSaidYourName:{ cost: 2, label: "RINGBRP.Panic.Trig.Named" } +}; + +/** + * What takes a point off, and what it is rolled against. + * + * `self` steadies you; `other` steadies somebody else, which is the whole reason + * Psychology is worth carrying. `heard` reaches everyone who can hear it: naming a thing + * correctly is the department's actual work, and it is worth a mechanical reward. + * A special takes two. + */ +export const PANIC_RELIEF = { + steadyYourself: { skill: null, reach: "self", once: "scene", + label: "RINGBRP.Panic.Relief.Steady" }, + religion: { skill: "religion", reach: "self", once: "scene", + label: "RINGBRP.Panic.Relief.Religion" }, + psychology: { skill: "psychology", reach: "other", once: "scene", + label: "RINGBRP.Panic.Relief.Psychology" }, + occult: { skill: "occult", reach: "heard", once: "encounter", + label: "RINGBRP.Panic.Relief.Occult" }, + anchorObject: { skill: null, reach: "self", once: "case", + label: "RINGBRP.Panic.Relief.Anchor" } +}; + +/** Breaking is a choice, not a table: three bad options, taken fast. */ +export const PANIC_BREAK = ["freeze", "flee", "fixate"]; + +/** Breaking spends the panic. You come back at 3, not at 0 — it is not a rest. */ +export const PANIC_AFTER_BREAK = 3; + +export function panicBandFrom(value) { + const v = Math.max(0, Math.min(PANIC_MAX, Number(value) || 0)); + return PANIC_BANDS.find(b => v >= b.min) ?? PANIC_BANDS.at(-1); +} + +/** The roll penalty a level of panic carries. Never positive. */ +export function panicPenaltyFor(value) { + return Math.min(0, panicBandFrom(value).all); +} + +/** Does this level break the character this round? */ +export function panicBreaksAt(value) { + return panicBandFrom(value).breaks === true; +} + +/** Clamped, so nothing can push the track past its own ends. */ +export function panicAfter(value, delta) { + return Math.max(0, Math.min(PANIC_MAX, (Number(value) || 0) + (Number(delta) || 0))); +} + +/** A success takes one point, a special or better takes two. */ +export function panicReliefFor(level) { + if (level === "critical" || level === "special") return 2; + if (level === "success") return 1; + return 0; +} + export function coherenceBandFrom(value) { const v = Math.max(0, Number(value) || 0); return COHERENCE_BANDS.find(b => v >= b.min) ?? COHERENCE_BANDS.at(-1); diff --git a/styles/ringbrp.css b/styles/ringbrp.css index 2b1e9a4..6e816da 100644 --- a/styles/ringbrp.css +++ b/styles/ringbrp.css @@ -1788,3 +1788,46 @@ #pause.paused::after { animation: none; } #pause.paused img { transform: rotate(-32.7deg); } } + +/* ============================================================ + PANIC — the short track + ============================================================ + Deliberately NOT the same colour as Coherence. The two tracks run in opposite + directions on different timescales, and a table that reads one as the other will make + the wrong decision at the worst moment: Coherence is cool and yellow like the rest of + the department, Panic warms as it climbs and ends red. + ============================================================ */ + +.rb-vital-panic .rb-big { color: var(--rb-white); } +.rb-panic-steady .rb-big, .rb-panic-steady .rb-cohband { color: var(--rb-muted); } +.rb-panic-rattled .rb-big, .rb-panic-rattled .rb-cohband { color: var(--rb-white); } +.rb-panic-shaken .rb-big, .rb-panic-shaken .rb-cohband { color: var(--rb-yellow); } +.rb-panic-losing .rb-big, .rb-panic-losing .rb-cohband { color: #ffb066; } +.rb-panic-overwhelmed .rb-big, .rb-panic-overwhelmed .rb-cohband { color: var(--rb-danger); } +.rb-panic-broken .rb-big, .rb-panic-broken .rb-cohband { color: var(--rb-danger); } +.rb-vital-panic.rb-panic-broken { border-color: var(--rb-danger); } + +.rb-panic-pips { display: flex; gap: 5px; list-style: none; margin: 8px 0; padding: 0; } +.rb-panic-pip { + width: 20px; height: 8px; + background: transparent; border: 1px solid var(--rb-hairline); +} +.rb-panic-pip.on { background: var(--rb-danger); border-color: var(--rb-danger); } + +.rb-panic-move { display: flex; align-items: baseline; gap: 8px; margin: 6px 0; } +.rb-panic-from, .rb-panic-to { + font-family: var(--rb-display); font-size: 20px; color: var(--rb-white); +} +.rb-panic-arrow { color: var(--rb-muted); } +.rb-panic-band { + font-family: var(--rb-body); font-size: 11px; font-weight: 700; + letter-spacing: .1em; text-transform: uppercase; color: var(--rb-yellow); +} +.rb-panic-reason { font-size: 13px; color: var(--rb-white); margin: 4px 0; } +.rb-panic-note { font-size: 12px; color: var(--rb-muted); margin: 2px 0 6px; } +.rb-panic-pen { font-size: 13px; font-weight: 700; color: var(--rb-danger); margin: 2px 0; } +.rb-panic-break { + font-family: var(--rb-body); font-size: 12px; font-weight: 700; letter-spacing: .08em; + color: var(--rb-void); background: var(--rb-danger); + padding: 5px 8px; margin: 6px 0 0; +} diff --git a/system.json b/system.json index eefd591..69f426c 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": "1.0.7", + "version": "1.1.0", "compatibility": { "minimum": 13, "verified": "14.364" diff --git a/template.json b/template.json index bc9a91f..4a68026 100644 --- a/template.json +++ b/template.json @@ -69,6 +69,11 @@ "value": 10, "max": 10, "drift": [] + }, + "panic": { + "value": 0, + "max": 5, + "log": [] } } }, diff --git a/templates/character-sheet.hbs b/templates/character-sheet.hbs index da31033..58b4ebf 100644 --- a/templates/character-sheet.hbs +++ b/templates/character-sheet.hbs @@ -51,6 +51,16 @@ {{localize coherence.band.label}} +
+ {{localize "RINGBRP.Panic.Label"}} + + − + {{sys.panic.value}} + + + + {{localize panic.band.label}} +
{{localize "RINGBRP.Sheet.Armour"}} diff --git a/templates/chat-panic.hbs b/templates/chat-panic.hbs new file mode 100644 index 0000000..70a21cd --- /dev/null +++ b/templates/chat-panic.hbs @@ -0,0 +1,21 @@ +
+
{{localize "RINGBRP.Panic.Label"}}
+
{{actorName}}
+ +
    + {{#each pips as |on|}}
  1. {{/each}} +
+ +
+ {{before}} + → + {{after}} + {{localize band.label}} +
+ +

{{reason}}{{#if note}} — {{note}}{{/if}}

+

{{localize band.note}}

+ + {{#if penalty}}

{{penalty}}% {{localize "RINGBRP.Chat.ToEverything"}}

{{/if}} + {{#if breaks}}

{{localize "RINGBRP.Panic.Breaks"}}

{{/if}} +
diff --git a/tools/check-behaviour.mjs b/tools/check-behaviour.mjs index fa97fad..2f1346d 100644 --- a/tools/check-behaviour.mjs +++ b/tools/check-behaviour.mjs @@ -448,6 +448,81 @@ test("no stylesheet rule sets display on a bare #pause", async () => { } }); +/* ---------------------------------------------------------------- * + * Panic — the short track, added after the first game asked for it + * ---------------------------------------------------------------- */ + +// The first point is free ON PURPOSE: the opening shock of a scene should be colour, +// not arithmetic. If this ever starts costing, the whole track has been re-tuned. +test("panic 1 costs nothing to roll at", () => + assert.equal(RULES.panicPenaltyFor(1), 0)); +test("panic bites from 2 upward and never rewards", () => { + let prev = 1; + for (const v of [2, 3, 4, 5]) { + const pen = RULES.panicPenaltyFor(v); + assert.ok(pen <= 0, `panic ${v} gives a bonus`); + assert.ok(pen <= prev, `panic ${v} is kinder than ${v - 1}`); + prev = pen; + } +}); +test("only 5 breaks you", () => { + for (const v of [0, 1, 2, 3, 4]) assert.equal(RULES.panicBreaksAt(v), false, `${v} breaks`); + assert.equal(RULES.panicBreaksAt(5), true); +}); +// A track that can be pushed past its own ends is a track that will be, by a GM +// clicking + on a bad night. +test("panic cannot leave 0..5", () => { + assert.equal(RULES.panicAfter(5, 3), RULES.PANIC_MAX); + assert.equal(RULES.panicAfter(0, -3), 0); + assert.equal(RULES.panicAfter(2, 2), 4); +}); +// Breaking spends it. Coming back at 0 would make breaking the cheapest way to clear +// the track, which is exactly backwards. +test("breaking does not reset you to calm", () => { + assert.ok(RULES.PANIC_AFTER_BREAK > 0, "breaking wipes the track clean"); + assert.ok(RULES.PANIC_AFTER_BREAK < RULES.PANIC_MAX, "breaking leaves you still broken"); +}); +test("a special takes two points, a failure takes none", () => { + assert.equal(RULES.panicReliefFor("special"), 2); + assert.equal(RULES.panicReliefFor("critical"), 2); + assert.equal(RULES.panicReliefFor("success"), 1); + assert.equal(RULES.panicReliefFor("failure"), 0); + assert.equal(RULES.panicReliefFor("fumble"), 0); +}); +// Every band and every trigger and relief must be sayable, or the chat card prints a key. +test("every panic band, trigger and relief has a label", async () => { + const { readFile } = await import("node:fs/promises"); + const lang = JSON.parse(await readFile(new URL("../lang/en.json", import.meta.url), "utf8")); + for (const b of RULES.PANIC_BANDS) { + assert.ok(lang[b.label], `no label for panic band ${b.id}`); + assert.ok(lang[b.note], `no note for panic band ${b.id}`); + } + for (const [k, t] of Object.entries(RULES.PANIC_TRIGGERS)) + assert.ok(lang[t.label], `no label for panic trigger ${k}`); + for (const [k, r] of Object.entries(RULES.PANIC_RELIEF)) + assert.ok(lang[r.label], `no label for panic relief ${k}`); +}); +// The reliefs name skills; a skill that is not in the catalogue is a relief nobody can +// roll. This is the same defect as a kit key with no item behind it. +test("every panic relief names a skill the catalogue has", async () => { + const { SKILL_CATALOGUE } = await import("./content.mjs"); + const known = new Set(SKILL_CATALOGUE.map(r => r[0])); + for (const [k, r] of Object.entries(RULES.PANIC_RELIEF)) { + if (!r.skill) continue; + assert.ok(known.has(r.skill), `panic relief "${k}" rolls ${r.skill}, which is not a skill`); + } +}); +// The three the first game asked for. +test("occult, religion and psychology exist and can be learned", async () => { + const { SKILL_CATALOGUE } = await import("./content.mjs"); + for (const fam of ["occult", "religion", "psychology"]) { + const row = SKILL_CATALOGUE.find(r => r[0] === fam); + assert.ok(row, `${fam} is not in the catalogue`); + assert.ok(row[3] > 0, `${fam} has no printed base, so nobody can roll it untrained`); + assert.notEqual(row[5]?.canBeTaught, false, `${fam} cannot be taught`); + } +}); + /* ---------------------------------------------------------------- */ await runAll(); diff --git a/tools/check-rules.mjs b/tools/check-rules.mjs index cb4cccc..9a1e0b0 100644 --- a/tools/check-rules.mjs +++ b/tools/check-rules.mjs @@ -179,6 +179,23 @@ const checks = [ ["coherenceBandFrom(4).id", RULES.coherenceBandFrom(4).id, "adrift"], ["coherenceBandFrom(1).id", RULES.coherenceBandFrom(1).id, "unmoored"], ["coherenceBandFrom(0).id", RULES.coherenceBandFrom(0).id, "displaced"], + + // Panic. The free first point is deliberate, so 1 must cost nothing. + ["panicBandFrom(0).id", RULES.panicBandFrom(0).id, "steady"], + ["panicBandFrom(1).id", RULES.panicBandFrom(1).id, "rattled"], + ["panicBandFrom(3).id", RULES.panicBandFrom(3).id, "losing"], + ["panicBandFrom(5).id", RULES.panicBandFrom(5).id, "broken"], + ["panicPenaltyFor(1)", RULES.panicPenaltyFor(1), 0], + ["panicPenaltyFor(2)", RULES.panicPenaltyFor(2), -5], + ["panicPenaltyFor(4)", RULES.panicPenaltyFor(4), -20], + ["panicBreaksAt(4)", RULES.panicBreaksAt(4), false], + ["panicBreaksAt(5)", RULES.panicBreaksAt(5), true], + ["panicAfter(4,1)", RULES.panicAfter(4, 1), 5], + ["panicAfter(5,1)", RULES.panicAfter(5, 1), 5], + ["panicAfter(0,-1)", RULES.panicAfter(0, -1), 0], + ["panicReliefFor('success')", RULES.panicReliefFor("success"), 1], + ["panicReliefFor('special')", RULES.panicReliefFor("special"), 2], + ["panicReliefFor('failure')", RULES.panicReliefFor("failure"), 0], // a lost location must cost something, for every kind the species tables use ["locationEffectsFor([]) is inert", RULES.locationEffectsFor([]).all, 0], ["leg disabled → physical", RULES.locationEffectsFor([{kind:"leg",disabled:true}]).physical, -30], diff --git a/tools/content.mjs b/tools/content.mjs index dbe4b12..ab21a31 100644 --- a/tools/content.mjs +++ b/tools/content.mjs @@ -75,6 +75,13 @@ export const SKILL_CATALOGUE = [ ["navigate", "", "mental", 10, "Navigate"], ["research", "", "mental", 1, "Research"], ["anomaly_lore", "", "mental", 0, "Anomaly Lore", { canBeTaught: false }], + // Added after the first game, which wanted all three and had none of them. Occult and + // Religion are the two halves of what people believed BEFORE the department existed — + // one is what the books say, the other is what the congregation does — and Psychology + // is the only skill that steadies somebody who is not you. + ["occult", "", "mental", 5, "Occult"], + ["religion", "", "mental", 5, "Religion"], + ["psychology", "", "mental", 5, "Psychology"], // perception ["listen", "", "percep", 25, "Listen"], ["spot", "", "percep", 25, "Spot"], diff --git a/tools/rules-text.mjs b/tools/rules-text.mjs index 097f3fc..f756f02 100644 --- a/tools/rules-text.mjs +++ b/tools/rules-text.mjs @@ -28,7 +28,8 @@ import { powerStartingRating, brawlFallbackFor, bleedStoppedBy, crossingArtPoolFor, crossingGrantsArt, crossingCostFor, REACTION, BLEED, CIRCUMSTANCE, RANGE_LADDER, rangeBandFrom, - MAJOR_WOUND_TABLE, COHERENCE_BANDS, COHERENCE_COSTS, COHERENCE_RECOVERY + MAJOR_WOUND_TABLE, COHERENCE_BANDS, COHERENCE_COSTS, COHERENCE_RECOVERY, + PANIC_BANDS, PANIC_TRIGGERS, PANIC_RELIEF, PANIC_MAX, PANIC_AFTER_BREAK, PANIC_BREAK } from "../rules.mjs"; // Postings, trades and induction are plain data and are imported as such. import { ROLES, TRADES, INDUCTION, TIERS, TRADE_BANDS } from "../postings.mjs"; @@ -75,6 +76,13 @@ const woundRows = MAJOR_WOUND_TABLE.map(r => { return [`${r.min}`, L(`RINGBRP.Wound.${r.key}.title`), bits.join(" · ") || "—"]; }); +const panicRows = [...PANIC_BANDS].reverse().map(b => + [String(b.min), L(b.label), b.all ? `${pc(b.all)} everything` : "none", L(b.note)]); +const panicTrigRows = Object.values(PANIC_TRIGGERS) + .map(t => [L(t.label), `+${t.cost}`]); +const panicReliefRows = Object.values(PANIC_RELIEF) + .map(r => [L(r.label), r.reach, r.once]); + const cohRows = COHERENCE_BANDS.map((b, i) => { const hi = i === 0 ? 10 : COHERENCE_BANDS[i - 1].min - 1; const rng = b.min === hi ? `${b.min}` : `${hi}–${b.min}`; @@ -518,6 +526,43 @@ return does not strand you either — you arrive near your target rather than at There is no state in which an agent cannot get home. There are only states in which getting home costs more than the case was worth.

` }, +{ name: "4b · Panic", content: ` +

Coherence is what knowing has cost you, and it is measured in weeks. Panic is +what is happening to you now, and it is measured in minutes. Do not confuse them at +the table: one goes down over a career, the other goes up over a corridor.

+ +

Panic runs from 0 up to ${PANIC_MAX}. You take a point when +something happens to you — no roll. Asking for a die to find out +whether you are frightened slows the scene down and tells you how to feel. The die is +for getting rid of it.

+ +${table(["Panic", "Band", "Penalty", "What it looks like"], panicRows)} + +

The first point is free. At Panic 1 you are rattled and you roll at no penalty, +because the opening shock of a scene should be colour rather than arithmetic.

+ +

Taking a point

+${table(["This happens", "Panic"], panicTrigRows)} + +

Getting rid of one

+

Roll it. A success takes one point off; a special or better takes two. +A failure takes nothing and costs nothing but the moment you spent on it.

+${table(["How", "Reaches", "How often"], panicReliefRows)} + +

Occult is the odd one. Naming a thing correctly, out loud, helps +everybody who can hear you — which is the department's actual work, and the only relief +that reaches a whole room. Psychology is the only one that steadies +somebody who is not you. Religion steadies you and nobody else, and it +does not care whether you are right.

+ +

Breaking

+

At ${PANIC_MAX} you break. Choose one — ${PANIC_BREAK.join(", ")} — +take it, and come back at ${PANIC_AFTER_BREAK}. You do not come back at +zero. Breaking is not a rest.

+ +

Panic clears when the scene does. When you are somewhere safe with the door shut and +somebody has made tea, you are at nought again.

` }, + { name: "5 · The Quiet Arts", content: `

Some agents come back with something. It is not a gift and it is not training — it is the crossing, left in a person.