From 24b1aaa6826f41a6be2507f26af5c941d220ffd9 Mon Sep 17 00:00:00 2001 From: slaguru666 <111923774+slaguru666@users.noreply.github.com> Date: Tue, 18 Aug 2026 23:23:03 +0100 Subject: [PATCH] Development: skills improve from what beat you, not what worked Reversed experience. A skill is marked when it FAILS - failure or fumble - and never when it works, and the improvement roll is the same reversal: roll OVER your rating and the skill goes up 1d6. Nobody learns anything from the lock that opened first time. The two halves pull against each other on purpose, so ratings converge instead of running away. Simulated over 2000 campaigns: a 10% and an 85% skill start 75 points apart and are 11 apart after 60 cases. - rules.mjs owns it: DEVELOPMENT, developmentMarksOn, developmentImproves, developmentCeiling, developmentGainFor - trainingCap has been on the skill schema and the item sheet since it was written and nothing has ever read it. Development reads it now: a skill stops at its own cap, and everything else stops at DEVELOPMENT.ceiling, because a d6 gain could otherwise push a 98% skill past 100 - GM opens the phase from a GM-only toggle in the token controls; it posts a card naming every agent and their marked count, and puts a Development button on each sheet. Players press their own - the toggle re-reads the setting on change, since getSceneControlButtons only fires at load and the button would otherwise lie if the phase were opened by the API or a second GM - rules journal and player primer rewritten from the tables; the old text described the pre-reversal rule on both halves - 7 spot-checks, 4 behaviour tests; verified end to end in a running Foundry Co-Authored-By: Claude Opus 5 --- README.md | 6 +- lang/en.json | 16 ++++- packs/armour/LOG | 10 ++-- packs/armour/LOG.old | 2 +- packs/expedition/LOG | 10 ++-- packs/expedition/LOG.old | 2 +- packs/gear/LOG | 10 ++-- packs/gear/LOG.old | 2 +- packs/lastadmission/LOG | 10 ++-- packs/lastadmission/LOG.old | 2 +- packs/npcs/LOG | 10 ++-- packs/npcs/LOG.old | 2 +- packs/powers/LOG | 10 ++-- packs/powers/LOG.old | 2 +- packs/pregens/LOG | 10 ++-- packs/pregens/LOG.old | 2 +- packs/primer/000005.ldb | Bin 23087 -> 24730 bytes packs/primer/000006.log | Bin 37635 -> 39576 bytes packs/primer/LOG | 10 ++-- packs/primer/LOG.old | 2 +- packs/primer/MANIFEST-000004 | Bin 147 -> 147 bytes packs/prologue/LOG | 10 ++-- packs/prologue/LOG.old | 2 +- packs/rules/000005.ldb | Bin 36532 -> 37376 bytes packs/rules/000006.log | Bin 73480 -> 74867 bytes packs/rules/LOG | 10 ++-- packs/rules/LOG.old | 2 +- packs/rules/MANIFEST-000004 | Bin 147 -> 147 bytes packs/skills/LOG | 10 ++-- packs/skills/LOG.old | 2 +- packs/slice/LOG | 10 ++-- packs/slice/LOG.old | 2 +- packs/starter/LOG | 10 ++-- packs/starter/LOG.old | 2 +- packs/tables/LOG | 10 ++-- packs/tables/LOG.old | 2 +- packs/talents/LOG | 10 ++-- packs/talents/LOG.old | 2 +- packs/throughtrain/LOG | 10 ++-- packs/throughtrain/LOG.old | 2 +- packs/vehicles/LOG | 10 ++-- packs/vehicles/LOG.old | 2 +- packs/weapons/LOG | 10 ++-- packs/weapons/LOG.old | 2 +- ringbrp.mjs | 103 +++++++++++++++++++++++++++++---- rules.mjs | 59 +++++++++++++++++++ styles/ringbrp.css | 11 ++++ system.json | 2 +- templates/character-sheet.hbs | 7 +++ templates/chat-development.hbs | 19 ++++++ tools/check-behaviour.mjs | 53 +++++++++++++++++ tools/check-rules.mjs | 17 ++++++ tools/primer-text.mjs | 27 ++++++++- tools/rules-text.mjs | 29 ++++++++-- 54 files changed, 435 insertions(+), 130 deletions(-) create mode 100644 templates/chat-development.hbs diff --git a/README.md b/README.md index 472ce6d..956f839 100644 --- a/README.md +++ b/README.md @@ -180,9 +180,9 @@ The build runs all five guards before it writes anything: ``` -check-rules: OK — 92 rules, 21 files scanned, 3 aliases + 45 constant sets checked, 291 formulas verified +check-rules: OK — 96 rules, 21 files scanned, 3 aliases + 46 constant sets checked, 306 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, 981 keys, no leaf/branch collisions -check-behaviour: OK — 59 behavioural tests +check-lang: OK — en.json, 995 keys, no leaf/branch collisions +check-behaviour: OK — 67 behavioural tests ``` diff --git a/lang/en.json b/lang/en.json index c289746..8907e2a 100644 --- a/lang/en.json +++ b/lang/en.json @@ -581,6 +581,8 @@ "RINGBRP.Heal.Fumbled": "First Aid fumbled — the patient loses a further hit point.", "RINGBRP.Exp.Title": "End of case — improvement", "RINGBRP.Exp.NoGain": "no gain", + "RINGBRP.Exp.Capped": "held at its ceiling", + "RINGBRP.Exp.AtCeiling": "already as far as training takes it", "RINGBRP.Exp.NoTicks": "No skill earned a tick this case.", "RINGBRP.Flavour.Gear": "{item}: +{mod}%", "RINGBRP.Target.Title": "Target", @@ -979,5 +981,17 @@ "RINGBRP.Panic.SceneEnd": "Scene over", "RINGBRP.Panic.SceneEndNote": "Panic clears when the scene does. Coherence does not.", "RINGBRP.Party.Title": "The team, right now", - "RINGBRP.Party.Who": "Agent" + "RINGBRP.Party.Who": "Agent", + "RINGBRP.Dev.Title": "Development", + "RINGBRP.Dev.SettingName": "Development phase open", + "RINGBRP.Dev.SettingHint": "While open, every agent's sheet offers a Development roll for the skills that failed them this case.", + "RINGBRP.Dev.Opened": "The case is closed. Roll for the skills that beat you.", + "RINGBRP.Dev.Closed": "The development phase is closed.", + "RINGBRP.Dev.Marked": "skills marked", + "RINGBRP.Dev.Toggle": "Development phase", + "RINGBRP.Dev.PressIt": "Each agent presses Development on their own sheet.", + "RINGBRP.Dev.GMOnly": "Only the GM can open the development phase.", + "RINGBRP.Dev.Button": "Development", + "RINGBRP.Dev.NotOpen": "The GM has not opened the development phase.", + "RINGBRP.Dev.Tooltip": "Roll to improve the skills that failed you this case. A skill improves if the roll is ANOTHER failure." } diff --git a/packs/armour/LOG b/packs/armour/LOG index c67ae16..3669d22 100644 --- a/packs/armour/LOG +++ b/packs/armour/LOG @@ -1,5 +1,5 @@ -2026/08/18-22:37:32.799186 176b97000 Recovering log #3 -2026/08/18-22:37:32.799323 176b97000 Level-0 table #5: started -2026/08/18-22:37:32.799504 176b97000 Level-0 table #5: 4591 bytes OK -2026/08/18-22:37:32.799868 176b97000 Delete type=0 #3 -2026/08/18-22:37:32.799916 176b97000 Delete type=3 #2 +2026/08/18-23:21:42.522827 16edc3000 Recovering log #3 +2026/08/18-23:21:42.522857 16edc3000 Level-0 table #5: started +2026/08/18-23:21:42.523016 16edc3000 Level-0 table #5: 4591 bytes OK +2026/08/18-23:21:42.523288 16edc3000 Delete type=0 #3 +2026/08/18-23:21:42.523332 16edc3000 Delete type=3 #2 diff --git a/packs/armour/LOG.old b/packs/armour/LOG.old index 6aab54a..f1db83c 100644 --- a/packs/armour/LOG.old +++ b/packs/armour/LOG.old @@ -1 +1 @@ -2026/08/18-22:37:15.356757 172877000 Delete type=3 #1 +2026/08/18-23:21:17.000759 16ec7f000 Delete type=3 #1 diff --git a/packs/expedition/LOG b/packs/expedition/LOG index 6f43f8f..331cc1f 100644 --- a/packs/expedition/LOG +++ b/packs/expedition/LOG @@ -1,5 +1,5 @@ -2026/08/18-22:37:32.827220 176b97000 Recovering log #3 -2026/08/18-22:37:32.827303 176b97000 Level-0 table #5: started -2026/08/18-22:37:32.827422 176b97000 Level-0 table #5: 4129 bytes OK -2026/08/18-22:37:32.827654 176b97000 Delete type=0 #3 -2026/08/18-22:37:32.827715 176b97000 Delete type=3 #2 +2026/08/18-23:21:42.542861 1705e7000 Recovering log #3 +2026/08/18-23:21:42.542881 1705e7000 Level-0 table #5: started +2026/08/18-23:21:42.543000 1705e7000 Level-0 table #5: 4129 bytes OK +2026/08/18-23:21:42.543233 1705e7000 Delete type=0 #3 +2026/08/18-23:21:42.543262 1705e7000 Delete type=3 #2 diff --git a/packs/expedition/LOG.old b/packs/expedition/LOG.old index e962a5f..c6c19fb 100644 --- a/packs/expedition/LOG.old +++ b/packs/expedition/LOG.old @@ -1 +1 @@ -2026/08/18-22:37:15.433657 17388f000 Delete type=3 #1 +2026/08/18-23:21:17.085840 1704a3000 Delete type=3 #1 diff --git a/packs/gear/LOG b/packs/gear/LOG index 52c8408..3289fc0 100644 --- a/packs/gear/LOG +++ b/packs/gear/LOG @@ -1,5 +1,5 @@ -2026/08/18-22:37:32.801021 17638b000 Recovering log #3 -2026/08/18-22:37:32.801454 17638b000 Level-0 table #5: started -2026/08/18-22:37:32.801772 17638b000 Level-0 table #5: 31614 bytes OK -2026/08/18-22:37:32.802069 17638b000 Delete type=0 #3 -2026/08/18-22:37:32.802111 17638b000 Delete type=3 #2 +2026/08/18-23:21:42.523809 16edc3000 Recovering log #3 +2026/08/18-23:21:42.523911 16edc3000 Level-0 table #5: started +2026/08/18-23:21:42.524193 16edc3000 Level-0 table #5: 31614 bytes OK +2026/08/18-23:21:42.524392 16edc3000 Delete type=0 #3 +2026/08/18-23:21:42.524442 16edc3000 Delete type=3 #2 diff --git a/packs/gear/LOG.old b/packs/gear/LOG.old index 7584902..4e62250 100644 --- a/packs/gear/LOG.old +++ b/packs/gear/LOG.old @@ -1 +1 @@ -2026/08/18-22:37:15.358422 17409b000 Delete type=3 #1 +2026/08/18-23:21:17.002659 1704a3000 Delete type=3 #1 diff --git a/packs/lastadmission/LOG b/packs/lastadmission/LOG index 7bdf5d6..92ad3e4 100644 --- a/packs/lastadmission/LOG +++ b/packs/lastadmission/LOG @@ -1,5 +1,5 @@ -2026/08/18-22:37:32.818774 176b97000 Recovering log #3 -2026/08/18-22:37:32.819665 176b97000 Level-0 table #5: started -2026/08/18-22:37:32.820075 176b97000 Level-0 table #5: 46857 bytes OK -2026/08/18-22:37:32.820335 176b97000 Delete type=0 #3 -2026/08/18-22:37:32.820381 176b97000 Delete type=3 #2 +2026/08/18-23:21:42.537220 1705e7000 Recovering log #3 +2026/08/18-23:21:42.537462 1705e7000 Level-0 table #5: started +2026/08/18-23:21:42.537820 1705e7000 Level-0 table #5: 46857 bytes OK +2026/08/18-23:21:42.538102 1705e7000 Delete type=0 #3 +2026/08/18-23:21:42.538144 1705e7000 Delete type=3 #2 diff --git a/packs/lastadmission/LOG.old b/packs/lastadmission/LOG.old index c5694a1..5549886 100644 --- a/packs/lastadmission/LOG.old +++ b/packs/lastadmission/LOG.old @@ -1 +1 @@ -2026/08/18-22:37:15.417310 172877000 Delete type=3 #1 +2026/08/18-23:21:17.066464 16ec7f000 Delete type=3 #1 diff --git a/packs/npcs/LOG b/packs/npcs/LOG index e85dc95..f37bb6d 100644 --- a/packs/npcs/LOG +++ b/packs/npcs/LOG @@ -1,5 +1,5 @@ -2026/08/18-22:37:32.784037 17638b000 Recovering log #3 -2026/08/18-22:37:32.786085 17638b000 Level-0 table #5: started -2026/08/18-22:37:32.787900 17638b000 Level-0 table #5: 234672 bytes OK -2026/08/18-22:37:32.788307 17638b000 Delete type=0 #3 -2026/08/18-22:37:32.788379 17638b000 Delete type=3 #2 +2026/08/18-23:21:42.511991 16edc3000 Recovering log #3 +2026/08/18-23:21:42.512828 16edc3000 Level-0 table #5: started +2026/08/18-23:21:42.514313 16edc3000 Level-0 table #5: 234672 bytes OK +2026/08/18-23:21:42.514681 16edc3000 Delete type=0 #3 +2026/08/18-23:21:42.514785 16edc3000 Delete type=3 #2 diff --git a/packs/npcs/LOG.old b/packs/npcs/LOG.old index e9a811c..65c1374 100644 --- a/packs/npcs/LOG.old +++ b/packs/npcs/LOG.old @@ -1 +1 @@ -2026/08/18-22:37:15.394128 172877000 Delete type=3 #1 +2026/08/18-23:21:17.040817 1704a3000 Delete type=3 #1 diff --git a/packs/powers/LOG b/packs/powers/LOG index 601fd23..3a1ae77 100644 --- a/packs/powers/LOG +++ b/packs/powers/LOG @@ -1,5 +1,5 @@ -2026/08/18-22:37:32.795533 176b97000 Recovering log #3 -2026/08/18-22:37:32.795934 176b97000 Level-0 table #5: started -2026/08/18-22:37:32.796065 176b97000 Level-0 table #5: 3295 bytes OK -2026/08/18-22:37:32.796355 176b97000 Delete type=0 #3 -2026/08/18-22:37:32.796392 176b97000 Delete type=3 #2 +2026/08/18-23:21:42.520180 16edc3000 Recovering log #3 +2026/08/18-23:21:42.520206 16edc3000 Level-0 table #5: started +2026/08/18-23:21:42.520334 16edc3000 Level-0 table #5: 3295 bytes OK +2026/08/18-23:21:42.520647 16edc3000 Delete type=0 #3 +2026/08/18-23:21:42.520681 16edc3000 Delete type=3 #2 diff --git a/packs/powers/LOG.old b/packs/powers/LOG.old index a2f69d8..23535aa 100644 --- a/packs/powers/LOG.old +++ b/packs/powers/LOG.old @@ -1 +1 @@ -2026/08/18-22:37:15.428065 173083000 Delete type=3 #1 +2026/08/18-23:21:17.079222 16f48b000 Delete type=3 #1 diff --git a/packs/pregens/LOG b/packs/pregens/LOG index 6d74ddc..e268a49 100644 --- a/packs/pregens/LOG +++ b/packs/pregens/LOG @@ -1,5 +1,5 @@ -2026/08/18-22:37:32.768723 176b97000 Recovering log #3 -2026/08/18-22:37:32.772081 176b97000 Level-0 table #5: started -2026/08/18-22:37:32.777725 176b97000 Level-0 table #5: 560581 bytes OK -2026/08/18-22:37:32.778401 176b97000 Delete type=0 #3 -2026/08/18-22:37:32.778494 176b97000 Delete type=3 #2 +2026/08/18-23:21:42.501500 16edc3000 Recovering log #3 +2026/08/18-23:21:42.503444 16edc3000 Level-0 table #5: started +2026/08/18-23:21:42.506554 16edc3000 Level-0 table #5: 560581 bytes OK +2026/08/18-23:21:42.506965 16edc3000 Delete type=0 #3 +2026/08/18-23:21:42.507130 16edc3000 Delete type=3 #2 diff --git a/packs/pregens/LOG.old b/packs/pregens/LOG.old index 5de7279..8b53a27 100644 --- a/packs/pregens/LOG.old +++ b/packs/pregens/LOG.old @@ -1 +1 @@ -2026/08/18-22:37:15.379031 173083000 Delete type=3 #1 +2026/08/18-23:21:17.025927 16fc97000 Delete type=3 #1 diff --git a/packs/primer/000005.ldb b/packs/primer/000005.ldb index 6abe3a0e88d83d24661019b35ce935c01ce6000c..75e5695c6d8102897ba2cc3d2cf12b6ef1934124 100644 GIT binary patch delta 9922 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 delta 8415 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 Wf3(>6ee&HSXuQyVGWC~(pz$v%%?Min delta 95 zcmbQSm8p3dlfcOUSHGY2OuP&bz_U@{bUxE}uE}@vl{Pn)hq7$eE;C|e>NcAkU#_%S xr}hfdX7+|AcJ_qoKL`H^v25;apUl7cS~Vl%WZ4-~jAu4m%urz5oIO*Q5dblSB5MEu diff --git a/packs/primer/LOG b/packs/primer/LOG index b5f686c..f6a0730 100644 --- a/packs/primer/LOG +++ b/packs/primer/LOG @@ -1,5 +1,5 @@ -2026/08/18-22:37:32.804709 175b7f000 Recovering log #3 -2026/08/18-22:37:32.804956 175b7f000 Level-0 table #5: started -2026/08/18-22:37:32.805208 175b7f000 Level-0 table #5: 23087 bytes OK -2026/08/18-22:37:32.805486 175b7f000 Delete type=0 #3 -2026/08/18-22:37:32.805525 175b7f000 Delete type=3 #2 +2026/08/18-23:21:42.526413 16edc3000 Recovering log #3 +2026/08/18-23:21:42.526467 16edc3000 Level-0 table #5: started +2026/08/18-23:21:42.526727 16edc3000 Level-0 table #5: 24730 bytes OK +2026/08/18-23:21:42.526971 16edc3000 Delete type=0 #3 +2026/08/18-23:21:42.526999 16edc3000 Delete type=3 #2 diff --git a/packs/primer/LOG.old b/packs/primer/LOG.old index eed8a7c..5835654 100644 --- a/packs/primer/LOG.old +++ b/packs/primer/LOG.old @@ -1 +1 @@ -2026/08/18-22:37:15.424767 17409b000 Delete type=3 #1 +2026/08/18-23:21:17.075510 16fc97000 Delete type=3 #1 diff --git a/packs/primer/MANIFEST-000004 b/packs/primer/MANIFEST-000004 index 757bfe944e7f3856db1bad7b92701ef7463f15c8..7f1100dd9bf48abe776e1bea149ebde5cbd799ae 100644 GIT binary patch delta 30 mcmbQtIGJ&RvWVta0h>$)MkY2+24;2^UUml7SqCSY#{d9l2L^ip delta 30 mcmbQtIGJ&RvWU%#!b6!1j7)5t49x5-JnRgt>$glaj{yLBHwSM3 diff --git a/packs/prologue/LOG b/packs/prologue/LOG index 741965e..412ad02 100644 --- a/packs/prologue/LOG +++ b/packs/prologue/LOG @@ -1,5 +1,5 @@ -2026/08/18-22:37:32.821353 176b97000 Recovering log #3 -2026/08/18-22:37:32.821969 176b97000 Level-0 table #5: started -2026/08/18-22:37:32.822328 176b97000 Level-0 table #5: 44887 bytes OK -2026/08/18-22:37:32.822647 176b97000 Delete type=0 #3 -2026/08/18-22:37:32.822686 176b97000 Delete type=3 #2 +2026/08/18-23:21:42.538839 16f5cf000 Recovering log #3 +2026/08/18-23:21:42.539012 16f5cf000 Level-0 table #5: started +2026/08/18-23:21:42.539381 16f5cf000 Level-0 table #5: 44887 bytes OK +2026/08/18-23:21:42.539604 16f5cf000 Delete type=0 #3 +2026/08/18-23:21:42.539638 16f5cf000 Delete type=3 #2 diff --git a/packs/prologue/LOG.old b/packs/prologue/LOG.old index 148d211..36808ba 100644 --- a/packs/prologue/LOG.old +++ b/packs/prologue/LOG.old @@ -1 +1 @@ -2026/08/18-22:37:15.421322 17409b000 Delete type=3 #1 +2026/08/18-23:21:17.071063 16ec7f000 Delete type=3 #1 diff --git a/packs/rules/000005.ldb b/packs/rules/000005.ldb index 1e29a850bfa40ba12796f129007e4ee86fcfdd41..b4e6a9ad4641f554043c109a4f3fc1c066924059 100644 GIT binary patch delta 3431 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 r&s16f?+W^S;@-Op#i$BO; zH#fezn4PKgwdZesxch#U?cVwFvz;q%Tzve?x3htMKKq<9eX5OX8*SrazkGX{*hRZs z=w|t}tODT^OHHnAU8tg_wd%>c&SsQ**HGg^q(U_skrt5%&s9^IevcGls>lTzt!zwe zbhOg*#Xd?e_FMEBxKW7S*A(Yn-@3p_Y{^v)CNUANfZ#0uFpJaZE<40FRj zqJqD%oVP|BJ}vb?Tgw6Dx{9R)P8PZMa;N`eJ&LX-D5w1w*baZIe@d zSEghc`7mH@m~kX@1}F;H+);|VAl9#;CvOK0fQ-0aT+F#T+oRwxe$Y@n8(WgNXtPH1 z#2y)JfwbGGAZ`=*&T1@D5G(6!GR1g^u{9}flKd}6l-vN`d345_Dtn4dJ{$aVQyPI7 zqGc?6<>j>TG4~yAWUfNKH4~gM=D%g{o|9@U;TA< d{Wo)ccJalF`jzv=M{k|~^!M&}M^}Eh_7B8+U6ud< delta 269 zcmXYqu}T9$7=$_5kW$nxg&9!Wn6prj5D?-62$t8Zdw(v=t-Hs+yOFC*sjU{azJR1S z#LmjX!nd%uv9t1&bi*+7ec$im=2bi{#NN;DQ6m;ot=+CS#pj2(9_`N##+e;e 0 ? heal(actor, total, { source: device || "natural" }) : actor.system.hp.value; } +/** Is the development phase open? Safe before init, so the sheet can ask freely. */ +export function developmentOpen() { + try { return game.settings.get("ringbrp", "developmentPhase") === true; } + catch { return false; } +} + +/** + * The GM opens the phase at the end of a case. Everybody's sheet grows a button; each + * player presses their own. Closing it again is the GM saying the case is finished. + */ +export async function setDevelopmentPhase(open = true) { + if (!game.user.isGM) { ui.notifications?.warn(game.i18n.localize("RINGBRP.Dev.GMOnly")); return null; } + await game.settings.set("ringbrp", "developmentPhase", !!open); + const marked = game.actors.filter(a => a.type === "character") + .map(a => ({ name: a.name, + n: a.items.filter(i => ["skill","power"].includes(i.type) && i.system.experienceTicked).length })) + .filter(r => r.n > 0); + await ChatMessage.create({ + speaker: { alias: game.i18n.localize("RINGBRP.Dev.Title") }, + content: await rbRenderTemplate("systems/ringbrp/templates/chat-development.hbs", + { open: !!open, rows: marked }) + }); + return !!open; +} + /** * End of case. Every ticked skill rolls to improve: succeed against d100 > current * rating and it goes up 1D6, and the tick clears either way. This is the step that @@ -3690,18 +3716,23 @@ export async function endOfCase(actor) { const rating = Number(sk.system.value) || 0; const roll = await new Roll("1d100").evaluate(); // Roll OVER the current rating: the better you already are, the harder to gain. - const improved = roll.total > rating; - const gain = improved ? (await new Roll("1d6").evaluate()).total : 0; + const improved = developmentImproves(roll.total, rating); + const rolled = improved ? (await new Roll(DEVELOPMENT.gain).evaluate()).total : 0; + // A d100 gain can overshoot the top, so clip it to the skill's ceiling. + const gain = developmentGainFor(rating, rolled, sk.system.trainingCap); + const capped = improved && gain < rolled; await sk.update({ "system.value": rating + gain, "system.experienceTicked": false }); - results.push({ name: sk.name, rating, roll: roll.total, gain, improved }); + results.push({ name: sk.name, rating, roll: roll.total, gain, improved, capped }); } await ChatMessage.create({ speaker: ChatMessage.getSpeaker({ actor }), content: `

