bestiary + check-powers: read the seam, and classify what that reveals

bestiary.mjs built the document from NPCS, so it described 47 creatures of
147 while check-bestiary agreed with it — two things agreeing is not
verification when both read the same wrong source. It reads collectSpecs()
now: 184 creatures across 5 sections, every one with a measured lethality
figure, which only works because check-lethality was fixed first.

Three faults surfaced fixing it:

  - a second NPCS reference in focusParagraph()
  - which then broke on `rows`, because focusParagraph declares its own
    local `rows` for focus-fire data and shadowed the creature list. Keyed
    lookup now, so the shadowing cannot bite again.
  - the document grouped on the raw role tag against a list of four, and
    `if (!group.length) continue` DROPS an unlisted tag rather than
    mis-filing it. Nine of the twelve tags were absent, so those creatures
    would not have appeared at all. Twelve tags now map onto five sections,
    with a new "Beasts and heraldry", and a completeness assertion counts
    every creature into a section and fails if one is unplaced.

check-powers was the fifth consumer bypassing all-specs.mjs. It reconciled
its counts against BESTIARY while reading NPCS, so it called the document
wrong when the document was right.

Reading the real population then demanded 101 classifications. All 101 are
done and NOT ONE was wired. That is the right answer, not a shortfall:
the three available effects describe the creature — its own attack rating,
the damage it takes, whether it feels defence stacking — and essentially
every power is conditional on a place, a posture, a round count, a weapon
type, an attacker count, a named agent, or Coherence. R-275 records what
wiring one anyway costs: 25.1% to 75.2% on a single creature.

The near-misses are the evidence the reluctance was applied, not recited:
tangie's "HALF damage" is edged-only and ends in open flame; the_heron is
+30% or -20% depending on movement the harness does not have, so which
branch applies is undecidable; mirrorline's halving lands on an AGENT, not
on the creature, so wiring it would have protected the wrong combatant;
yale's defence waiver is capped at two attackers and its written counter is
a fourth agent, which defenceStacking:"ignores" cannot express.

Classifications live in four band files and are spread into POWERS last, so
a hand-written entry always wins a collision. 142 classified: 2 wired, 78
beyond the harness with a reason, 62 not fight rules.

