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 <noreply@anthropic.com>
852 lines
46 KiB
JavaScript
852 lines
46 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,
|
||
FALL, fallDamageFor, ASPHYXIA, breathRoundsFor, FIRE, BLAST, BLAST_COVER,
|
||
BURST, burstHitsFor, TWO_WEAPON, twoWeaponPenaltyFor,
|
||
FIRE_MODE, SECOND_SHOT, SUSTAIN, SIGHT, LIGHTING, LIGHT_FLOOR, LAMP,
|
||
SHIELD, shieldWearFor, COVERAGE,
|
||
STATIONS, SPACE_BANDS, SPACE_RANGE_MOD, HEAT, heatEffectsFor, spaceHitFor,
|
||
powerStartingRating, brawlFallbackFor, bleedStoppedBy,
|
||
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,
|
||
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";
|
||
|
||
const LANG = JSON.parse(await readFile(new URL("../lang/en.json", import.meta.url), "utf8"));
|
||
const L = key => LANG[key] ?? key;
|
||
|
||
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 panicRows = [...PANIC_BANDS].reverse().map(b =>
|
||
[String(b.min), L(b.label), b.all ? `${pc(b.all)} everything` : "none", L(b.note)]);
|
||
const panicTrigRows = Object.values(PANIC_TRIGGERS)
|
||
.map(t => [L(t.label), `+${t.cost}`]);
|
||
const panicReliefRows = Object.values(PANIC_RELIEF)
|
||
.map(r => [L(r.label), r.reach, r.once]);
|
||
|
||
const cohRows = COHERENCE_BANDS.map((b, i) => {
|
||
const hi = i === 0 ? 10 : COHERENCE_BANDS[i - 1].min - 1;
|
||
const rng = b.min === hi ? `${b.min}` : `${hi}–${b.min}`;
|
||
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: "2b · The world, and how it kills you", content: `
|
||
<h2>Falling</h2>
|
||
<p>A die of damage for every ${FALL.metresPerDie} metres, to a random location, and
|
||
<strong>armour does not help</strong> — the ground is not a weapon. Landing on
|
||
something soft halves the distance, and so does a controlled descent — but they do not
|
||
stack, because they are the same mitigation twice. A descent harness makes any drop a
|
||
controlled one, which is what it is for.</p>
|
||
${table(["Fall", "Damage"], [3, 6, 12, 20, 30, 45].map(m =>
|
||
[`${m} m`, fallDamageFor(m).formula]))}
|
||
|
||
<h2>Not breathing</h2>
|
||
<p>You hold on for CON rounds with warning, half that without. After that it is
|
||
${ASPHYXIA.perRound} point a round and it does not stop, because the water does not
|
||
get bored. Armour is irrelevant and so is your Major Wound: this one runs straight to
|
||
zero and then the dying count begins.</p>
|
||
${table(["CON", "Ready for it", "Caught out"], [8, 12, 16, 18].map(c =>
|
||
[String(c), `${breathRoundsFor(c)} rounds`, `${breathRoundsFor(c, {prepared:false})} rounds`]))}
|
||
|
||
<h2>Fire</h2>
|
||
<p>Fire is not a hit, it is a condition with a size. Worn armour helps on the round
|
||
you <em>catch</em> and never again, because after that the fire is inside it.</p>
|
||
${table(["Fire", "Damage a round", "Burns for"],
|
||
FIRE.map(f => [L(f.label), f.damage, `${f.rounds} rounds`]))}
|
||
|
||
<h2>Explosives</h2>
|
||
<p>One roll of damage at the centre, shared out by how close you were. <strong>Cover
|
||
is the answer; armour is only ever half of one</strong>, and only at contact. The order
|
||
is fixed: <em>rolled × range share × cover, rounded down once, and only then does
|
||
armour come off</em>.</p>
|
||
${table(["Where you were", "Share of the damage", "Armour"],
|
||
BLAST.map(b => [L(b.label), `${Math.round(b.share * 100)}%`,
|
||
b.armour < 1 ? "half of it" : "all of it"]))}
|
||
${table(["Cover", "What gets through"],
|
||
Object.entries(BLAST_COVER).map(([k, v]) => [k, v === 0 ? "nothing" : `${Math.round(v * 100)}%`]))}
|
||
` },
|
||
|
||
{ name: "2c · How a weapon fires, and what is bolted to it", content: `
|
||
<h2>What a weapon can do with a round</h2>
|
||
<p>Every weapon has a <strong>fire mode</strong>, and it is a decision rather than a
|
||
drawback. The trade runs one way: the faster a weapon puts rounds out, the less each
|
||
individual shot is worth, and the slower it is, the more the one shot you get is worth
|
||
taking properly.</p>
|
||
${table(["Mode", "To hit", "What it offers"],
|
||
Object.values(FIRE_MODE).map(m => [L(m.label), pc(m.attack),
|
||
m.burst.length ? "bursts"
|
||
: m.sustain ? "the beam can be held on the target"
|
||
: m.second ? `a second shot, at ${pc(SECOND_SHOT)} to both`
|
||
: m.reloads ? "one shot, then work"
|
||
: "one aimed shot a round"]))}
|
||
<p>A weapon that cannot fire a burst <strong>cannot be made to</strong>. A muzzle-loader
|
||
has no automatic setting however the button is pressed.</p>
|
||
|
||
<h2>Holding a beam</h2>
|
||
<p>A beam does not fire discrete rounds, so counting hits is the wrong shape for it.
|
||
What a beam can do instead is stay on the target, and that buys <strong>damage</strong>
|
||
— the one firing choice in this game that adds dice rather than adding rolls. It is
|
||
paid for twice, in accuracy and in charge.</p>
|
||
${table(["", "To hit", "Damage", "Charge"],
|
||
Object.values(SUSTAIN).map(s => [L(s.label), pc(s.mod),
|
||
s.bonus ? `+${s.bonus}` : "—", `×${s.charge}`]))}
|
||
|
||
<h2>Sights</h2>
|
||
<p>A sight is not a flat bonus, which is why most games end up with everybody carrying
|
||
the same scope. A sight is ground for a distance and is a <strong>liability at the
|
||
others</strong>. You never choose to apply it: the range band is already known by the
|
||
time the modifier is wanted, so a carried sight applies itself.</p>
|
||
${table(["Sight", "Short", "Medium", "Long", "Extreme"],
|
||
Object.values(SIGHT).filter(s => s.id !== "none").map(s => [L(s.label),
|
||
pc(s.bands.short), pc(s.bands.medium), pc(s.bands.long), pc(s.bands.extreme)]))}
|
||
<p>The telescopic sight is a <strong>penalty inside ten metres</strong>. That is the
|
||
whole point of it, and it is why nobody sensibly carries all four. A sight only counts
|
||
if it <em>fits</em> — the scope and the thermal sight are rifle furniture and do nothing
|
||
bolted to a service pistol.</p>
|
||
|
||
<h2>Shields</h2>
|
||
<p>A shield is the one piece of armour you <em>hold</em>, and it protects you exactly
|
||
as much as you are currently holding it up. <strong>Raised</strong>, it adds its points
|
||
to what it covers and it is the only armour you can also defend with — a parry on the
|
||
Shield skill. <strong>Lowered</strong>, it is weight.</p>
|
||
<p>What stops "always raised" being the only answer is that it occupies a hand and you
|
||
are shooting around your own shield: ${pc(SHIELD.ranged)} to your own ranged attacks
|
||
while it is up. It is obviously right for a containment officer and obviously wrong for
|
||
a marksman, which is the point.</p>
|
||
<p><strong>Shields break.</strong> A blow the shield <em>absorbed</em> — stopped
|
||
outright, or merely dragged down a step — costs it a point of wear for every step the
|
||
attack stood above an ordinary success — so an ordinary blow costs
|
||
it nothing, a special costs ${shieldWearFor("special")}, and a critical costs
|
||
${shieldWearFor("critical")}. At its limit it is firewood: no points, nothing to parry
|
||
with, and it drops out of your hand.</p>
|
||
|
||
<h2>The dark</h2>
|
||
<p>Most crossings do not come with the lights on.</p>
|
||
${table(["", "To hit"], Object.values(LIGHTING).map(l => [L(l.label), pc(l.mod)]))}
|
||
<p>A lamp buys the dark back to <strong>${L(LIGHTING[LIGHT_FLOOR].label)}</strong> and
|
||
never better — and it is a position. The card will say so.</p>
|
||
<p><strong>A lamp lights everyone standing in it</strong>, not only the person holding
|
||
it. Each one has a reach: ${LAMP.reachM} metres for the issue field lamp, less for a
|
||
weapon lamp, a great deal more for a vehicle searchlight. Anyone inside that reach is
|
||
in the light, with everything that means in both directions — the pool is one position
|
||
and the whole group is standing in it. If your tokens are on the map the game works
|
||
this out for you; if they are not, the attack dialog asks.</p>
|
||
<p>A thermal sight removes the dark outright <em>and</em> does not announce you, which
|
||
is most of what you are paying its weight for.</p>
|
||
|
||
<h2>Automatic fire</h2>
|
||
<p>One roll, not four. The <strong>success level</strong> says how much of the burst
|
||
arrived, and <strong>the rounds are gone either way</strong> — that is the honest part
|
||
of automatic fire and the part players forget.</p>
|
||
${table(["Burst", "Rounds", "To hit", "Critical", "Special", "Success"],
|
||
Object.values(BURST).map(b => [L(b.label), String(b.rounds), pc(b.mod),
|
||
String(burstHitsFor("critical", b.id)), String(burstHitsFor("special", b.id)),
|
||
String(burstHitsFor("success", b.id))]))}
|
||
<p>A critical burst puts its <em>first</em> round through armour and the rest
|
||
normally. However many rounds connect, <strong>a burst is one wounding event</strong>
|
||
and rolls the Major Wound table at most once. A fumble is a jam, an empty magazine, or
|
||
a muzzle that climbed — and the GM decides what was behind the target.</p>
|
||
|
||
<h2>Two weapons</h2>
|
||
<p>The off hand is a real second attack and it is meant to cost something. Untrained,
|
||
it costs you in <em>both</em> hands, because a person waving two knives is worse with
|
||
each of them.</p>
|
||
${table(["", "Main hand", "Off hand"], [
|
||
["Untrained", pc(twoWeaponPenaltyFor().main), pc(twoWeaponPenaltyFor().off)],
|
||
["<strong>Ambidextrous</strong>", pc(twoWeaponPenaltyFor({trained:true}).main),
|
||
pc(twoWeaponPenaltyFor({trained:true}).off)]
|
||
])}
|
||
<p>Ambidextrous buys <strong>consistency, not a free attack</strong> — both hands at
|
||
the same penalty rather than a full-strength main hand and a cheap extra, which made it
|
||
the only sane build for anyone who could hold two things. The off-hand weapon must be
|
||
the lighter of the two and no heavier than ${TWO_WEAPON.offHandMaxKg} kg, which is the
|
||
rule that stops two poleaxes.</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: "4b · Panic", content: `
|
||
<p><em>Coherence is what knowing has cost you, and it is measured in weeks. Panic is
|
||
what is happening to you now, and it is measured in minutes. Do not confuse them at
|
||
the table: one goes down over a career, the other goes up over a corridor.</em></p>
|
||
|
||
<p>Panic runs from <strong>0 up to ${PANIC_MAX}</strong>. You take a point when
|
||
something happens to you — <strong>no roll</strong>. Asking for a die to find out
|
||
whether you are frightened slows the scene down and tells you how to feel. The die is
|
||
for getting rid of it.</p>
|
||
|
||
${table(["Panic", "Band", "Penalty", "What it looks like"], panicRows)}
|
||
|
||
<p>The first point is free. At Panic 1 you are rattled and you roll at no penalty,
|
||
because the opening shock of a scene should be colour rather than arithmetic.</p>
|
||
|
||
<h2>Taking a point</h2>
|
||
${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 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
|
||
everybody who can hear you — which is the department's actual work, and the only relief
|
||
that reaches a whole room. <strong>Psychology</strong> is the only one that steadies
|
||
somebody who is not you. <strong>Religion</strong> steadies you and nobody else, and it
|
||
does not care whether you are right.</p>
|
||
|
||
<h2>Breaking</h2>
|
||
<p>At ${PANIC_MAX} you break. Choose one — <strong>${PANIC_BREAK.join(", ")}</strong> —
|
||
take it, and come back at <strong>${PANIC_AFTER_BREAK}</strong>. You do not come back at
|
||
zero. Breaking is not a rest.</p>
|
||
|
||
<p>Panic clears when the scene does. When you are somewhere safe with the door shut and
|
||
somebody has made tea, you are at nought again.</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>
|
||
|
||
<h2>Or a trade, and six weeks</h2>
|
||
<p>The department does not only promote from within, because there is no degree in
|
||
crossings. It recruits <strong>a doctor, a joiner, a detective sergeant</strong> — and
|
||
puts them through the training centre.</p>
|
||
${table(["Trade", "Key", "What they were"],
|
||
Object.values(TRADES).map(r => [`<strong>${r.label}</strong>`, r.stat.toUpperCase(), r.blurb ?? ""]))}
|
||
<p>A trade character is built in two halves, and the join is the interesting part.</p>
|
||
<p><strong>The trade is prior career, and it does not move with rank.</strong> It is
|
||
rated at ${TRADE_BANDS.core[0]}–${TRADE_BANDS.core[1]}% for what you did for a living
|
||
and ${TRADE_BANDS.support[0]}–${TRADE_BANDS.support[1]}% for what came with it, whether
|
||
you were sworn in last month or ten years ago. Fifteen years of picking locks is not
|
||
something the department conferred and it does not improve because somebody was
|
||
promoted.</p>
|
||
<p><strong>Induction is the department's, so it does.</strong> ${INDUCTION.blurb} It
|
||
teaches ${INDUCTION.skills.map(k => LANG[`RINGBRP.Skill.${k}`] ?? k).join(", ")}, and
|
||
issues a sidearm, a vest, cordon kit, a cover identity and a ward on top of standing
|
||
issue.</p>
|
||
${table(["Rank", "Induction at"],
|
||
Object.entries(INDUCTION.bandByTier).map(([k, b]) =>
|
||
[TIERS[k] ? TIERS[k].label : k, `${b[0]}–${b[1]}%`]))}
|
||
<p>So a surgeon recruited last year is a far better surgeon than they are a Custodian,
|
||
and the distance between those two numbers is most of what it feels like to play one —
|
||
and it is a distance that closes with service rather than one they are stuck with.</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 — development</h2>
|
||
<p><em>This is the opposite way round from most games in this family, and it is meant
|
||
to be.</em></p>
|
||
<p><strong>A skill is marked when it FAILS you</strong> — ${DEVELOPMENT.marksOn.join(' or ')} —
|
||
and not when it works. Nobody learns anything from the lock that opened first time.</p>
|
||
<p>At the end of a case the GM opens the <strong>development phase</strong>, and each
|
||
agent rolls once for every marked skill. <strong>Roll 1D100 OVER your current rating and
|
||
it goes up ${DEVELOPMENT.gain}</strong>. If you make the roll you have learned nothing this
|
||
time. Marks clear either way.</p>
|
||
<p>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.</p>
|
||
<h3>Running the phase at the table</h3>
|
||
<p>The GM opens it with the <strong>Development phase</strong> 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 <strong>Development</strong>
|
||
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.</p>
|
||
<p><em>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.</em></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: "6b · Space, and everybody in it", content: `
|
||
<h2>Stations</h2>
|
||
<p>The usual way to run a spacecraft fight is that one player flies and everybody else
|
||
watches. This does not do that. A craft has <strong>stations</strong>; every agent
|
||
takes one, every station uses a skill they already have, and the round is one roll
|
||
each in the normal reaction order. There is no map and no vector arithmetic.</p>
|
||
${table(["Station", "Rolls", "Does", "Heat"],
|
||
Object.values(STATIONS).map(st => [L(st.label), st.skill.replace(/_/g, " "),
|
||
st.effect, st.heat === 0 ? "—" : (st.heat > 0 ? `+${st.heat}` : String(st.heat))]))}
|
||
<p>The Physician has a chair. So does the Translation Officer. That is the point.</p>
|
||
|
||
<h2>Range is a band</h2>
|
||
${table(["Band", "Gunnery"], SPACE_BANDS.map(b =>
|
||
[L(`RINGBRP.SpaceBand.${b}`),
|
||
SPACE_RANGE_MOD[b] === null ? "nothing to shoot at" : pc(SPACE_RANGE_MOD[b])]))}
|
||
<p>The helm moves you <strong>one band a round</strong>, in or out, and a critical
|
||
moves two. Knife range is where a small craft wins and where one bad round ends it.</p>
|
||
|
||
<h2>Heat is the whole game</h2>
|
||
<p>Shooting and manoeuvring make the craft hot. Gunnery is charged whether or not it
|
||
hits, because the shot left. Engines vent it — 3, or more on a good roll — and
|
||
everything else is free.</p>
|
||
${table(["Heat", "What it costs you"], [
|
||
["0–3", "nothing"],
|
||
[`${HEAT.loudFrom}–${HEAT.blindFrom - 1}`, `loud: everything can see you, ${pc(heatEffectsFor(HEAT.loudFrom).penalty)} on sensors and gunnery`],
|
||
[`${HEAT.blindFrom}–${HEAT.slowFrom - 1}`, `blind as well, ${pc(heatEffectsFor(HEAT.blindFrom).penalty)}`],
|
||
[`${HEAT.slowFrom}–${HEAT.max}`, "and the helm cannot change band at all"]
|
||
])}
|
||
<p>Marking a target with Sensors adds one hit to the next gunnery roll, which is the
|
||
reason that chair is worth filling. And the anchor is a station: transposing out of a
|
||
fight is always available, it costs Heat, and it is not tidy.</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", "never succeeds, whatever your skill — and is a fumble if it is inside your fumble range"],
|
||
["00", "always a fumble, whatever your skill"],
|
||
["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>` }
|
||
];
|