Hit points clamp at zero, so further damage to somebody already there changed nothing — and a STABILISED agent absorbed it forever, because the count was held and there was nothing else left to move. You could empty a magazine into someone bleeding out and the system would not notice. Damage at zero now breaks the stabilisation and costs a round of the count, and hitLocationRoll calls died() when that meets the limit. Found by playing LOW VERGE until it killed somebody: two agents who were medically stable died in the water on a failed tidal extraction. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
596 lines
31 KiB
JavaScript
596 lines
31 KiB
JavaScript
/**
|
||
* 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,
|
||
DYING, dyingLimitFor, LAST_ENTRY,
|
||
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>⁄<sub>20</sub> of your skill", "<strong>Critical</strong>", "the best version of what you wanted"],
|
||
["up to <sup>1</sup>⁄<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 ±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) ÷ 2, rounded up", `${CEYLA.con} + ${CEYLA.siz} → <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 ÷ ${REACTION.dexDivisor}, rounded up`, `${CEYLA.dex} → ${cReflex}`],
|
||
["Awareness", `Readiness ÷ ${REACTION.readinessDivisor}, rounded up`, `${CEYLA.readiness} → <strong>${cAware}</strong>`]
|
||
])}
|
||
<p><strong>Readiness</strong> is the best of your Tradecraft, Insight, Spot or
|
||
Listen, floored at INT × 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},
|
||
× ${REACTION.laneMultiplier} = ${cBase}, less ${cLoad} for
|
||
${cEnc.over} kg over her free carry → <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
|
||
±${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> −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>Zero hit points</h2>
|
||
<p>Hit points do not go below zero. Zero starts a clock instead.</p>
|
||
${table(["Hit points", "You are"], [
|
||
["above 2", "<strong>up</strong> — no restriction from injury alone"],
|
||
["2 or below", "<strong>down</strong> — out of the fight, still conscious, still yours"],
|
||
["0", "<strong>dying</strong> — unconscious, no actions, no defences, and the count begins"]
|
||
])}
|
||
<p>Each round at zero adds ${DYING.perRound} to the count. When the count reaches your
|
||
own <strong>Major Wound</strong> threshold you are <strong>dead</strong> — so the same
|
||
number that decides how much damage you can take standing up also decides how long you
|
||
have on the floor. A minimum of ${DYING.minCount} rounds applies however slight you are.</p>
|
||
<p><strong>Being hurt again at zero</strong> breaks any stabilisation and costs a
|
||
round of the count. Hit points cannot go below zero, so this is what further damage to
|
||
a body on the floor actually does — and it is why leaving somebody stable in a place
|
||
that is still dangerous is not the same as getting them out.</p>
|
||
<p><strong>Stabilising</strong> takes ${DYING.stabiliseWith.join(" or ")} and stops the
|
||
count. It does not put you back on your feet: you are still at zero and still
|
||
unconscious until somebody heals you above it. Healing above zero clears the count.
|
||
A <em>fumbled</em> attempt over a dying agent costs them a round they do not have.</p>
|
||
|
||
<h3>The last entry</h3>
|
||
<p>A Custodian's death is a filing question, because everything here is. What the
|
||
record ends up saying depends on how well filed they were when it happened.</p>
|
||
${table(["Coherence at death", "What the record says"],
|
||
LAST_ENTRY.map((e, i) => {
|
||
const upper = i === 0 ? "10" : String(LAST_ENTRY[i - 1].atLeast - 1);
|
||
return [e.atLeast === 0 ? "0" : `${e.atLeast}–${upper}`, L(e.label)];
|
||
}))}
|
||
<p>The badly filed are not recorded as dead at all. That is the department's problem
|
||
first, and then somebody else's.</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), `−${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],
|
||
`−${powerCostFor(k)}`, `−${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", "—", "no"],
|
||
["Failure", "you arrive near your target, not at it",
|
||
`−${crossingCostFor("failure")}`, "only if you were already Unmoored"],
|
||
["Fumble", "you come back, and not the same",
|
||
`−${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 — Anchored, or Loose", crossingArtPoolFor(10).map(a => L(`RINGBRP.Art.${a}.name`)).join(", ")],
|
||
["4 to 3 — Adrift", crossingArtPoolFor(4).map(a => L(`RINGBRP.Art.${a}.name`)).join(", ")],
|
||
["2 and below — 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 → Approach → Investigation → Confrontation →
|
||
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 ±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 ? `−${e.movPenalty}` : "—",
|
||
reactionLoadFrom(e.srLoad) ? `−${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}, × ${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>` }
|
||
|
||
];
|