Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ec7efd29b | ||
|
|
5aaae186e0 |
@@ -180,9 +180,9 @@ The build runs all five guards before it writes anything:
|
||||
|
||||
<!-- guards:start -->
|
||||
```
|
||||
check-rules: OK — 92 rules, 20 files scanned, 3 aliases + 45 constant sets checked, 290 formulas verified
|
||||
check-rules: OK — 92 rules, 20 files scanned, 3 aliases + 45 constant sets checked, 291 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, 976 keys, no leaf/branch collisions
|
||||
check-behaviour: OK — 55 behavioural tests
|
||||
check-lang: OK — en.json, 977 keys, no leaf/branch collisions
|
||||
check-behaviour: OK — 57 behavioural tests
|
||||
```
|
||||
<!-- guards:end -->
|
||||
|
||||
+2
-1
@@ -974,5 +974,6 @@
|
||||
"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"
|
||||
"RINGBRP.Chat.ToEverything": "to everything",
|
||||
"RINGBRP.Panic.Backfired": "Fumbled —"
|
||||
}
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2026/08/17-21:58:29.401379 16e463000 Delete type=3 #1
|
||||
2026/08/17-23:31:35.181337 1726ef000 Delete type=3 #1
|
||||
|
||||
@@ -1 +1 @@
|
||||
2026/08/17-21:58:29.473673 16fc87000 Delete type=3 #1
|
||||
2026/08/17-23:31:35.256758 173707000 Delete type=3 #1
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2026/08/17-21:58:29.403187 16e463000 Delete type=3 #1
|
||||
2026/08/17-23:31:35.183232 171ee3000 Delete type=3 #1
|
||||
|
||||
@@ -1 +1 @@
|
||||
2026/08/17-21:58:29.456562 16ec6f000 Delete type=3 #1
|
||||
2026/08/17-23:31:35.237581 171ee3000 Delete type=3 #1
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2026/08/17-21:58:29.438818 16ec6f000 Delete type=3 #1
|
||||
2026/08/17-23:31:35.219821 173707000 Delete type=3 #1
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2026/08/17-21:58:29.468218 16f47b000 Delete type=3 #1
|
||||
2026/08/17-23:31:35.250188 1726ef000 Delete type=3 #1
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2026/08/17-21:58:29.424479 16f47b000 Delete type=3 #1
|
||||
2026/08/17-23:31:35.204640 173707000 Delete type=3 #1
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2026/08/17-21:58:29.464811 16f47b000 Delete type=3 #1
|
||||
2026/08/17-23:31:35.246285 171ee3000 Delete type=3 #1
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2026/08/17-21:58:29.461075 16f47b000 Delete type=3 #1
|
||||
2026/08/17-23:31:35.242302 172efb000 Delete type=3 #1
|
||||
|
||||
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
2026/08/17-21:58:29.471824 16e463000 Delete type=3 #1
|
||||
2026/08/17-23:31:35.254558 1726ef000 Delete type=3 #1
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2026/08/17-21:58:29.395237 16e463000 Delete type=3 #1
|
||||
2026/08/17-23:31:35.174562 171ee3000 Delete type=3 #1
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2026/08/17-21:58:29.466508 16ec6f000 Delete type=3 #1
|
||||
2026/08/17-23:31:35.248311 1726ef000 Delete type=3 #1
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2026/08/17-21:58:29.449085 16ec6f000 Delete type=3 #1
|
||||
2026/08/17-23:31:35.230398 172efb000 Delete type=3 #1
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2026/08/17-21:58:29.443432 16ec6f000 Delete type=3 #1
|
||||
2026/08/17-23:31:35.224441 1726ef000 Delete type=3 #1
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2026/08/17-21:58:29.397941 16fc87000 Delete type=3 #1
|
||||
2026/08/17-23:31:35.177864 1726ef000 Delete type=3 #1
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2026/08/17-21:58:29.405177 16fc87000 Delete type=3 #1
|
||||
2026/08/17-23:31:35.185513 171ee3000 Delete type=3 #1
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2026/08/17-21:58:29.399814 16fc87000 Delete type=3 #1
|
||||
2026/08/17-23:31:35.179597 1726ef000 Delete type=3 #1
|
||||
|
||||
+8
-4
@@ -1580,7 +1580,10 @@ export async function easePanic(actor, reliefKey, level = "success", { note = ""
|
||||
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);
|
||||
// A fumble comes back as negative relief, so the card has to say so — otherwise it
|
||||
// reads "Psychology" over a number that just went up, and the table reads it as a bug.
|
||||
await panicCard(actor, before, after, game.i18n.localize(String(spec?.label ?? reliefKey ?? "")),
|
||||
note, { backfired: back < 0 });
|
||||
return panicBandFrom(after);
|
||||
}
|
||||
|
||||
@@ -1596,13 +1599,13 @@ export async function breakPanic(actor, choice = "freeze") {
|
||||
return panicBandFrom(after);
|
||||
}
|
||||
|
||||
async function panicCard(actor, before, after, reason, note) {
|
||||
async function panicCard(actor, before, after, reason, note, { backfired = false } = {}) {
|
||||
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),
|
||||
band, breaks: panicBreaksAt(after), penalty: panicPenaltyFor(after), backfired,
|
||||
choices: PANIC_BREAK,
|
||||
pips: Array.from({ length: PANIC_MAX }, (_, i) => i < after)
|
||||
})
|
||||
@@ -6175,7 +6178,8 @@ Hooks.once("init", () => {
|
||||
reactionLoadFrom, reactionPrintedFrom,
|
||||
readinessRating, percentileFaces, circumstanceTotal,
|
||||
coherenceBand, coherencePenaltyFor, woundPenaltyFor, loseCoherence, regainCoherence, displace,
|
||||
panicBand, panicPenaltyFor, panicBreaksAt, gainPanic, easePanic, breakPanic,
|
||||
panicBand, panicPenaltyFor, panicBreaksAt, panicReliefFor, panicAfter,
|
||||
gainPanic, easePanic, breakPanic,
|
||||
isAcross, setAcross, useTransposed, debrief, activeCaseFile, returnCrossing, caseMembers,
|
||||
earnedDiscoveries, deviceFunctionStatus, earnDiscovery, useDeviceFunction, pickContactActor,
|
||||
resolveContact, newEncounter, claimRewards, activeRewardEffects,
|
||||
|
||||
@@ -358,10 +358,18 @@ 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. */
|
||||
/**
|
||||
* A success takes one point, a special or better takes two — and a FUMBLE puts one on.
|
||||
*
|
||||
* Returned as a negative relief so the whole thing stays one number and one direction:
|
||||
* easePanic subtracts what this gives it, so -1 subtracts a negative and the track goes
|
||||
* up. Trying to talk somebody down and making it worse is the most familiar thing in
|
||||
* this list, and until now it was the only outcome that cost nothing.
|
||||
*/
|
||||
export function panicReliefFor(level) {
|
||||
if (level === "critical" || level === "special") return 2;
|
||||
if (level === "success") return 1;
|
||||
if (level === "fumble") return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -1831,3 +1831,12 @@
|
||||
color: var(--rb-void); background: var(--rb-danger);
|
||||
padding: 5px 8px; margin: 6px 0 0;
|
||||
}
|
||||
|
||||
/* A fumbled relief puts a point ON. The card must say which way it went, or it reads
|
||||
as "Psychology" printed over a number that just rose, and the table reads that as a
|
||||
bug rather than as the rule. */
|
||||
.rb-panic-worse {
|
||||
font-family: var(--rb-body); font-size: 11px; font-weight: 700; letter-spacing: .08em;
|
||||
text-transform: uppercase; color: var(--rb-void); background: var(--rb-danger);
|
||||
padding: 2px 6px; margin-right: 6px;
|
||||
}
|
||||
|
||||
+1
-1
@@ -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.1.1",
|
||||
"version": "1.1.3",
|
||||
"compatibility": {
|
||||
"minimum": 13,
|
||||
"verified": "14.364"
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<span class="rb-panic-band rb-panic-{{band.id}}">{{localize band.label}}</span>
|
||||
</div>
|
||||
|
||||
<p class="rb-panic-reason">{{reason}}{{#if note}} — <em>{{note}}</em>{{/if}}</p>
|
||||
<p class="rb-panic-reason">{{#if backfired}}<span class="rb-panic-worse">{{localize "RINGBRP.Panic.Backfired"}}</span> {{/if}}{{reason}}{{#if note}} — <em>{{note}}</em>{{/if}}</p>
|
||||
<p class="rb-panic-note">{{localize band.note}}</p>
|
||||
|
||||
{{#if penalty}}<p class="rb-panic-pen">{{penalty}}% {{localize "RINGBRP.Chat.ToEverything"}}</p>{{/if}}
|
||||
|
||||
@@ -482,13 +482,25 @@ 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", () => {
|
||||
test("a special takes two points, a failure takes none, a fumble ADDS one", () => {
|
||||
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);
|
||||
// Negative relief. Trying to steady somebody and making it worse is the most familiar
|
||||
// thing on the list and it used to be the only outcome that cost nothing.
|
||||
assert.equal(RULES.panicReliefFor("fumble"), -1);
|
||||
});
|
||||
// The sign has to survive the arithmetic easePanic does, or a fumble quietly heals.
|
||||
test("a fumbled relief puts a point ON, through the same subtraction", () => {
|
||||
const before = 2;
|
||||
const after = RULES.panicAfter(before, -RULES.panicReliefFor("fumble"));
|
||||
assert.equal(after, 3, "a fumbled relief did not raise panic");
|
||||
assert.equal(RULES.panicAfter(before, -RULES.panicReliefFor("success")), 1);
|
||||
});
|
||||
// And it must still not be able to push past the top.
|
||||
test("a fumbled relief at 5 stays at 5", () =>
|
||||
assert.equal(RULES.panicAfter(5, -RULES.panicReliefFor("fumble")), RULES.PANIC_MAX));
|
||||
// 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");
|
||||
|
||||
@@ -196,6 +196,7 @@ const checks = [
|
||||
["panicReliefFor('success')", RULES.panicReliefFor("success"), 1],
|
||||
["panicReliefFor('special')", RULES.panicReliefFor("special"), 2],
|
||||
["panicReliefFor('failure')", RULES.panicReliefFor("failure"), 0],
|
||||
["panicReliefFor('fumble')", RULES.panicReliefFor("fumble"), -1],
|
||||
// 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],
|
||||
|
||||
@@ -546,7 +546,9 @@ ${table(["This happens", "Panic"], panicTrigRows)}
|
||||
|
||||
<h2>Getting rid of one</h2>
|
||||
<p>Roll it. A success takes one point off; a <strong>special or better takes two</strong>.
|
||||
A failure takes nothing and costs nothing but the moment you spent on it.</p>
|
||||
A failure costs nothing but the moment you spent on it — and a <strong>fumble puts a
|
||||
point ON</strong>. Trying to steady somebody and making it worse is the most familiar
|
||||
thing on this list.</p>
|
||||
${table(["How", "Reaches", "How often"], panicReliefRows)}
|
||||
|
||||
<p><strong>Occult is the odd one.</strong> Naming a thing correctly, out loud, helps
|
||||
|
||||
Reference in New Issue
Block a user