${ game.i18n.localize("RINGBRP.Exp.Title")}

    ${ results.map(r => `
  • ${r.name}: ${r.roll} vs ${r.rating}% — ${ - r.improved ? `+${r.gain} → ${r.rating + r.gain}%` : - game.i18n.localize("RINGBRP.Exp.NoGain")}
  • `).join("")}
` + !r.improved ? game.i18n.localize("RINGBRP.Exp.NoGain") : + r.gain ? `+${r.gain} → ${r.rating + r.gain}%${ + r.capped ? ` (${game.i18n.localize("RINGBRP.Exp.Capped")})` : ""}` : + game.i18n.localize("RINGBRP.Exp.AtCeiling")}`).join("")}` }); return results; } @@ -4889,6 +4920,11 @@ class RingBRPCharacterSheet extends ActorSheet { ctx.reaction = this._reactionRail(); ctx.readiness = this._readiness ?? { rating: 0 }; ctx.coherence = { band: coherenceBand(sys.coherence?.value) }; + ctx.development = { + open: developmentOpen(), + marked: this.actor.items.filter(i => ["skill", "power"].includes(i.type) + && i.system?.experienceTicked).length + }; 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)) }; @@ -4953,6 +4989,13 @@ class RingBRPCharacterSheet extends ActorSheet { el.addEventListener("click", ev => this._onRollAttribute(ev))); root.querySelectorAll("[data-action='roll-characteristic']").forEach(el => el.addEventListener("click", ev => this._onRollCharacteristic(ev))); + root.querySelectorAll("[data-action='development']").forEach(el => + el.addEventListener("click", async ev => { + ev.preventDefault(); ev.stopPropagation(); + if (!developmentOpen()) { ui.notifications?.warn(game.i18n.localize("RINGBRP.Dev.NotOpen")); return; } + await endOfCase(this.actor); + this.render(false); + })); root.querySelectorAll("[data-action='full-skills']").forEach(el => el.addEventListener("click", async () => { const n = await grantFullSkillList(this.actor); @@ -6176,6 +6219,25 @@ async function resolveLegDialog(expedition) { Hooks.once("init", () => { console.log("RingBRP | initialising"); + // The development phase. World-scoped and GM-only, because it is a thing the table + // does together at the end of a case rather than a preference. It is the first game + // setting this system registers, and it earns that: the players' button has to know + // whether the GM has opened the phase, and that answer must survive a reload. + game.settings.register("ringbrp", "developmentPhase", { + name: "RINGBRP.Dev.SettingName", + hint: "RINGBRP.Dev.SettingHint", + scope: "world", config: false, type: Boolean, default: false, + onChange: open => { + for (const a of game.actors) a.sheet?.rendered && a.sheet.render(false); + // getSceneControlButtons fires once at load, so the toggle would keep showing + // the state it had then if the phase were opened by anything else - the API, or + // a second GM. Foundry preserves `active` on toggle tools across a render, so + // setting it and re-rendering is what keeps the button honest. + const tool = ui.controls?.controls?.tokens?.tools?.ringbrpDevelopment; + if (tool) { tool.active = !!open; ui.controls.render(); } + } + }); + // The figure is drawn in three places — combat, anatomy, and the header thumbnail. // One partial, so a location cannot be present in one of them and missing from // another, which is the same rule rules.mjs enforces for the formulas. @@ -6237,6 +6299,7 @@ Hooks.once("init", () => { coherenceBand, coherencePenaltyFor, woundPenaltyFor, loseCoherence, regainCoherence, displace, panicBand, panicPenaltyFor, panicBreaksAt, panicReliefFor, panicAfter, gainPanic, easePanic, breakPanic, clearPanic, partyState, + developmentOpen, setDevelopmentPhase, developmentMarksOn, developmentImproves, developmentGainFor, isAcross, setAcross, useTransposed, debrief, activeCaseFile, returnCrossing, caseMembers, earnedDiscoveries, deviceFunctionStatus, earnDiscovery, useDeviceFunction, pickContactActor, resolveContact, newEncounter, claimRewards, activeRewardEffects, @@ -6434,6 +6497,24 @@ for (const hook of [foundry?.applications?.sidebar?.tabs?.ChatLog }); } +// The GM needs a way to open the development phase that is not the console. It rides +// in the token controls beside the other table-facing tools, GM-only, and reflects the +// world setting so a reload or a second GM screen shows the true state. +Hooks.on("getSceneControlButtons", controls => { + if (!game.user?.isGM) return; + const group = controls?.tokens; + if (!group?.tools) return; + group.tools.ringbrpDevelopment = { + name: "ringbrpDevelopment", + order: Object.keys(group.tools).length + 1, + title: "RINGBRP.Dev.Toggle", + icon: "fa-solid fa-arrow-trend-up", + toggle: true, + active: developmentOpen(), + onChange: (_event, active) => setDevelopmentPhase(active) + }; +}); + Hooks.on("renderCombatTracker", (app, html) => { const root = html instanceof HTMLElement ? html : html?.[0]; if (!root || !game.combat) return; diff --git a/rules.mjs b/rules.mjs index 66c9466..49300da 100644 --- a/rules.mjs +++ b/rules.mjs @@ -283,6 +283,65 @@ export const COHERENCE_RECOVERY = { debrief: 1, downtimeWeek: 1, anchorObject: 1 arithmetic. ============================================================ */ +/* ============================================================ + DEVELOPMENT — you learn from what beat you + ============================================================ + Call of Cthulhu marks a skill when you SUCCEED with it and improves it later. + This department works the other way round: a skill is marked when it FAILS you. + Nobody learns anything from the lock that opened first time. + + The improvement roll itself is the familiar one and is deliberately unchanged: roll + d100 against the current rating and you improve by OVERSHOOTING it. The better you + already are, the less often a case teaches you anything, which is what stops a + long campaign turning everyone into ninety-percenters. + + The two halves therefore pull in opposite directions on purpose. A low skill fails + constantly in play (so it gets marked) and then fails its development roll (so it + climbs fast). A high skill rarely fails at all, and when it does it usually passes + the development roll and gains nothing. Skills converge rather than run away. + + There is still a hard ceiling, because a d100 gain can overshoot the top: a skill is + held at its own trainingCap if it has one, and at DEVELOPMENT.ceiling otherwise. + ============================================================ */ + +/** A skill is marked by the outcomes that went badly. Nothing else marks it. */ +export const DEVELOPMENT = { + marksOn: ["failure", "fumble"], + gain: "1d6", + improveOn: "over", + ceiling: 100 +}; + +/** Did this outcome mark the skill for development? */ +export function developmentMarksOn(level) { + return DEVELOPMENT.marksOn.includes(level); +} + +/** + * Does this development roll improve the skill? Roll OVER the current rating. + * A rating of 0 always improves; a rating of 100 or better never does. + */ +export function developmentImproves(roll, rating) { + return (Number(roll) || 0) > (Number(rating) || 0); +} + +/** + * The highest a skill can be taken to. A skill carrying its own trainingCap is held + * there — that is the only thing trainingCap has ever meant — and everything else + * stops at the point where a d100 can no longer overshoot it. + */ +export function developmentCeiling(trainingCap) { + const cap = Number(trainingCap); + return Number.isFinite(cap) && cap > 0 ? Math.min(cap, DEVELOPMENT.ceiling) : DEVELOPMENT.ceiling; +} + +/** What the skill actually becomes: the gain, clipped to the ceiling. */ +export function developmentGainFor(rating, rolled, trainingCap) { + const from = Number(rating) || 0; + const to = Math.min(from + (Number(rolled) || 0), developmentCeiling(trainingCap)); + return Math.max(0, to - from); +} + export const PANIC_MAX = 5; export const PANIC_BANDS = [ diff --git a/styles/ringbrp.css b/styles/ringbrp.css index 3871b1d..bb43ad6 100644 --- a/styles/ringbrp.css +++ b/styles/ringbrp.css @@ -1858,3 +1858,14 @@ .rb-scene-list li { display: flex; justify-content: space-between; padding: 2px 0; font-size: 13px; } .rb-scene-name { color: var(--rb-white); } .rb-scene-drop { color: var(--rb-yellow); font-variant-numeric: tabular-nums; } + +/* Development — the button only exists while the GM has the phase open. */ +.rb-cbtn-dev { + color: var(--rb-void); background: var(--rb-yellow); border-color: var(--rb-yellow); +} +.rb-cbtn-dev:hover { background: var(--rb-white); border-color: var(--rb-white); } +.rb-dev-count { + font-family: var(--rb-body); font-size: 11px; font-weight: 700; letter-spacing: .06em; + text-transform: uppercase; color: var(--rb-danger); +} +.rb-dev-open { color: var(--rb-yellow); font-size: 13px; margin: 4px 0; } diff --git a/system.json b/system.json index 4fcbd58..4c6c988 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.4.2", + "version": "1.5.0", "compatibility": { "minimum": 13, "verified": "14.364" diff --git a/templates/character-sheet.hbs b/templates/character-sheet.hbs index 58b4ebf..dc06fa6 100644 --- a/templates/character-sheet.hbs +++ b/templates/character-sheet.hbs @@ -181,6 +181,13 @@ {{/if}}
{{skillCount}} {{localize "RINGBRP.Sheet.SkillsShown"}} + {{#if development.marked}} + {{development.marked}} {{localize "RINGBRP.Dev.Marked"}} + {{/if}} + {{#if development.open}} + {{localize "RINGBRP.Dev.Button"}} + {{/if}} {{localize "RINGBRP.Sheet.FullList"}}
{{#each RINGBRP.categories as |cat key|}} diff --git a/templates/chat-development.hbs b/templates/chat-development.hbs new file mode 100644 index 0000000..693b25e --- /dev/null +++ b/templates/chat-development.hbs @@ -0,0 +1,19 @@ +
+
{{localize "RINGBRP.Dev.Title"}}
+ {{#if open}} +

{{localize "RINGBRP.Dev.Opened"}}

+ {{#if rows.length}} + + + + {{#each rows as |r|}}{{/each}} + +
{{localize "RINGBRP.Party.Who"}}{{localize "RINGBRP.Dev.Marked"}}
{{r.name}}{{r.n}}
+

{{localize "RINGBRP.Dev.PressIt"}}

+ {{else}} +

{{localize "RINGBRP.Exp.NoTicks"}}

+ {{/if}} + {{else}} +

{{localize "RINGBRP.Dev.Closed"}}

+ {{/if}} +
diff --git a/tools/check-behaviour.mjs b/tools/check-behaviour.mjs index 3a9ba53..5dd3e89 100644 --- a/tools/check-behaviour.mjs +++ b/tools/check-behaviour.mjs @@ -564,6 +564,59 @@ test("no roster agent has Anomaly Lore 0", async () => { "induction's lowest band is 0, so a probationer still cannot roll it"); }); +/* ---------------------------------------------------------------- * + * Development — reversed from Call of Cthulhu + * ---------------------------------------------------------------- */ + +// The whole point of the house rule: a skill is marked by FAILING, not by working. +// If this ever flips back, characters stop improving at the things they are bad at, +// which is the entire behaviour the rule exists to produce. +test("only failure and fumble mark a skill for development", () => { + for (const good of ["critical", "special", "success"]) + assert.equal(RULES.developmentMarksOn(good), false, `${good} marked a skill`); + for (const bad of ["failure", "fumble"]) + assert.equal(RULES.developmentMarksOn(bad), true, `${bad} did not mark a skill`); +}); +// Roll OVER to improve, so the better you are the less a case teaches you. Together +// with marking-on-failure this is what makes ratings converge instead of running away. +test("development improves on ANOTHER failure, and ties do not count", () => { + assert.equal(RULES.developmentImproves(72, 45), true); + assert.equal(RULES.developmentImproves(30, 45), false); + assert.equal(RULES.developmentImproves(45, 45), false, "a tie improved the skill"); + assert.equal(RULES.developmentImproves(1, 0), true, "a rating of 0 could not improve"); + assert.equal(RULES.developmentImproves(100, 100), false, "a rating of 100 still improved"); +}); +test("a d100 gain cannot push a skill past the ceiling", () => { + // 98% + the best possible 1d6 would be 104% without the clip + assert.equal(98 + RULES.developmentGainFor(98, 6, null), RULES.DEVELOPMENT.ceiling); +}); + +test("a skill with a trainingCap stops there, not at the global ceiling", () => { + assert.equal(60 + RULES.developmentGainFor(60, 6, 62), 62); + assert.equal(RULES.developmentGainFor(62, 6, 62), 0, "already at its cap, gains nothing"); +}); + +test("trainingCap is honoured but cannot raise the ceiling above it", () => { + assert.equal(RULES.developmentCeiling(250), RULES.DEVELOPMENT.ceiling); + assert.equal(RULES.developmentCeiling(null), RULES.DEVELOPMENT.ceiling); +}); + +test("a skill at the ceiling can still be marked but never gains", () => { + // 96-00 always fails, so even a maxed skill gets marked - it just never improves + assert.ok(RULES.developmentMarksOn("failure")); + assert.equal(RULES.developmentGainFor(RULES.DEVELOPMENT.ceiling, 6, null), 0); +}); + +test("the gain is a die, not a fixed number", () => + assert.match(RULES.DEVELOPMENT.gain, /^\d*d\d+$/)); +// The two halves must disagree: what marks a skill in play must be an outcome the +// development roll treats as improvement, or the rule reads as two unrelated coins. +test("the marking outcome and the improving outcome are the same idea", () => { + assert.ok(RULES.DEVELOPMENT.marksOn.includes("failure")); + assert.equal(RULES.DEVELOPMENT.improveOn, "over", + "improveOn changed; the rulebook and primer describe rolling over"); +}); + /* ---------------------------------------------------------------- */ await runAll(); diff --git a/tools/check-rules.mjs b/tools/check-rules.mjs index af9762a..92fe21e 100644 --- a/tools/check-rules.mjs +++ b/tools/check-rules.mjs @@ -197,6 +197,23 @@ const checks = [ ["panicReliefFor('special')", RULES.panicReliefFor("special"), 2], ["panicReliefFor('failure')", RULES.panicReliefFor("failure"), 0], ["panicReliefFor('fumble')", RULES.panicReliefFor("fumble"), -1], + + // Development is REVERSED from the usual: failure marks the skill, not success. + ["developmentMarksOn('failure')", RULES.developmentMarksOn("failure"), true], + ["developmentMarksOn('fumble')", RULES.developmentMarksOn("fumble"), true], + ["developmentMarksOn('success')", RULES.developmentMarksOn("success"), false], + ["developmentMarksOn('special')", RULES.developmentMarksOn("special"), false], + ["developmentMarksOn('critical')",RULES.developmentMarksOn("critical"), false], + ["developmentImproves(72,45)", RULES.developmentImproves(72, 45), true], + ["developmentImproves(30,45)", RULES.developmentImproves(30, 45), false], + ["developmentImproves(45,45)", RULES.developmentImproves(45, 45), false], + ["developmentCeiling(null)", RULES.developmentCeiling(null), 100], + ["developmentCeiling(75)", RULES.developmentCeiling(75), 75], + ["developmentCeiling(140)", RULES.developmentCeiling(140), 100], + ["developmentGainFor(45,6,null)", RULES.developmentGainFor(45, 6, null), 6], + ["developmentGainFor(97,6,null)", RULES.developmentGainFor(97, 6, null), 3], + ["developmentGainFor(73,6,75)", RULES.developmentGainFor(73, 6, 75), 2], + ["developmentGainFor(75,6,75)", RULES.developmentGainFor(75, 6, 75), 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/primer-text.mjs b/tools/primer-text.mjs index cc983d8..cb15b08 100644 --- a/tools/primer-text.mjs +++ b/tools/primer-text.mjs @@ -16,7 +16,8 @@ import { TRADE_BANDS, INDUCTION } from "../postings.mjs"; import { hitPointsFor, majorWoundFor, damageModifierFor, defencePenaltyFor, COHERENCE_BANDS, RANGE_LADDER, CIRCUMSTANCE, - PANIC_BANDS, PANIC_TRIGGERS, PANIC_RELIEF, PANIC_MAX, PANIC_AFTER_BREAK, PANIC_BREAK + PANIC_BANDS, PANIC_TRIGGERS, PANIC_RELIEF, PANIC_MAX, PANIC_AFTER_BREAK, PANIC_BREAK, + DEVELOPMENT } from "../rules.mjs"; import { readFile } from "node:fs/promises"; @@ -412,6 +413,30 @@ export const PRIMER_PAGES = [ + P("Panic clears when the scene does. Somewhere safe, door shut, " + "somebody has made tea: you are back to nought. Coherence does not clear.") }, +{ name: "8c · Getting better at things", content: + P("You improve at what BEATS you." + + "Not at what you are already good at. If you have played this kind of game before, " + + "this is the opposite way round, and it is deliberate.") + + H(2, "During the case") + + P("Every time a skill fails you, it gets marked. You do not have to " + + "do anything — the sheet marks it. A success marks nothing. Nobody learns " + + "anything from the lock that opened first time.") + + H(2, "At the end") + + P("The GM opens the development phase and a button appears on your " + + "sheet. Press it once. For every marked skill you roll 1D100, and:") + + TABLE(["The roll", "What happens"], [ + ["Over your current rating — another failure", + "the skill goes up ${DEVELOPMENT.gain}"], + ["Equal to or under your rating — you made it", + "nothing. You knew that already."] + ]) + + P("Marks clear either way, and the next case starts clean.") + + P("Why it works like that. A skill you are bad at fails all the " + + "time, so it gets marked, and then it fails the development roll too, so it climbs " + + "fast. A skill you are already good at hardly ever fails, and when it does it " + + "usually passes the development roll and gains nothing. Everybody's weak spots " + + "close and nobody runs away with a 95%.") }, + { name: "9 · The whole game, in one page", content: P("You play ordinary adults in the modern world, pulled into an old and quiet " + "government department because you stay yourselves when reality goes wrong.") diff --git a/tools/rules-text.mjs b/tools/rules-text.mjs index 2f46397..7793c21 100644 --- a/tools/rules-text.mjs +++ b/tools/rules-text.mjs @@ -29,7 +29,8 @@ import { crossingArtPoolFor, crossingGrantsArt, crossingCostFor, REACTION, BLEED, CIRCUMSTANCE, RANGE_LADDER, rangeBandFrom, MAJOR_WOUND_TABLE, COHERENCE_BANDS, COHERENCE_COSTS, COHERENCE_RECOVERY, - PANIC_BANDS, PANIC_TRIGGERS, PANIC_RELIEF, PANIC_MAX, PANIC_AFTER_BREAK, PANIC_BREAK + PANIC_BANDS, PANIC_TRIGGERS, PANIC_RELIEF, PANIC_MAX, PANIC_AFTER_BREAK, PANIC_BREAK, + DEVELOPMENT } 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"; @@ -686,10 +687,28 @@ find the brief was, as always, incomplete. They sort it out or they do not. They back and are debriefed, and the debrief is where coherence is recovered and where the GM finds out what the players think they did.

-

Afterwards

-

Every skill you used successfully under pressure is ticked. At the end of the case -each ticked skill rolls to improve: beat your current rating on 1D100 and it goes up -1D6. Ticks clear either way.

+

Afterwards — development

+

This is the opposite way round from most games in this family, and it is meant +to be.

+

A skill is marked when it FAILS you — ${DEVELOPMENT.marksOn.join(' or ')} — +and not when it works. Nobody learns anything from the lock that opened first time.

+

At the end of a case the GM opens the development phase, and each +agent rolls once for every marked skill. Roll 1D100 OVER your current rating and +it goes up ${DEVELOPMENT.gain}. If you make the roll you have learned nothing this +time. Marks clear either way.

+

So the two halves pull against each other on purpose. A skill you are bad at fails +constantly, gets marked, and then fails its development roll and climbs quickly. A skill +you are good at rarely fails at all, and when it does it usually passes the development +roll and gains nothing. Ratings converge instead of running away.

+

Running the phase at the table

+

The GM opens it with the Development phase toggle in the token +controls on the left of the screen, which only GMs see. Opening it posts a card naming +every agent and how many skills each has marked, and puts a Development +button on every agent's sheet. Each player presses their own button once; the rolls and +the results go to chat. Marks clear as they are rolled, so a second press does nothing. +Close the toggle when everyone has rolled.

+

A scene has to be open on the canvas for the toggle to respond — that is true of +every control on that bar, not just this one.

Cases do not need to connect. The running thread of a campaign is not a plot — it is the four coherence tracks going down.

` },