All 23 guards pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
slaguru666
2026-09-22 23:28:13 +01:00
co-authored by Claude Opus 5
parent 4d591f635d
commit f68b98190f
8 changed files with 2595 additions and 17 deletions
+2118 -9
View File
File diff suppressed because it is too large Load Diff
+60 -6
View File
@@ -24,7 +24,7 @@
import { writeFile, readFile } from "node:fs/promises";
import { existsSync } from "node:fs";
import path from "node:path";
import { NPCS } from "./content.mjs";
import { collectSpecs } from "./all-specs.mjs";
import { POWERS, combatPowerFor } from "./powers.mjs";
const wiredPowers = Object.values(POWERS).filter(p => p.kind === "wired").length;
@@ -54,13 +54,32 @@ const focus = existsSync(FOCUS)
/* ---------------------------------------------------------------- shaping */
const TAG_ORDER = ["FOLKLORE", "HORROR", "FORWARD", ""];
const TAG_ORDER = ["FOLKLORE", "BEAST", "HORROR", "FORWARD", ""];
const TAG_TITLE = {
FOLKLORE: "Folklore",
BEAST: "Beasts and heraldry",
HORROR: "Horror",
FORWARD: "The far side",
"": "Uncategorised"
};
/* Twelve role tags, five sections.
*
* The document grouped on the raw tag against a list of four, and `if (!group.length)
* continue` meant any tag not on that list was not mis-filed — it was DROPPED. Nine of
* the twelve tags the bestiary now uses were absent, so those creatures would simply not
* have appeared, in a document whose entire purpose is that a GM can look at them.
*
* Unmapped tags fall to "" rather than vanishing, and the completeness assertion below
* makes dropping one impossible rather than unlikely. */
const SECTION_OF = {
FOLKLORE: "FOLKLORE",
HERALDRY: "BEAST", BEAST: "BEAST", LIVESTOCK: "BEAST",
MENAGERIE: "BEAST", HYBRID: "BEAST",
HORROR: "HORROR", ANOMALOUS: "HORROR", REVENANT: "HORROR", ECHO: "HORROR",
FORWARD: "FORWARD", "FAR-SIDE": "FORWARD"
};
const sectionOf = tag => SECTION_OF[tag] ?? "";
const TAG_BLURB = {
FOLKLORE: "Older than the department and largely uninterested in it. Most of these are "
+ "arrangements rather than fights, and the arrangement is nearly always cheaper.",
@@ -68,6 +87,9 @@ const TAG_BLURB = {
+ "after the reason for it stopped.",
FORWARD: "The far side, and what it sends. This is where the non-human anatomies are: "
+ "a Vesh has no head to shoot off, a Cadence has no vital at all.",
BEAST: "Animals, and the things off the coats of arms that are shaped like animals. "
+ "Nearly all of these are hit-located as quadrupeds or winged beasts rather than as "
+ "people, so a wing or a foreleg is a real place to aim.",
"": "People, mostly — contractors, locals, and the ones who were already here."
};
@@ -111,7 +133,20 @@ function rowFor(spec) {
};
}
const rows = NPCS.map(rowFor);
/* Read the shared list, not content.mjs.
*
* This built the document from NPCS, so when the bestiary moved into its own files it
* went on describing 47 creatures out of 147 and check-bestiary agreed with it, because
* the guard compares the document against this generator rather than against the game.
* Two things agreeing with each other is not verification when both read the same wrong
* source. Scenario casts come in too, as they do for check-lethality: the Uncategorised
* section already exists for "people, mostly", which is what they are. */
const rows = (await collectSpecs()).filter(r => r.kind === "npc").map(r => rowFor(r.spec));
/* focusParagraph() declares its own local `rows` for the focus-fire data, so the creature
list is not reachable by that name inside it. Keyed lookup rather than a search, so the
shadowing cannot bite again. */
const nameByKey = new Map(rows.map(r => [r.spec.key, r.spec.name]));
const danger = r => r.measured ? r.measured.down : -1;
/* Which of them are the wrong shape.
@@ -166,7 +201,7 @@ function focusParagraph() {
const clear = rows.filter(r => r.gain > r.noise).length;
const best = rows.reduce((a, b) => (b.gain > a.gain ? b : a));
const bestKey = Object.keys(focus.creatures).find(k => focus.creatures[k] === best);
const bestName = (NPCS.find(n => n.key === bestKey) ?? {}).name ?? bestKey;
const bestName = nameByKey.get(bestKey) ?? bestKey;
const every = helped === rows.length && clear === rows.length;
return `**What you can do is kill them one at a time.** Against a pack, every agent putting\n`
+ `their attacks into the same creature beats spreading them \u2014 in ${every ? "**every one**" : `${helped}`} of the\n`
@@ -244,8 +279,27 @@ function focusParagraph() {
/* --- the entries themselves --- */
/* Nothing may be dropped.
*
* sectionOf() falls back to "" and "" is in TAG_ORDER, so at the moment nothing can be.
* That is an argument, not a guarantee: it stops holding the day somebody reorders
* TAG_ORDER or adds a section and forgets the fallback, and the failure mode is a
* creature quietly missing from a reference document — exactly what was happening
* before, and exactly the kind of thing nobody notices. Count them instead. */
{
const placed = new Set(TAG_ORDER.flatMap(tag =>
rows.filter(r => sectionOf(r.tag) === tag).map(r => r.spec.key)));
const dropped = rows.filter(r => !placed.has(r.spec.key));
if (dropped.length) {
console.error(`bestiary: FAILED — ${dropped.length} creature(s) belong to no section and `
+ `would not appear in the document: `
+ dropped.map(r => `${r.spec.key} (tag "${r.tag}")`).join(", "));
process.exit(1);
}
}
for (const tag of TAG_ORDER) {
const group = rows.filter(r => r.tag === tag).sort((a, b) => danger(b) - danger(a));
const group = rows.filter(r => sectionOf(r.tag) === tag).sort((a, b) => danger(b) - danger(a));
if (!group.length) continue;
md += `## ${TAG_TITLE[tag]}\n\n${TAG_BLURB[tag]}\n\n`;
for (const r of group) {
@@ -330,5 +384,5 @@ if (CHECK) {
await writeFile(OUT, md, "utf8");
console.log(`bestiary: wrote ${path.relative(ROOT, OUT)} — ${rows.length} creatures across `
+ `${TAG_ORDER.filter(t => rows.some(r => r.tag === t)).length} groupings, `
+ `${TAG_ORDER.filter(t => rows.some(r => sectionOf(r.tag) === t)).length} groupings, `
+ `${rows.filter(r => r.measured).length} with measured lethality`);
+11 -1
View File
@@ -21,7 +21,17 @@
*/
import { existsSync, readFileSync } from "node:fs";
import path from "node:path";
import { NPCS } from "./content.mjs";
import { collectSpecs } from "./all-specs.mjs";
/* Read the same list BESTIARY is generated from.
*
* This imported NPCS from content.mjs and reconciled its counts against docs/BESTIARY.md.
* That held only while the two had the same source. Once the bestiary moved into its own
* files, the document described 184 creatures and this guard checked 47 of them, so it
* reported the document wrong when the document was right — the fifth consumer found
* bypassing all-specs.mjs, after build-packs, bestiary, check-lethality and the art
* tools. A guard reconciling two numbers must read the population both of them describe. */
const NPCS = (await collectSpecs()).filter(r => r.kind === "npc").map(r => r.spec);
import { ROSTER } from "./roster.mjs";
import { POWERS, EFFECT_KINDS, combatPowerFor } from "./powers.mjs";
import { runFight, makeRng, seedFor, buildCombatant } from "./simulate.mjs";
+113
View File
@@ -0,0 +1,113 @@
/**
* POWER classification for the ANOMALOUS AND FAR-SIDE band (tools/bestiary-anomalous.mjs).
*
* Merged into POWERS in tools/powers.mjs, which holds the scheme, the three kinds and the
* three effects the simulator understands. Read that header first; this file is only the
* twenty-five verdicts for this band.
*
* Nothing here is wired, and that is the finding rather than an omission. Two things make
* this band unwirable almost by construction:
*
* The three effects only describe THIS creature's own attack rating, the damage IT takes,
* and whether IT feels the cumulative defence penalty. Several powers below look like a
* halving or a nullification until you ask who the number lands on — mirrorline's half
* damage lands on an AGENT, carried_over's attack still lands and does something worse
* than damage. Wiring either as damageFactor/attackFactor would be R-275 again: the
* supporter's wipe rate moved 25.1% to 75.2% because a conditional power was applied as
* an unconditional factor.
*
* Every power in the band carries a condition — a room, a sight line, a nearest agent, a
* Major Wound, a grapple held for two rounds, an uses-per-encounter count, a Coherence
* price. The harness has no ground, no agents it can name, no Coherence and no scene
* state, so each condition is exactly the thing it cannot check.
*
* The Cadence per-body arithmetic is NOT a power. −10% to everything per disabled body,
* no vital, no head and destructionOutcomeFor returning "none" are the CADENCE location
* table doing its job (bestiary-anomalous.mjs header). quorate, five_of_six, the_remainder,
* counterpoint and the_iteration all restate it in prose; the classification below covers
* only the extra rule each one puts on top. Likewise the Vesh headlessness that
* returned_surveyor and five_of_six talk about is the VESH table, not a power, and
* turnback's neck kill is the quadruped table.
*/
export const POWERS_ANOMALOUS = {
/* ---- vesh geometry ---- */
foldwalker: { name: "HALF PRESENT", kind: "notSimulable",
why: "every incoming attack is rolled twice and the lower result stands, and it stops once both exits "
+ "are closed; the harness resolves one roll and has no exits" },
anglesmith: { name: "THE NEW CORNER", kind: "notSimulable",
why: "it edits the room each round — a DEXx5 to cross, prone on a failure, a permanent extra exit after "
+ "four — and the harness has no room, no movement and no prone" },
halfpast: { name: "FORTY SECONDS", kind: "notSimulable",
why: "acts first regardless of Reaction and voids the first attack on it each round, both only until it "
+ "takes a Major Wound; the harness has no initiative order and no per-round attack counter" },
returned_surveyor: { name: "THE ELEVEN YEARS", kind: "outOfCombat",
why: "reading its survey notes trades 1 Coherence for a true fact about the far side; its own entry says "
+ "talk to it, and it fights only if restrained" },
unmeasured: { name: "NO TWO AGREE", kind: "outOfCombat",
why: "it breaks instruments and fails Chain of Custody — a case problem, and its entry says it does not attack" },
patient_grasp: { name: "DO NOT LET GO", kind: "notSimulable",
why: "it never strikes; it seizes on a Brawl and holds at STR 22 until the held agent reaches 0 from "
+ "exhaustion, and the harness models neither grapples nor exhaustion" },
the_tideline: { name: "THE MARK", kind: "outOfCombat",
why: "the mark is a clock that finds the agent again within a day, cleared by a reversed crossing "
+ "regulator; nothing about the fight it is in" },
glass_auditor: { name: "BROUGHT INTO COMPLIANCE", kind: "notSimulable",
why: "2d6 every third round, but only to whoever is standing where a wall has retroactively always been, "
+ "and the harness has no positions to stand in" },
/* ---- cadence swarms ---- */
quorate: { name: "THE VOTE", kind: "notSimulable",
why: "it does nothing until three bodies share line of sight to one target and then makes five attacks at "
+ "once; the harness has no sight lines and cannot gate a round on them" },
five_of_six: { name: "THE SIXTH PLACE", kind: "notSimulable",
why: "a grapple held two consecutive rounds removes an agent from the case entirely, resisted by POWx1; "
+ "the harness has no grapple, no multi-round hold and no agent to lose" },
the_remainder: { name: "THE GAP", kind: "notSimulable",
why: "once per encounter a body leaves the fight for two rounds and returns anywhere within fifty metres; "
+ "the harness has no ground and cannot suspend and restore a body" },
counterpoint: { name: "OUT OF SEQUENCE", kind: "notSimulable",
why: "damage is applied at the end of the FOLLOWING round so a disabled body still acts, unless the damage "
+ "was area damage; the harness applies damage immediately and has no area attacks" },
the_tally: { name: "THE RECONCILIATION", kind: "outOfCombat",
why: "its count becomes the department record at the end of the scene; its entry says it will not fight and "
+ "cannot win one, and the fix is paperwork" },
many_hands: { name: "TAKEN TO PIECES", kind: "notSimulable",
why: "each round a body removes one carried item on Sleight of Hand rather than doing damage, and the "
+ "harness has no inventory to take anything out of" },
the_iteration: { name: "THE LONG VIEW", kind: "notSimulable",
why: "it fixes the outcome of a named future action with no roll and charges 1 Coherence, and the harness "
+ "models neither declared actions nor Coherence" },
/* ---- echoes and revenants ---- */
backscatter: { name: "WRONG ANGLES", kind: "notSimulable",
why: "−30% to attacks AGAINST it, but only when aimed — blind fire is unpenalised — and there is no effect "
+ "for penalising incoming attacks, only attackFactor for its own" },
afterimage: { name: "THE SECOND TIME", kind: "outOfCombat",
why: "watching the loop costs 1 Coherence and hands over how the death happened; its entry says it is not "
+ "a threat and cannot leave the room" },
second_pass: { name: "I KNOW HOW THIS GOES", kind: "notSimulable",
why: "three no-roll cancellations of an agent's action per encounter, forfeited if the party changes the "
+ "scene; the harness has no agent actions to cancel and no scene to change" },
turnback: { name: "STILL TRAINED", kind: "outOfCombat",
why: "a correctly given department recall hands it to whoever gave the order for the rest of the scene — "
+ "whether it is an enemy at all is a handler question, not a fight rule" },
the_overflight: { name: "THE OTHER GROUND", kind: "notSimulable",
why: "it ignores cover that exists on this side and is stopped only by solid earth; the harness has no "
+ "cover and no terrain to distinguish" },
recurrence: { name: "AGAIN", kind: "notSimulable",
why: "every fourth round the location resets and restores all hit points on BOTH sides; the harness has no "
+ "reset and no way to give the party its hit points back" },
inside_out: { name: "CONTAINED", kind: "notSimulable",
why: "1d6 a round to everyone in the location with no roll and no armour, 12 points of regenerating armour, "
+ "and the win condition is a Spot at −40% to leave — none of which is an attack the harness rolls" },
latecomer: { name: "THE CALL NOT MADE", kind: "outOfCombat",
why: "it answers one truthful question about the next hour and demands command in exchange; it stands down "
+ "if the party makes the call it came for" },
mirrorline: { name: "REFLECTED", kind: "notSimulable",
why: "it borrows the NEAREST agent's skills and half the damage it takes is dealt to that agent — the "
+ "halving lands on an agent, not on itself, so damageFactor 0.5 would be the wrong creature" },
carried_over: { name: "IT NEEDS SOMEWHERE TO BE", kind: "notSimulable",
why: "a hit does no damage and instead sets a rolled location to that agent's worst historical hit points "
+ "with 1 bleed a round; attackFactor 0 would make it harmless when it is the opposite" }
};
+89
View File
@@ -0,0 +1,89 @@
/**
* What each POWER in bestiary-beasts.mjs does in a fight — the beasts, heraldry and
* hybrids share of the classification, merged into POWERS in powers.mjs.
*
* Same three kinds, same rule: read the header of powers.mjs first, and in particular the
* `supporter` entry, which is the nearest relative of everything in this file. Its ARGENT
* AND GULES was wired as a flat half-damage because the harness has no ground, and the
* condition "while it stands on ground the borough holds" therefore applied always — the
* wipe rate moved from 25.1% to 75.2%, a fifty-point error caused entirely by wiring a
* conditional power. R-275.
*
* Nothing in this file is wired, and the reason is worth stating because this band is
* where the temptation is worst. Half of these creatures have a route through their
* armour written into the tactics, and every one of those routes is a condition:
*
* heath_cow half damage from edged, firearms and energy, FULL from bone or blunt
* water_bull armour 8 wet, dropping 1 a round from round three to a floor of 3
* sign_wyvern / gryphon armour halves once a wing or a leg is out
*
* damageFactor is a single unconditional number with no notion of what hit the creature,
* which round it is, or whether the thing is still standing up. Wiring any of the above
* would repeat the supporter's mistake exactly.
*
* Nor are the armour routes powers in the first place. Grounding a flyer halves its hide
* everywhere but the vital, and a wing carries none at all: that is `exposedHideFor` and
* the `bare` flag in anatomy.mjs already doing the work through the location table, and
* simulate.mjs applies it per location in armourAt(). The tactics of the eagle, the
* gryphon and the wyvern only describe it. So the classification below is about what each
* power adds ON TOP of that, and where the answer is "nothing", the entry says so.
*
* The charges and the drops — the azure bull's doubled gore, the tup's extra 1d4 on the
* run-up, the gryphon's 15m stoop for 3d6 — are extra damage DEALT on a named manoeuvre.
* The three effects the harness understands cover the creature's defence stacking, the
* damage it takes, and its own attack rating. None of them is a bonus to the damage it
* deals, and none of them can be gated on a straight line, a blind side or a target's SIZ.
*/
export const POWERS_BEASTS = {
/* ---- notSimulable: fight rules the harness cannot express ---- */
bull_azure: { name: "THE CHARGE", kind: "notSimulable",
why: "a named manoeuvre that doubles the gore only after 10m or more in a straight line, which the tactics itself says can be broken with cover or a scattered team — the harness has no ground to run across and no effect that multiplies damage dealt" },
enfield: { name: "BADLY ASSEMBLED", kind: "notSimulable",
why: "a crippled limb costs it nothing in initiative or attack, and the harness has no key that waives a wound penalty — woundPenaltyFrom is what simulate.mjs charges for a ruined limb, and defenceStacking: \"ignores\" waives the cumulative penalty instead, which is not the penalty this power waives" },
yale: { name: "BOTH WAYS AT ONCE", kind: "notSimulable",
why: "it defends two attackers in the same round at no penalty but the ordinary cumulative penalty returns at three or more, and defenceStacking: \"ignores\" has no attacker count — wiring it would waive the penalty against a whole party, which is the answer the tactics explicitly denies" },
man_tyger: { name: "THE FACE", kind: "notSimulable",
why: "costs each attacker a round to overcome the fact that it has a face, only while it is talking, and not at all from surprise or while it is already engaged — the harness has no talking, no surprise, and no way for an agent to lose a round" },
eagle_displayed: { name: "TWO HEADS", kind: "notSimulable",
why: "it cannot be surprised and no stealth approach works while it is airborne; the harness has neither surprise nor stealth, and the wings carrying none of its armour is the `bare` flag in anatomy.mjs, not this power" },
gryphon: { name: "STOOP AND DROP", kind: "notSimulable",
why: "a manoeuvre gated on the target being SIZ 16 or less, whose damage is a 15m fall for 3d6 ignoring armour and which then removes the agent from the fight — the harness cannot gate on a target's SIZ, has no height to drop from, and has no effect for damage dealt" },
sign_wyvern: { name: "A FOOT OF PAINT", kind: "notSimulable",
why: "−30% to attacks made against it from directly in front or behind and nothing from the side; the harness has no facing, and the only attack effect it has is the creature's own attackFactor, not a penalty on its attackers" },
cockatrice: { name: "THE LOOK", kind: "notSimulable",
why: "a POWx5 on meeting its eyes that cripples a location with no wound, ends when it dies, and is void for anybody working by touch or a mirror — the harness rolls no characteristics and has no eye contact to avoid" },
bearward_bear: { name: "THE DOGS ARE STILL COMING", kind: "notSimulable",
why: "it fights on while it can hear anything it takes for dogs and lies down after three full rounds of total silence; the harness has no sound, and a party in it never stops shouting or stops fighting" },
heath_cow: { name: "MILKED DRY", kind: "notSimulable",
why: "half damage from edged steel, firearms and energy but FULL from bone or a blunt improvised weapon, plus a foreleg-and-hind-leg route on the same side — damageFactor is one number with no notion of what hit it, and wiring 0.5 would make the rib in the church useless and halve the blunt hits too" },
tup: { name: "THE BLIND SIDE", kind: "notSimulable",
why: "a charge manoeuvre against a target who is not looking at it: automatic knockdown, a further 1d4 for the run-up and no defence roll, and nothing at all against anybody facing it — the harness has no facing and no effect for damage dealt" },
hollow_hog: { name: "GONE", kind: "notSimulable",
why: "any round it is not engaged it is out of sight at a −30% Spot and re-enters from a new direction; the harness has no positions to vanish into, never looks, and has no disengagement" },
water_bull: { name: "WET HIDE", kind: "notSimulable",
why: "armour 8 while wet, then 1 less each round from the third out of the water down to a floor of 3, so the whole power is a round count and a place — the harness has no clock on armour and damageFactor cannot decay; its 8 is already on the statblock" },
moor_cat: { name: "ONE PASS", kind: "notSimulable",
why: "its FIRST attack in a scene only, from ambush at +30% and knocking the target prone, after which any real injury sends it away for good — attackFactor is unconditional and permanent, so wiring 1.3 would give it the ambush bonus every round of a fight it does not want" },
gaunt_grey: { name: "STILL HUNGRY", kind: "notSimulable",
why: "ignores the penalties from crippled legs and fights on at any hit point total above zero; neither is one of the three effects — simulate.mjs charges the leg through woundPenaltyFrom and stops a combatant at hp 2 through conditionFor, and no key waives either" },
menagerie_ape: { name: "HANDS", kind: "notSimulable",
why: "opens any mechanical lock in a round, climbs anything, and disarms on a successful brawl attack — the harness has no doors and no inventory to take a weapon out of, and it flees an open fight rather than pressing one" },
/* ---- outOfCombat: not fight rules ---- */
white_hart: { name: "THE CHASE IS THE POINT", kind: "outOfCombat",
why: "it does not fight at all — an hourly POWx5 to break off a pursuit, and everybody still following after three hours is somewhere the case did not include" },
melusine: { name: "THE FONT IS A DOOR", kind: "outOfCombat",
why: "a movement rule between bodies of standing water within sight of her carving, confined by draining the font or breaking the line of sight; on land her own entry says she is a conversation" },
martlet: { name: "IT CANNOT COME DOWN", kind: "outOfCombat",
why: "it does not attack and is not a threat to anybody — a mark on one agent for the rest of the case and no recovery for anybody sleeping in earshot, ended by giving it somewhere to be" },
piety_pelican: { name: "PIETY", kind: "outOfCombat",
why: "she will not fight and cannot be driven off the nest; it is a healing bargain priced in her own hit points and in 1 Coherence permanently for every drinker if she dies of it" },
talbot: { name: "IT DOES NOT LOSE THE LINE", kind: "outOfCombat",
why: "an unbreakable track across hours and a Persuade off-switch out of the kennel book; its own entry says the fight itself is an 8-stone dog with a bite that four armed agents can kill" },
gazehound: { name: "IT SEES YOU FIRST", kind: "outOfCombat",
why: "its own entry says it is a HERALD, not a fight — automatic detection at 400m and whatever owns it knowing where the team is, and killing it is easy and pointless" },
cade_lamb: { name: "IT WANTS TO COME IN", kind: "outOfCombat",
why: "it cannot hurt anybody; the power is getting through a door left ajar, plus a −20% to Listen and Spot, and the harness never looks or listens" },
hinny: { name: "IT KNOWS THE ROUTE", kind: "outOfCombat",
why: "it does not attack — a sixty-year round it will not deviate from, and following it is the scenario rather than a fight" }
};
+70
View File
@@ -0,0 +1,70 @@
/**
* What each POWER in bestiary-folklore.mjs does in a fight — the folklore share of the
* classification, merged into POWERS in powers.mjs.
*
* Same three kinds, same rule: read the header of powers.mjs first, and in particular the
* `supporter` entry. Its power was wired unconditionally because the harness has no ground,
* and that moved its wipe rate from 25.1% to 75.2% — R-275. Nothing in this file is wired.
* Every one of these twenty-five powers either is not about the exchange of blows at all, or
* turns on something the harness does not have: a place (a road, a pass, a cut, a pond, a
* church roof), a characteristic roll, a damage type, a target's SIZ, Coherence, a named
* quarry, or a manoeuvre that takes more than one round to land. The three effects the
* harness understands all describe the creature's own defences, damage taken, or attack
* rating; a penalty aimed at the agent's roll is not one of them, however tempting the
* arithmetic looks.
*/
export const POWERS_FOLKLORE = {
/* ---- notSimulable: fight rules the harness cannot express ---- */
hunt_master: { name: "RIGHT OF WAY", kind: "notSimulable",
why: "only against whatever stands between it and one named quarry, and the ride-down is its own 3d6 with a DEXx5 for half — the harness has no quarry, no ground to stand between, and rolls no characteristics" },
gabriel_hounds: { name: "OVERHEAD", kind: "notSimulable",
why: "fires on the round it passes above a group and costs a failed POWx5 the agent's next action; the harness has no fly-over, no action to lose and rolls no characteristics" },
black_annis: { name: "THE REACH", kind: "notSimulable",
why: "denies the target its Dodge, and only from full concealment in a cut it will not leave in daylight — the harness has no concealment, and none of the three effects touches the agent's defence" },
jenny_greenteeth: { name: "UNDER", kind: "notSimulable",
why: "only within arm's reach of water it cannot leave, and it resolves as a cumulative STRx5 grapple handed off to the ASPHYXIA rules rather than a combat" },
hunky_punk: { name: "MASONRY", kind: "notSimulable",
why: "the factor depends on the damage type — minimum from bullets, none from edged, full from a hammer or a fall — and damageFactor is a single number with no notion of what hit it; its ten points of armour are already on the statblock" },
the_teind: { name: "THE ACCOUNT", kind: "notSimulable",
why: "once per scene, an opposed POWx5 that removes an agent from the scene rather than injuring them, and is voidable by a volunteered substitution or by paying the debt — the harness rolls no characteristics and cannot lose a combatant to a bargain" },
dullahan: { name: "THE CALL", kind: "notSimulable",
why: "a POWx5 compulsion on one named person, and the flail only lands on whoever physically restrains them; the flail is already on the statblock and the harness has no name, no roll and no restraint" },
cu_sidhe: { name: "THREE BARKS", kind: "notSimulable",
why: "lands only on the third bark, after the scent and the close, and costs a failed POWx5 the next action — the harness has no sequence of rounds before a power arms and rolls no characteristics" },
tatterfoal: { name: "THE HEDGE", kind: "notSimulable",
why: "−40% to ranged attacks made against it, and the only attack effect the harness has is its own attackFactor; it also has no ranged fire and no hedge to be behind" },
dun_cow: { name: "THE CHARGE", kind: "notSimulable",
why: "a separate straight-line manoeuvre, triggered only by an attempt to take a sample from it, 4d6 with a DEXx5 to be elsewhere and four rounds to line up again — the harness has no trigger, no line and no reload clock" },
boobrie: { name: "THE LIFT", kind: "notSimulable",
why: "only against a target of SIZ 14 or under, and the damage is a drop a round after the talons close — the harness cannot gate on the target's SIZ and has no two-round manoeuvre" },
/* ---- outOfCombat: not fight rules ---- */
grey_mare: { name: "MOUNT UP", kind: "outOfCombat",
why: "it does not attack; touching its back is a POWx5 and a one-way ride to the parish boundary, and nobody has ever been killed by it" },
cailleach: { name: "THE PASS IS SHUT", kind: "outOfCombat",
why: "she cannot fight and will not try — a refusal, then an exposure clock of 1 point per ten minutes on the high ground, and a dead compass" },
gwrach: { name: "THE NAMING", kind: "outOfCombat",
why: "it does not fight and shooting it achieves nothing; the naming costs 1 Coherence and runs for twenty-four hours after the scene it appears in" },
sin_eater: { name: "SIXPENCE", kind: "outOfCombat",
why: "a bargain offered at a cordon and taken only by consent — 2 Coherence for a trained skill, and he will not take by force" },
lych_waiter: { name: "THE WATCH", kind: "outOfCombat",
why: "a STRx5 at a gate to carry grave goods out, without violence; the disposal is burying somebody new in the yard" },
crossroads_man: { name: "NO PARISH", kind: "outOfCombat",
why: "it defeats cordons, warrants, Borrowed Authority and boundaries — containment, not the exchange of blows; its fighting is the 1d6+1 already on the statblock" },
march_stone: { name: "ALWAYS WAS", kind: "outOfCombat",
why: "it rewrites every map and register within a mile and costs a reader 1 Coherence; containment is a two-officer watch, and it cannot be broken by anything carried" },
foundation_child: { name: "THE PIER", kind: "outOfCombat",
why: "it cannot hurt anybody — it holds a viaduct up, and removing it is an engineering problem on a multi-year clock" },
bog_offering: { name: "STANDING WATER", kind: "outOfCombat",
why: "it works on the building, flooding rooms an inch a day and ruining archives; it has never injured anybody" },
hearth_hob: { name: "THE DISH", kind: "outOfCombat",
why: "a payment taboo — thank it and it leaves, and the building's machinery fails over the following month" },
cauld_lad: { name: "COLD WORK", kind: "outOfCombat",
why: "it has never touched anybody; the −20% is on fine manual work and it doubles Repair and First Aid times" },
changeling: { name: "THE SUBSTITUTION", kind: "outOfCombat",
why: "it cannot hurt an adult and does not try; the power is one piece of information about the real child, and acting on it is a crossing" },
uninvited: { name: "THE GIFT", kind: "outOfCombat",
why: "it never attacks; the gift is permanent, rolled on a 1d6 table, and it is declined by inviting it in advance" },
pooka: { name: "THE RIDE", kind: "outOfCombat",
why: "it is not trying to kill anybody — an hour's journey ending in one Major Wound check and an agent who is inconvenienced rather than hurt" }
};
+98
View File
@@ -0,0 +1,98 @@
/**
* WATER, COAST AND THE DROWNED — what each of those POWERs does in a fight.
*
* The water band's share of the classification described in the header of tools/powers.mjs,
* merged into POWERS there. Same three kinds, same rule: `wired` only where the power maps
* exactly onto one of the three effects the simulator understands AND applies in any fight
* without condition.
*
* Nothing in this band is wired, and the reason is the whole character of the file. Water
* creatures kill by holding a subject under, and their powers hand off to the ASPHYXIA
* rules — which are conditional on being in or near water, and the harness has no water,
* exactly as it has no ground. R-275 is what wiring a conditional power looks like: the
* supporter's half damage was applied unconditionally and moved its wipe rate 25.1% to
* 75.2%. A damageFactor standing in for drowning would be the same mistake with a wetter
* name. Where a power here reads like one of the three effects — the tangie's half damage,
* the heron's double damage, the eel mother's +20% — a condition sits in front of it that
* the harness cannot evaluate, so it is notSimulable and the condition is named.
*/
export const POWERS_WATER = {
/* ---- notSimulable: fight rules the harness cannot express ---- */
sea_mare: { name: "THE TIDE IT CAME IN ON", kind: "notSimulable",
why: "half damage and 2 hit points a round back only below the high-water mark, and the harness "
+ "has no tide line — the same place-conditional shape as the supporter's ground, R-275" },
tangie: { name: "WEED", kind: "notSimulable",
why: "half damage to edged and piercing weapons only, full to blunt, and gone after a round of "
+ "open flame — conditional on the attacker's weapon and on the wrack not being burnt off" },
wrack_gull: { name: "BY ACCLAMATION", kind: "notSimulable",
why: "−30% to the subject's own Dodge and Athletics while the flock is on them; the harness has "
+ "no effect that penalises a target's skills, only the creature's own three" },
shellycoat: { name: "SOMETHING OF YOURS", kind: "notSimulable",
why: "takes one carried object a round, GM's pick of the useful one, and the harness has no "
+ "inventory to take from" },
peg_powler: { name: "UNDER", kind: "notSimulable",
why: "a STR 23 grapple from the pool into the ASPHYXIA rules, and only against someone within "
+ "two metres of the water's edge — the harness has no water and no edge to stand back from" },
greenteeth: { name: "IT LOOKS LIKE GROUND", kind: "notSimulable",
why: "holds whoever steps onto the weed and runs the ASPHYXIA rules from the second round under; "
+ "no damage exchange at all and the harness has no surface to step through" },
fen_hound: { name: "OFF THE PATH", kind: "notSimulable",
why: "only fires on a subject who runs — a walk needs no roll — and its effect is chest-deep "
+ "water and a dropped item, none of which the harness has" },
wrackman: { name: "FLOOD LINE", kind: "notSimulable",
why: "heals 3 hit points a round while standing in standing water, and fire is exempt; regeneration "
+ "is not one of the three effects and the condition is a place the harness cannot see" },
hulk_haunt: { name: "WATERTIGHT", kind: "notSimulable",
why: "shuts one door, hatch or scuttle a round against agents inside the hull, and the harness has "
+ "no interior, no doors and no wedges" },
drowned_crew: { name: "ALL HANDS", kind: "notSimulable",
why: "a POWx5 to not join the watch, at −10% per body still upright; the harness rolls no "
+ "characteristics and has no vessel to be aboard" },
lock_keeper: { name: "THE CHAMBER", kind: "notSimulable",
why: "only reaches someone inside the lock, and resolves as hull damage or an Athletics roll into "
+ "the ASPHYXIA rules — conditional on a place, and he will not touch anyone on the bank" },
the_undertow: { name: "SET", kind: "notSimulable",
why: "moves and holds under anybody in the water and runs the ASPHYXIA rules; armour and weapons "
+ "do nothing to it, and it does not exist at slack water" },
the_fathom: { name: "SOUNDING", kind: "notSimulable",
why: "once a scene, 1d4 with no roll and no armour to everyone in the water above it, reduced to "
+ "1 by ear defenders — conditional on being in the water and on what the party was issued" },
eelmother: { name: "THE ROLL", kind: "notSimulable",
why: "a manoeuvre that only follows a bite which held, ending in the ASPHYXIA rules, and its +20% "
+ "needs blood in the water first — conditional on a prior hit and on the water itself" },
lamprey_host: { name: "IT COMES APART", kind: "notSimulable",
why: "a single hit of 8 or more splits it into two hosts sharing its hit points; the harness cannot "
+ "add a combatant mid-fight or divide one" },
washland_cattle: { name: "DRIVEN", kind: "notSimulable",
why: "the whole herd going at once for 2d6 and a knockdown, with no roll to avoid in water above "
+ "the knee and the ASPHYXIA rules under eleven animals; the harness has one creature and no water" },
the_heron: { name: "THE STRIKE", kind: "notSimulable",
why: "+30% and double damage against a target that did not move on its previous turn, −20% against "
+ "one that did, and the harness has no movement — so which branch applies is undecidable" },
/* ---- outOfCombat: not fight rules ---- */
harbour_dog: { name: "HEEL", kind: "outOfCombat",
why: "a POWx5 at the end of every scene in sight of the harbour that walks the subject to the quay; "
+ "it is friendly until somebody is between it and the water" },
selkie: { name: "THE SKIN IS THE CONTRACT", kind: "outOfCombat",
why: "its own entry says a Borrowed Authority and bargain problem, not a combat, and the price is "
+ "Coherence" },
sea_crow: { name: "THE COUNT", kind: "outOfCombat",
why: "it is harmless and one bird; the power is a list of the drowned that costs Coherence to hear, "
+ "ended by salvage and burial" },
cut_walker: { name: "PASSING PLACE", kind: "outOfCombat",
why: "it does not chase, hurry or turn round — the power is giving ground on a towpath, and only "
+ "someone still in its path is affected" },
net_mender: { name: "UNPICKED", kind: "outOfCombat",
why: "unmakes the record and the memory of a named person's return; she dies like a woman of sixty "
+ "and her price is another name" },
mudlark_child: { name: "COME AND SEE", kind: "outOfCombat",
why: "it never touches anybody — a real clue placed out on the foreshore, soft mud and an incoming "
+ "tide, cancelled by a line thrown from hard ground" },
the_sump: { name: "DRAWDOWN", kind: "outOfCombat",
why: "its own entry says nothing here is a combat unless the agents dive; the power is a dusk POWx5 "
+ "gated on reservoir capacity, and the case is a conversation with the water company" },
drowned_parish: { name: "THE SERVICE IS AT SIX", kind: "outOfCombat",
why: "a clock, not a fight — they will not strike first and cannot be dispersed by force; a formal "
+ "deconsecration read aloud ends it" }
};
+36 -1
View File
@@ -26,6 +26,25 @@
* damageFactor: n damage it takes is multiplied by n, floored
* attackFactor: n its attack rating is multiplied by n, floored
*/
import { POWERS_FOLKLORE } from "./powers-folklore.mjs";
import { POWERS_WATER } from "./powers-water.mjs";
import { POWERS_ANOMALOUS } from "./powers-anomalous.mjs";
import { POWERS_BEASTS } from "./powers-beasts.mjs";
/* The bestiary's own classifications, one file per band, merged here.
*
* Kept in separate files for the same reason the creatures are: four of them were
* written at once, and a single shared object literal is the one thing concurrent
* authors cannot both edit. Spread last so this file's hand-written entries always win
* a key collision — the originals were argued over and measured, and a band file must
* not silently take one of them over.
*
* Not one of the 99 was wired. That is the correct outcome rather than a shortfall:
* every effect the harness offers describes the CREATURE — its own attack rating, the
* damage it takes, whether it feels the defence-stacking penalty — and essentially every
* power in the bestiary is conditional on a place, a posture, a round count, a weapon
* type, an attacker count, a named agent, or Coherence. R-275 is what wiring one of
* those anyway costs: 25.1% to 75.2% on a single creature. */
export const POWERS = {
/* ---- wired: the harness applies these ---- */
redcap: { name: "NOT TIRED", kind: "wired", effect: { defenceStacking: "ignores" },
@@ -82,7 +101,23 @@ export const POWERS = {
the_census: { name: "ENUMERATED", kind: "outOfCombat", why: "its own entry says the threat is not a combat one" },
the_yield: { name: "THE COUNT", kind: "outOfCombat" },
the_overwriter: { name: "THE CORRECTION", kind: "outOfCombat" },
the_arrears: { name: "THE PRINCIPAL", kind: "outOfCombat" }
the_arrears: { name: "THE PRINCIPAL", kind: "outOfCombat" },
/* The starter's two. Unclassified until check-powers began reading the shared spec
list rather than NPCS — they are scenario cast, so the guard had never seen them.
Both happen during a fight and neither can be expressed: the harness has no
documents to present and no doors to seal. */
bp_inspector: { name: "NO DESTINATION SHOWN", kind: "notSimulable",
why: "it will not strike anyone formally offering a document — a ticket, a warrant card held flat — "
+ "and the harness has nothing to offer and no notion of a round spent offering it" },
bp_conductor: { name: "ALL PASSENGERS REMAIN ABOARD", kind: "notSimulable",
why: "once per fight it seals every door in a carriage, and the harness has no doors, no carriage "
+ "and nowhere to be shut out of; opening one costs an action it also cannot spend" },
...POWERS_FOLKLORE,
...POWERS_WATER,
...POWERS_ANOMALOUS,
...POWERS_BEASTS
};
export const EFFECT_KINDS = ["defenceStacking", "damageFactor", "attackFactor"];