Files
RingBRP/tools/rules-text.mjs
T
slaguru666andClaude Opus 5 cc23fdb7bd Rulebook and compendium refresh
Audited every exported rule in rules.mjs against what the rulebook says and closed
the gaps:

- Dodge starts at DEX x 2 — newly evaluated, and stated nowhere until now
- Encumbrance: free carry, and which skill categories it actually penalises
- Bleeding rates and what stops each kind
- The merge ladder for Borrowed Authority, as a table
- A new page listing all 25 postings, generated from ROLES

The rulebook is now 9 pages and every one is generated from the code it documents.
All packs, 231 icons and the dice textures rebuilt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 20:31:01 +01:00

564 lines
29 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* THE RULES, generated from the rules themselves.
*
* Every number in this journal is computed from rules.mjs at build time and every
* name is read from lang/en.json, so the book cannot say one thing while the engine
* does another. That failure has happened five times in this project already; a
* rulebook written by hand would have been the sixth.
*
* Written to be learned in order and abandoned early: pages 1 to 3 are the whole
* playable game, 4 to 6 are what happens when it goes wrong, and the rest is
* reference you look up rather than read.
*/
import { readFile } from "node:fs/promises";
import {
freeCarryFor, hitPointsFor, majorWoundFor, damageModifierFor, locationMaxHp,
reflexRating, awarenessRating, reactionBaseFrom, reactionLoadFrom, reactionPrintedFrom,
encumbranceFrom, woundEffectsFor, bleedRateFor,
defencePenaltyFor, defenceOutcomeFor, ARTS, powerCostFor,
SCOPE_TIERS, scopeStanceFor, scrutinyFor, authorityOutcomeFor,
dossierTrustFor, widenScopeCostFor, DOSSIER, MERGE_STAGES,
skillBaseFrom, encumbrancePenaltyFor, ENCUMBERED_CATEGORIES,
powerStartingRating, brawlFallbackFor, bleedStoppedBy,
crossingArtPoolFor, crossingGrantsArt, crossingCostFor,
REACTION, BLEED, CIRCUMSTANCE, RANGE_LADDER, rangeBandFrom,
MAJOR_WOUND_TABLE, COHERENCE_BANDS, COHERENCE_COSTS, COHERENCE_RECOVERY
} from "../rules.mjs";
const LANG = JSON.parse(await readFile(new URL("../lang/en.json", import.meta.url), "utf8"));
const L = key => LANG[key] ?? key;
// ROLES lives in ringbrp.mjs, which cannot be imported outside Foundry — it reaches
// for globals on load. Read the literal out of the source, the way check-kits does,
// so the postings page is generated from the same table the game uses.
const ROLES = eval("(" + (await readFile(new URL("../ringbrp.mjs", import.meta.url), "utf8"))
.match(/export const ROLES = (\{[\s\S]*?\n\});/)[1] + ")");
const pc = n => `${n > 0 ? "+" : ""}${n}%`;
/* ---------- small builders so the tables below stay readable ---------- */
const table = (head, rows) =>
`<table><thead><tr>${head.map(h => `<th>${h}</th>`).join("")}</tr></thead><tbody>` +
rows.map(r => `<tr>${r.map((c, i) => `<td${i ? ' style="text-align:right"' : ""}>${c}</td>`).join("")}</tr>`).join("") +
`</tbody></table>`;
/* ---------- derived figures, computed not typed ---------- */
const D = REACTION.die;
const band = 2; // two agents one lane apart, for the odds quoted below
const oddsOneLane = (() => {
let w = 0, t = 0, n = D * D;
for (let a = 1; a <= D; a++) for (let b = 1; b <= D; b++) {
const x = REACTION.laneMultiplier + a;
if (x > b) w++; else if (x === b) t++;
}
return { win: (100 * w / n).toFixed(0), tie: (100 * t / n).toFixed(0) };
})();
const damageLadder = [6, 12, 20, 28, 36, 45, 55, 65, 75]
.map(t => [`${t}`, damageModifierFor(t)]);
const woundRows = MAJOR_WOUND_TABLE.map(r => {
const eff = woundEffectsFor([{ key: r.key }]);
const bits = [];
if (eff.all) bits.push(`${pc(eff.all)} to everything`);
if (eff.manipulation) bits.push(`${pc(eff.manipulation)} hands, melee and ranged`);
if (eff.physical) bits.push(`${pc(eff.physical)} physical`);
if (eff.perception) bits.push(`${pc(eff.perception)} perception`);
if (eff.communication) bits.push(`${pc(eff.communication)} communication`);
if (eff.movFactor !== 1 && eff.movFactor > 0) bits.push(`MOV x${eff.movFactor}`);
if (eff.bleed) bits.push(`<strong>${eff.bleed} hp every round</strong>`);
if (eff.lostAction) bits.push("lose your next action");
if (eff.unconscious) bits.push("<strong>unconscious</strong>");
else if (eff.defenceOnly) bits.push("<strong>out of the fight</strong>, MOV 1");
return [`${r.min}`, L(`RINGBRP.Wound.${r.key}.title`), bits.join(" · ") || "—"];
});
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}`;
const eff = [b.all ? `${pc(b.all)} everything` : "", b.comm ? `${pc(b.comm)} communication` : ""]
.filter(Boolean).join(", ") || "none";
return [rng, L(b.label), eff];
});
/* Ceyla, from the shipped pregens — real numbers, so the worked example is checkable */
const CEYLA = { dex: 10, readiness: 71, con: 12, siz: 13, carried: 15.4, free: 12 };
const cReflex = reflexRating(CEYLA.dex);
const cAware = awarenessRating(CEYLA.readiness);
const cBase = reactionBaseFrom(CEYLA.dex, CEYLA.readiness);
const cEnc = encumbranceFrom(CEYLA.carried, CEYLA.free);
const cLoad = reactionLoadFrom(cEnc.srLoad);
const cPrinted = reactionPrintedFrom(cBase, cEnc.srLoad);
const cHp = hitPointsFor(CEYLA.con, CEYLA.siz);
const cMw = majorWoundFor(cHp);
export const RULES_PAGES = [
{ name: "1 · The whole game", content: `
<p><em>You are a Custodian. Something is here that should not be. Go there, sort it
out, come back. It is the coming back that gets harder.</em></p>
<h2>The only rule you must know</h2>
<p>Everything you attempt has a percentage. <strong>Roll 1D100 and get equal to or
under it.</strong> That is the whole engine. Everything else is a modifier to that
number or a consequence of the result.</p>
${table(["Roll", "Result", "What it means"], [
["01 to <sup>1</sup>&frasl;<sub>20</sub> of your skill", "<strong>Critical</strong>", "the best version of what you wanted"],
["up to <sup>1</sup>&frasl;<sub>5</sub> of your skill", "<strong>Special</strong>", "notably better than asked for"],
["up to your skill", "<strong>Success</strong>", "you do it"],
["over your skill", "Failure", "you do not"],
["the top 5% of the range", "<strong>Fumble</strong>", "you do it wrong, and it costs"]
])}
<p>A skill of 60 criticals on 01–03, specials on 04–12, succeeds to 60, and fumbles
on 98–00. The sheet prints those four numbers next to every skill so you never work
them out.</p>
<p><strong>Two rules override everything.</strong> <strong>96 to 99 never succeed</strong>,
however good you are, and <strong>00 is always a fumble</strong>. So no skill is ever
a certainty: a 100% expert still comes unstuck one roll in twenty. And no skill is
ever hopeless either — the floor is 1%, so there is always the chance of a
critical.</p>
<h2>The three things that change the number</h2>
<ol>
<li><strong>Difficulty</strong> — set by the GM before you roll. Average leaves it
alone, Difficult halves it, Easy doubles it.</li>
<li><strong>Circumstance</strong> — the situation, from ${pc(CIRCUMSTANCE[0].mod)} to
${pc(CIRCUMSTANCE.at(-1).mod)}, capped at &plusmn;40% however many apply.</li>
<li><strong>Your condition</strong> — wounds, what you are carrying, and how far you
have drifted from your own reality. All three are printed on the sheet and applied
automatically.</li>
</ol>
<p>Difficulty first, then everything else. A skill can never be reduced below 1%.</p>
<h2>Where the numbers come from</h2>
${table(["Thing", "How", "Ceyla, for example"], [
["Hit points", "(CON + SIZ) &divide; 2, rounded up", `${CEYLA.con} + ${CEYLA.siz} &rarr; <strong>${cHp}</strong>`],
["Major Wound", "half your hit points, rounded up", `<strong>${cMw}</strong> in one blow`],
["Damage modifier", "STR + SIZ on a ladder", "see the reference page"],
["Reaction", "your best route to acting first", `<strong>${cPrinted}</strong>`]
])}
<h2>That is enough to play</h2>
<p>Page 2 is fighting and page 3 is being hurt. Pages 4 and 5 are the setting doing
its work. The last page is a table you look things up in and never read.</p>` },
{ name: "2 · Fighting", content: `
<h2>Who goes first</h2>
<p>Your sheet prints one number: <strong>Reaction</strong>. Add <strong>1D${D}</strong>
to it. Highest acts first; a tie goes to the higher DEX. That is it — no
per-weapon arithmetic, and the die is rolled fresh every round.</p>
<p>Reaction is <strong>the better of two routes</strong>, because being quick and
being alert are both ways of acting first:</p>
${table(["Route", "How", "Ceyla"], [
["Reflex", `DEX &divide; ${REACTION.dexDivisor}, rounded up`, `${CEYLA.dex} &rarr; ${cReflex}`],
["Awareness", `Readiness &divide; ${REACTION.readinessDivisor}, rounded up`, `${CEYLA.readiness} &rarr; <strong>${cAware}</strong>`]
])}
<p><strong>Readiness</strong> is the best of your Tradecraft, Insight, Spot or
Listen, floored at INT &times; 3 so a clever but untrained agent is never flat. Take
the better of the two routes, multiply by ${REACTION.laneMultiplier}, and subtract
what you are carrying. Ceyla: the better of ${cReflex} and ${cAware} is ${cAware},
&times; ${REACTION.laneMultiplier} = ${cBase}, less ${cLoad} for
${cEnc.over} kg over her free carry &rarr; <strong>${cPrinted}</strong>.</p>
<p>An analyst can be quicker than a shooter. That is deliberate.</p>
<h3>Moving the marker</h3>
<p>Only three things move you on the rail during a fight, and they cap at
&plusmn;${REACTION.modCap}:</p>
<ul>
<li><strong>Aim</strong> +1 or +2 — spend a round aiming, act earlier the next one.
It is spent when it pays out.</li>
<li><strong>Edge</strong> +1 — equipment, a talent, high ground, a warning.</li>
<li><strong>Drag</strong> &minus;1 — surprised, in the dark, waist-deep in water.</li>
</ul>
<p>One lane apart wins about ${oddsOneLane.win}% of the time, ties ${oddsOneLane.tie}%.
Two lanes apart cannot be caught by the die at all.</p>
<h2>Your turn</h2>
<p><strong>One offensive action</strong>, plus as many defensive reactions as you like
at an escalating penalty. Attacking, aiming, taking cover, reloading, giving First
Aid and operating a device each use the action.</p>
<p><strong>Movement is free</strong> — 15 m if you also act, 30 m if you do not. So
you may move and take cover, but not take cover and shoot.</p>
<h3>Defending</h3>
<p>Defences are not actions and you may make as many as you like. The catch is that
they get harder, and you may not mix them: having chosen to dodge, you dodge.</p>
${table(["Defence this round", "Modifier"],
[0,1,2,3].map(n => [`${n + 1}${["st","nd","rd","th"][Math.min(n,3)]}`,
defencePenaltyFor(n) ? `${defencePenaltyFor(n)}%` : "no penalty"]))}
<p><strong>Dodge starts at DEX × 2</strong> and training raises it from there —
a DEX 13 agent who has never been taught to get out of the way still dodges on
${skillBaseFrom("dex*2", { dex: { value: 13 } })}%. Every posting can defend itself;
some are simply much better at it.</p>
<p><strong>Dodge</strong> uses the skill of that name and avoids the blow.
<strong>Parry</strong> uses the weapon in your hand and stops it. Standing in front
of three people is therefore possible, and stupid.</p>
<p>A defence is <strong>graded against the blow it answers</strong>. It stops a blow
of its own quality or worse outright. Against something better it still helps, but
only by as much as it was outclassed by — so an ordinary parry will not cancel a
critical, it will only reduce it. <strong>Only a critical stops a critical.</strong></p>
${table(["Blow", "vs critical", "vs special", "vs success", "vs failed defence"],
["critical", "special", "success"].map(atk =>
[`<strong>${atk}</strong>`, ...["critical", "special", "success", "failure"].map(def => {
const o = defenceOutcomeFor(atk, def);
return o.turnedAside ? "<em>turned aside</em>" : `lands as ${o.landsAt}`;
})]))}
<p>Read the reduced blow as the result for everything that follows: a critical cut
down to a special no longer ignores armour, it merely halves it.</p>
<h2>Access</h2>
<p>There is no hacking skill and nothing to break into. What an agent has is a
<strong>borrowed authority</strong>: a named credential belonging to somebody real,
with a scope of things that person could plausibly do, a Trust that erodes as it is
used, and a red flag or two describing how the real owner behaves.</p>
<p><strong>Inside its scope there is no roll.</strong> A facilities contractor opening
a plant room is not an act of intrusion, it is Tuesday. It costs 1 Trust and it works.
What costs you is asking the record to believe something one step beyond what the
person was for.</p>
${table(["What you are asking", "Against a credential for routine work"],
SCOPE_TIERS.slice(1).map(t => {
const stance = scopeStanceFor("routine", t);
return [L(`RINGBRP.Scope.${t}`),
stance === "in-scope" ? "<strong>no roll</strong> — 1 Trust"
: stance === "stretch" ? "a stretch — roll for it"
: "<strong>impossible</strong>, at any skill"];
}))}
<p>Two tiers beyond the credential is not difficult, it is <em>impossible</em>. No
percentage on any sheet gets a contractor issuing accounts. That is the rule that
stops one agent quietly owning every system in the setting.</p>
<h3>Behaving unlike them</h3>
<p>Each red flag you break is ${pc(scrutinyFor(1))} on the roll. The system is not
testing whether you are clever. It is testing whether you are behaving like the
person whose name is on the record.</p>
${table(["Result", "What happens to the credential"], [
["<strong>Critical</strong>", "it works, and Trust goes UP by 1"],
["<strong>Special</strong>", "it works and costs nothing"],
["<strong>Success</strong>", "it works; Trust down 1"],
["<strong>Failure</strong>", "refused; Trust down 1"],
["<strong>Fumble</strong>", "<strong>burned</strong> — and somebody real is going to be asked about it"]
])}
<p>At <strong>0 Trust</strong> the account is flagged and will not pass again until a
scene is spent rehabilitating it. Burned is permanent.</p>
<h3>The dossier</h3>
<p>A credential is not found, it is assembled. Up to ${DOSSIER.maxItems} dossier
items, <strong>each a separate scene using a different skill</strong>, and at most
${DOSSIER.maxComputerUse} of them at a keyboard — so access is something the whole
team builds and not one specialist's party trick. A dossier item never makes the
later roll easier; it makes the credential better. Trust starts at
${dossierTrustFor(0)} and rises to ${dossierTrustFor(DOSSIER.maxItems)} with a full
dossier, and the item that establishes what the person is actually <em>for</em>
widens the scope.</p>
<h3>Making it true</h3>
<p>You may spend Coherence to widen a scope, because reality can be made to remember
that the person always held that duty. The first costs
${widenScopeCostFor(0)} and each one after costs more — ${widenScopeCostFor(1)}, then
${widenScopeCostFor(2)}. It works. It always works.</p>
<p>Push a credential past what its Trust will carry and your file and theirs converge
one step, and <strong>nothing in this game spends anything to converge them back</strong>:
a circular arrives addressed to them; your pension record shows service you did not do;
colleagues are certain you chaired a meeting you did not attend; and at the end,
Registry holds one file where there were two, and yours is the copy.</p>
${table(["Times you pushed it past its Trust", "What has happened to your file"],
MERGE_STAGES.map((k, i) => [String(i), L(k)]))}
<h2>Shooting and hitting</h2>
${table(["Range", "Modifier"], RANGE_LADDER.map(r =>
[L(`RINGBRP.Range.${r.id}`) === `RINGBRP.Range.${r.id}` ? r.id : L(`RINGBRP.Range.${r.id}`),
r.mod === null ? "not possible" : pc(r.mod)]))}
<p>Cover <em>penalises</em>, it does not block: solid partial cover is
${pc(-30)} to the attacker and never removes line of sight.</p>
<h2>What a hit does</h2>
${table(["Result", "Damage", "Armour"], [
["<strong>Critical</strong>", "maximum weapon damage, plus a rolled damage modifier", "<strong>ignored entirely</strong>"],
["<strong>Special</strong>", "normal", "counts half, rounded down"],
["Success", "normal", "full"],
["Fumble", "none", "you drop or jam it; a firearm needs a round to clear"]
])}
<p>The critical is the pressure valve. Good armour makes an ordinary hit survivable
and does nothing at all about a critical.</p>` },
{ name: "3 · Getting hurt", content: `
<h2>Hit points and locations</h2>
<p>Damage comes off your total <strong>and</strong> the location it struck. Each
location has its own capacity, a fraction of your total.</p>
<ul>
<li>Damage in a location beyond its capacity <strong>disables</strong> it.</li>
<li>Twice its capacity <strong>destroys</strong> it.</li>
</ul>
<p>A disabled leg halves your movement and costs ${pc(-30)} physical; a disabled arm
costs ${pc(-30)} to anything done with hands, which includes shooting. The figure on
the Combat tab colours in as this happens.</p>
<h2>Major Wounds</h2>
<p>Take <strong>half your hit points or more in a single blow</strong> and roll
1D10 on this table. Ceyla's threshold is ${cMw}.</p>
${table(["1D10", "Wound", "What it does to you"], woundRows)}
<p>These stack, with each other and with a shattered limb, and they are applied to
every roll you make. The Skills tab shows a red bar when you are carrying any.</p>
<h2>Bleeding</h2>
${table(["Wound", "Per round", "Stopped by"], Object.entries(BLEED).map(([k, v]) =>
[L(`RINGBRP.Wound.${k}.title`), `${v.perRound} hp`,
v.stop.map(s => `${s.difficulty ? `<strong>${s.difficulty}</strong> ` : ""}${s.family.replace("_", " ")}`).join(", or ")]))}
<p>Blood loss is charged at the end of every round and does not stop on its own.
Someone has to spend an action. That is the decision the rule exists to create.</p>
<h2>Healing</h2>
<p>First Aid restores a few points and closes a bleed. Real recovery takes days at
home, or hours in an autodoc. Nothing in the field puts a destroyed limb back.</p>` },
{ name: "4 · Coherence", content: `
<p><em>You are from here. Every time you do something that only makes sense somewhere
else, you are a little less from here.</em></p>
<p>Coherence runs from <strong>10 down to 0</strong>. Ten is anchored. Zero is
displaced, and displacement is permanent.</p>
${table(["Coherence", "Band", "Effect"], cohRows)}
<p>Communication degrades first and worst. You can still do the job for a while, and
the people you are doing it for stop recognising you long before you stop being
useful.</p>
<h2>What it costs</h2>
${table(["Occurrence", "Cost"], Object.values(COHERENCE_COSTS)
.sort((a, b) => a.cost - b.cost)
.map(c => [L(c.label), `&minus;${c.cost}`]))}
<p>Charged once per occurrence. The first two losses cost you nothing at all — and
the third drops you out of Anchored, by which point you have stopped watching.</p>
<h2>What brings it back</h2>
<p>Only at home, and never faster than one step at a time: a debrief
+${COHERENCE_RECOVERY.debrief}, a week of downtime +${COHERENCE_RECOVERY.downtimeWeek},
time with an <strong>anchor</strong> +${COHERENCE_RECOVERY.anchorObject}. An anchor is
a person or object that remembers you as you were.</p>
<h2>The way home is never closed</h2>
<p>Being Unmoored makes the return dearer, never impossible. Another agent may always
aim it for you at no penalty; alone you may always aim it at
${pc(COHERENCE_BANDS.find(b => b.id === "unmoored").returnPenalty)}; and a failed
return does not strand you either — you arrive near your target rather than at it.
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.</p>` },
{ name: "5 · The Quiet Arts", content: `
<p><em>Some agents come back with something. It is not a gift and it is not
training — it is the crossing, left in a person.</em></p>
<h2>What an Art is</h2>
<p>A crossing is an <strong>induced filing error</strong>: you persuade the world you
are somewhere else and it moves you to resolve the contradiction. An Art is the same
trick, performed small and on purpose — a misfiling you commit deliberately.</p>
<p>So there are no fireballs. Every Art is a sentence about the record: what is filed
here, what was, what is about to be, and what could be persuaded not to have been.</p>
<h2>Using one</h2>
<p>Roll under its rating like any skill. It is governed by POW, and your own
<strong>Coherence penalises it</strong> — the further you have drifted, the worse you
are at the thing that made you drift.</p>
<p><strong>The cost is Coherence, and it is charged whether or not it works</strong>,
because the misfiling is committed the moment you try. Push an Art past its ordinary
reach and it costs more.</p>
${table(["Art", "It says", "Ordinary", "Pushed"],
Object.keys(ARTS).map(k => [
L(`RINGBRP.Art.${k}.name`),
{ thread: "what was together is still together",
echo: "when is loosely filed",
hollow: "this is not filed here",
witness: "the record agrees you belong",
unmake: "this was not present",
ledger: "read the record; amend it" }[k],
`&minus;${powerCostFor(k)}`, `&minus;${powerCostFor(k, { pushed: true })}`
]))}
<h2>Fumbling one</h2>
<p>A fumble is not a failure. <strong>The misfiling sticks.</strong> Something small
and true about the world is now different, and the GM never says which. That is the
cost of doing on purpose the thing the agency spends its budget undoing.</p>
<h2>Where they come from</h2>
<p><strong>Nobody learns an Art.</strong> They are not taught, bought, chosen at
character creation or found in a manual. A character who wants one has exactly one
route: <strong>go across, and have the way home go wrong.</strong></p>
<h3>Coming back</h3>
<p>The return is a second crossing and it has to be aimed. Roll Transposition.
Being Unmoored makes it dearer; another agent can always aim it for you at no
penalty. <strong>There is no result that leaves you across.</strong></p>
${table(["Return", "What happens", "Coherence", "An Art?"], [
["Success or better", "home, where and when you meant", "&mdash;", "no"],
["Failure", "you arrive near your target, not at it",
`&minus;${crossingCostFor("failure")}`, "only if you were already Unmoored"],
["Fumble", "you come back, and not the same",
`&minus;${crossingCostFor("fumble")}`, "<strong>always</strong>"]
])}
<h3>What follows you home</h3>
<p>You do not choose the Art, and the pool is decided by <strong>how far you had
already drifted when it went wrong</strong>. The worse the state you were in, the
worse the thing that comes back with you.</p>
${table(["Coherence at the time", "What can arrive"], [
["8 and up &mdash; Anchored, or Loose", crossingArtPoolFor(10).map(a => L(`RINGBRP.Art.${a}.name`)).join(", ")],
["4 to 3 &mdash; Adrift", crossingArtPoolFor(4).map(a => L(`RINGBRP.Art.${a}.name`)).join(", ")],
["2 and below &mdash; Unmoored", crossingArtPoolFor(1).map(a => L(`RINGBRP.Art.${a}.name`)).join(", ")]
])}
<p>Which is the trap the setting has been building the whole time: the agents most
likely to come back with an Art are the ones least able to afford using it.</p>` },
{ name: "5b · The postings", content: `
<h2>The postings</h2>
<p>Twenty-five of them. An agency this old has never got round to abolishing anything,
so the register still carries a Night Warden and an Antiquities Officer alongside the
Signals and Disposal Officers. Each posting has a key characteristic, the skills it
trains, the talents it leans on, and a loadout containing at least one thing issued to
no other posting.</p>
${table(["Posting", "Key", "What it is"],
Object.values(ROLES).map(r => [`<strong>${r.label}</strong>`, r.stat.toUpperCase(), r.blurb ?? ""]))}
<p>None of this is a class. A posting is what the department has you down as, and the
skills follow from the job rather than the other way round.</p>
` },
{ name: "6 · A case", content: `
<p>One case is one session. It has three tracks, and they are the pressure.</p>
<ul>
<li><strong>Containment</strong> — how well the anomaly is still contained. Falls when
the public sees something, when a body is left where it will be found, when a local
authority gets involved.</li>
<li><strong>Resources</strong> — kit, ammunition, favours, cover identities. Spent,
not regained mid-case.</li>
<li><strong>Clearance</strong> — how far the agency will back you. Falls when you go
off the brief. At zero the case becomes deniable and you are on your own.</li>
</ul>
<h2>The shape of a session</h2>
<p><strong>Briefing &rarr; Approach &rarr; Investigation &rarr; Confrontation &rarr;
Extraction.</strong> Each phase has a difficulty, a cost and the chance of a forced
encounter. Agents are briefed on a place, a time and a thing to do. They cross. They
find the brief was, as always, incomplete. They sort it out or they do not. They come
back and are debriefed, and the debrief is where coherence is recovered and where the
GM finds out what the players think they did.</p>
<h2>Afterwards</h2>
<p>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.</p>
<p>Cases do not need to connect. The running thread of a campaign is not a plot — it
is the four coherence tracks going down.</p>` },
{ name: "7 · Reference", content: `
<h2>Damage modifier</h2>
<p>STR + SIZ, added to melee and thrown damage.</p>
${table(["STR + SIZ", "Modifier"], damageLadder)}
<h2>Carrying it all</h2>
<p>Free carry is STR + SIZ in kilograms. Past that you are encumbered, and it costs
you ${pc(encumbrancePenaltyFor(1, "phys"))} per step on
${ENCUMBERED_CATEGORIES.length} skill categories — the physical ones — and nothing at
all on the rest. Thinking is not heavier when your pack is.</p>
${table(["Category", "Encumbered?"],
["phys", "melee", "ranged", "manip", "mental", "social"].map(c =>
[c, ENCUMBERED_CATEGORIES.includes(c) ? `yes, ${pc(encumbrancePenaltyFor(1, c))} a step` : "no"]))}
<h2>Bleeding</h2>
<p>A wound that bleeds takes it off you every round until somebody stops it, and what
stops it depends on how bad it is.</p>
${table(["Wound", "Per round", "Stopped by"],
Object.entries(BLEED).map(([k, v]) => [k, String(v.perRound),
v.stop.map(x => x.difficulty ? `${x.family} (${x.difficulty})` : x.family).join(", ")]))}
<h2>Circumstance</h2>
${table(["Situation", "Modifier"], CIRCUMSTANCE.map(c => [L(c.label).replace(/\s*\([^)]*\)/, ""), pc(c.mod)]))}
<p>Capped at &plusmn;40% no matter how many apply.</p>
<h2>Encumbrance</h2>
<p>Your free carry is <strong>STR + MAS</strong> in kilogrammes — the same pair that
sets your damage modifier. Over that:</p>
${table(["Over free carry", "Skills", "MOV", "Reaction"],
[0, 1, 5, 10, 15, 20, 30].map(kg => {
const e = encumbranceFrom(100 + kg, 100);
return [`${kg} kg`, e.skillPenalty ? pc(-e.skillPenalty) : "—",
e.movPenalty ? `&minus;${e.movPenalty}` : "—",
reactionLoadFrom(e.srLoad) ? `&minus;${reactionLoadFrom(e.srLoad)}` : "—"];
}))}
<p>Skills and MOV move in whole steps of 10 kg. Reaction is exact — every
kilogramme costs, so no weight is ever free.</p>
<p>The skill penalty applies to <strong>physical, melee, ranged and manipulation</strong>
only. A heavy pack does not make you worse at Knowledge.</p>
<h2>Location capacity</h2>
<p>A fraction of your total hit points, rounded up. For a ${cHp}-hit-point agent:</p>
${table(["Location", "Share", "Capacity"], [
["Head", "40%", `${locationMaxHp(cHp, 0.4)}`],
["Chest", "50%", `${locationMaxHp(cHp, 0.5)}`],
["Abdomen and legs", "42%", `${locationMaxHp(cHp, 0.42)}`],
["Arms", "35%", `${locationMaxHp(cHp, 0.35)}`]
])}
<h2>Rolls that ignore your skill</h2>
${table(["Roll", "Result"], [
["96–99", "always fails, whatever your skill"],
["00", "always a fumble"],
["01", "always at least a critical chance — the floor is 1%"]
])}
<h2>The turn, in order</h2>
<ol>
<li>Roll 1D${D}, add your printed Reaction, apply Aim, Edge or Drag.</li>
<li>Highest first. Ties to higher DEX.</li>
<li>On your turn: one action, free movement, unlimited defences at a rising penalty.</li>
<li>To attack: skill, then difficulty, then range and circumstance, then roll.</li>
<li>On a hit: damage, less armour by success level, to a rolled location.</li>
<li>Half your hit points in one blow? Roll 1D10 for a Major Wound.</li>
<li>End of round: anyone bleeding loses their hit points.</li>
</ol>` },
{ name: "8 · A round, worked through", content: `
<p>Ceyla Marspont, agency analyst, at a boathouse door with two Cleaners inside.
Every number here is the one her sheet actually prints.</p>
<h2>Initiative</h2>
<p>DEX ${CEYLA.dex} gives Reflex <strong>${cReflex}</strong>. Her Tradecraft is
${CEYLA.readiness}, giving Awareness <strong>${cAware}</strong> — she is not fast, she
is <em>ready</em>. The better route is ${cAware}, &times; ${REACTION.laneMultiplier} =
${cBase}. She is carrying ${CEYLA.carried} kg against a free carry of ${CEYLA.free},
so ${cEnc.over} kg over costs her ${cLoad}. Her sheet prints
<strong>${cPrinted}</strong>.</p>
<p>She rolls 1D${D} and gets 4: <strong>${Math.round((cPrinted + 4) * 10) / 10}</strong>.
The Cleaners print 12 and roll 5 and 2. Ceyla acts first, comfortably.</p>
<h2>The shot</h2>
<p>Redaction lance, Energy Weapon (Pistol) at 53%. The Cleaner is 25 m away —
medium range, ${pc(-10)}. Nothing else applies, so she needs
<strong>43</strong> or under. Her bands: critical on 01–02, special to 08, success to
43, fumble on 97+.</p>
<p>She rolls <strong>07</strong>. A <strong>special</strong>.</p>
<h2>The damage</h2>
<p>The lance does 1D4+6. She rolls 8. A special means the Cleaner's stab vest counts
<strong>half</strong> — 2 becomes 1 — so <strong>7</strong> gets through. The location
roll says the leg.</p>
<p>The Cleaner has 12 hit points, so a Major Wound needs 6. Seven is more than six:
<strong>roll 1D10</strong>. It comes up 6 — <strong>${L("RINGBRP.Wound.deepBleeding.title")}</strong>.
He is now losing <strong>${bleedRateFor([{ key: "deepBleeding" }])} hit points every
round</strong> until someone spends an action on him, and his friend has to decide
whether that someone is going to be him.</p>
<h2>What it cost her</h2>
<p>The lance is transposed materiel. Using it costs
<strong>1 Coherence</strong>, charged automatically. Ceyla drops from 10 to 9. Nothing
happens yet — the first two losses are free — which is exactly how the third one
arrives without anybody noticing.</p>` }
];