Files
slaguru666andClaude Opus 5 c61700566c Shields: armour you raise, and the only armour you can defend with
A shield is the one piece of armour you hold, and it protects you exactly as much as
you are currently holding it up. Raised, its points apply to what it covers and it can
parry on a new Shield skill. Lowered, it is weight. The price of keeping it up is that
it occupies a hand and you are shooting around it: -20% to your own ranged attacks,
printed on the card like everything else. Only one shield goes up at a time.

Seven of them, 2-point buckler to 6-point ballistic shield; the pavise and barrier
plate cover legs too because they are walls you carry.

`degradation` finally does something — it has been on every armour item since the packs
were first built and was read by nothing. A blow costs the shield a point of wear for
every step it stood above an ordinary success, so an ordinary hit costs nothing, a
special 1 and a critical 2. At its limit it comes apart, drops, and will not be raised
again.

Two defects found by driving the real functions rather than reading the diff, both
introduced by this change:

- Every shield defence quietly rolled DODGE. "shield" passed through
  defenceTypeAllowed unchanged, so neither parry branch matched and it fell through.
  Invisible in the card; it showed up as eight rolls at a supposed 85% returning six
  failures. Taking a blow on the shield IS a parry, so it is now a flag on a parry
  rather than a defence type of its own.
- The wear rule was unreachable: it fired only on a clean stop, but under graded
  defences only a critical fully stops a critical, so a shield would never have taken
  a scratch. Wear is for what the shield ABSORBED — dragging a critical down to a
  special is exactly when shields splinter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 18:25:30 +01:00

270 lines
16 KiB
JavaScript

/**
* The Custodians — icon set, v3.
*
* Flat, bold, saturated. Not photorealistic and not trying to be. What changed in
* v3 is DEPTH: a glyph is no longer a single silhouette but up to three layers —
*
* base the mass of the object, in near-white ink
* detail the parts that tell you WHICH object it is, in a darker tone:
* the grip and sight on a pistol, the fuller on a blade, the lens
* housing on a scanner. This is the whole point of the revision.
* mark a small accent in the rim colour — a muzzle flash, a lit indicator
*
* and ERA, because the agency crosses to other times and comes back with the
* shopping. Era is a corner tab plus a treatment of the tile, so a glance at an
* inventory tells you what century a thing is from before you read its name:
*
* antique notched corner, muted ground, no glow — flint, brass, oil
* modern plain tile — agency issue
* future cut corner and a hairline scan across it — recovered, elsewhere
* anomalous a broken rim: the tile does not close — should not exist
*
* Readable at 32px in a Foundry list, still clean at 256px on a sheet.
*/
import { writeFile, mkdir } from "node:fs/promises";
import path from "node:path";
const OUT = path.resolve("icons");
const S = 128;
/* Category hue families. Deliberately far apart so a mixed inventory reads fast. */
const FAMILY = {
weapon: { h: 8 }, melee: { h: 24 }, armour: { h: 205 },
gear: { h: 150 }, device: { h: 275 }, skill: { h: 45 },
talent: { h: 185 }, actor: { h: 330 }, threat: { h: 355 },
case: { h: 220 }, power: { h: 288 }
};
const hsl = (h, s, l) => `hsl(${h} ${s}% ${l}%)`;
/**
* Glyphs. A string is a plain silhouette (v2 behaviour, kept so nothing regresses).
* An object may carry { base, detail, mark, stroke } — `stroke` lists which of
* those layers are drawn as lines rather than filled.
*/
const G = {
/* ---------------- firearms: era told by the furniture ---------------- */
pistol: { base: `M22 62h52l10-10h16v14l-12 6v10H60l-6 12H34l6-12H22z`,
detail: `M30 70h18v8H30z M60 56h14v6H60z`, mark: `M112 60h10v8h-10z` },
revolver: { base: `M20 60h48l8-8h18v12l-10 6v10H56l-8 14H26l8-14H20z`,
detail: `M44 56a10 10 0 1 1 0 20 10 10 0 0 1 0-20z M28 70h14v8H28z`,
mark: `M110 58h10v10h-10z` },
flintlock: { base: `M18 64h44l40-16v10L70 74v8H56l-10 16H22l10-16H18z`,
detail: `M56 54l10-8 6 8-8 8z M26 72h16v8H26z` },
rifle: { base: `M12 60h84l20-8v12l-14 6v8H60l-8 14H32l8-14H12z`,
detail: `M46 66h30v6H46z M84 52h10v8H84z`, mark: `M116 58h6v10h-6z` },
musket: { base: `M10 66h96l14-6v8l-14 6v6H62l-10 14H24l10-14H10z`,
detail: `M40 70h26v6H40z M66 56l8-8 6 6-8 8z` },
smg: { base: `M18 54h60v14H18z M30 68h16v30H30z M78 56h30v10H78z`,
detail: `M52 68h12v22H52z M22 58h8v6h-8z`, mark: `M108 56h10v10h-10z` },
shotgun: { base: `M12 46h58v10H12z M12 60h58v10H12z M70 44l30 16-30 16z`,
detail: `M100 52h16v16h-16z M20 48h14v4H20z` },
gauss: { base: `M14 58h72v14H14z M86 50h26v30H86z`,
detail: `M24 62h48v6H24z M92 58h14v14H92z`,
mark: `M112 54h10v22h-10z`, stroke: ["mark"] },
/* ---------------- blades and hafted ---------------- */
blade: { base: `M78 12l12 12-44 44-12-12z M40 60l12 12-8 12-16-4z M34 84l10 10-16 16-10-10z`,
detail: `M74 22l6 6-38 38-6-6z` },
sword: { base: `M64 8l10 10v58H54V18z M40 76h48v10H40z M58 86h12v34H58z`,
detail: `M62 20h4v52h-4z M60 108h8v8h-8z` },
axe: { base: `M52 14h14v106H52z M66 20c22 0 34 14 34 26s-12 24-34 24z`,
detail: `M72 30c12 2 18 10 18 16s-6 14-18 16z` },
spear: { base: `M62 8l10 26-10 12-10-12z M58 46h12v74H58z`,
detail: `M60 20h8v12h-8z M56 92h16v6H56z` },
club: { base: `M44 100l40-72 14 8-40 72z M84 18a12 12 0 1 1 18 10 12 12 0 0 1-18-10z`,
detail: `M52 96l32-58` , stroke: ["detail"] },
bar: { base: `M52 12h24v92H52z M44 104h40v14H44z`,
detail: `M58 24h12v70H58z M52 12l-8-6h40l-8 6z` },
claw: { base: `M30 18c14 22 14 62 6 90M56 12c10 26 10 68 2 96M82 18c8 24 6 64-2 88`,
stroke: ["base"] },
bow: { base: `M40 16c30 14 30 82 0 96M36 12v104`, stroke: ["base"],
detail: `M40 16l52 48-52 48`, },
crossbow: { base: `M14 58h100v12H14z M52 40h12v46H52z`,
detail: `M20 46c18 8 18 28 0 36M108 46c-18 8-18 28 0 36`, stroke: ["detail"] },
/* ---------------- crossing tech ---------------- */
beam: { base: `M18 58h48v12H18z M66 46l30 18-30 18z`,
detail: `M26 62h30v4H26z`, mark: `M96 56h16v16H96z` },
bolt: { base: `M70 8L28 72h28l-10 48 46-68H62z`, detail: `M64 26L44 62h16z` },
lattice: { base: `M64 10l46 26v56l-46 26-46-26V36z`,
detail: `M64 34l24 14v28l-24 14-24-14V48z`, mark: `M64 58a8 8 0 1 1 0 16 8 8 0 0 1 0-16z` },
aperture: { base: `M64 14a50 50 0 1 1 0 100 50 50 0 0 1 0-100z`,
detail: `M64 38a26 26 0 1 1 0 52 26 26 0 0 1 0-52z`,
mark: `M64 54a10 10 0 1 1 0 20 10 10 0 0 1 0-20z` },
/* ---------------- armour ---------------- */
vest: { base: `M40 20h48l10 18v56a10 10 0 0 1-10 10H40a10 10 0 0 1-10-10V38z`,
detail: `M64 20v84 M38 46h20M74 46h20` , stroke: ["detail"] },
// A shield read from the front: a heater outline with a boss and a grip line.
shield: { base: `M64 14l38 12v34c0 30-16 48-38 58-22-10-38-28-38-58V26z`,
detail: `M64 14v104 M30 52h68`,
accent: `M64 56m-9 0a9 9 0 1 0 18 0a9 9 0 1 0-18 0`,
stroke: ["detail"] },
plate: { base: `M64 12l40 14v40c0 28-18 44-40 50-22-6-40-22-40-50V26z`,
detail: `M64 26l26 10v30c0 18-12 28-26 32-14-4-26-14-26-32V36z` },
jacket: { base: `M42 22h44l12 16v62H30V38z`, detail: `M64 22v78 M30 54h68`, stroke: ["detail"] },
suit: { base: `M64 8a16 16 0 1 1 0 32 16 16 0 0 1 0-32z M40 44h48l10 20v56H30V64z`,
detail: `M52 20h24v10H52z M44 72h40v12H44z` },
mail: { base: `M40 20h48l10 18v56a10 10 0 0 1-10 10H40a10 10 0 0 1-10-10V38z`,
detail: `M40 40h12v12H40zM60 40h12v12H60zM80 40h10v12H80zM50 56h12v12H50zM70 56h12v12H70zM40 72h12v12H40zM60 72h12v12H60zM80 72h10v12H80z` },
/* ---------------- gear, all eras ---------------- */
kit: { base: `M26 40h76v66H26z M52 40V26h24v14`, detail: `M64 56v34 M47 73h34`, stroke: ["detail"] },
pack: { base: `M34 38h60v70H34z M50 38V22h28v16`, detail: `M34 66h60 M50 78h28`, stroke: ["detail"] },
cell: { base: `M42 22h44v18h10v66H32V40h10z`, detail: `M52 58h24v30H52z`, mark: `M56 64h16v6H56z` },
key: { base: `M34 46a20 20 0 1 1 0 36 20 20 0 0 1 0-36z M54 60h56v10H88v14H78V70h-8v18H60V70z`,
detail: `M34 58a8 8 0 1 1 0 12 8 8 0 0 1 0-12z` },
disc: { base: `M64 20a44 44 0 1 1 0 88 44 44 0 0 1 0-88z`, detail: `M64 50a14 14 0 1 1 0 28 14 14 0 0 1 0-28z` },
scanner: { base: `M24 34h80v52H24z M38 96h52v10H38z`, detail: `M34 44h60v34H34z`,
mark: `M40 50h48M40 62h34M40 72h44`, stroke: ["mark"] },
lens: { base: `M56 20a34 34 0 1 1 0 68 34 34 0 0 1 0-68z M80 82l28 28`,
detail: `M56 34a20 20 0 1 1 0 40 20 20 0 0 1 0-40z` },
cog: { base: `M64 34a30 30 0 1 1 0 60 30 30 0 0 1 0-60z M58 8h12v18H58z M58 102h12v18H58z M8 58h18v12H8z M102 58h18v12h-18z`,
detail: `M64 52a12 12 0 1 1 0 24 12 12 0 0 1 0-24z` },
regulator: { base: `M20 40h88v48H20z`, detail: `M34 54h20v20H34z M74 54h20v20H74z`,
mark: `M54 64h20v4H54z M44 88v14M84 88v14`, stroke: ["mark"] },
radio: { base: `M28 52h72v56H28z M44 52V26h8v26`, detail: `M44 66a10 10 0 1 1 0 20 10 10 0 0 1 0-20z`,
mark: `M64 66h20M64 80h20`, stroke: ["mark"] },
flask: { base: `M52 14h24v34l24 50a12 12 0 0 1-10 18H38a12 12 0 0 1-10-18l24-50z`,
detail: `M40 78h48 M50 20h28`, stroke: ["detail"] },
lantern: { base: `M44 30h40v10H44z M38 40h52v58H38z M56 98h16v10H56z`,
detail: `M50 50h28v40H50z`, mark: `M58 60h12v20H58z` },
candle: { base: `M54 46h20v58H54z M44 104h40v10H44z`, detail: `M60 30c8 6 8 14 4 16-6-2-8-10-4-16z` },
compass: { base: `M64 16a48 48 0 1 1 0 96 48 48 0 0 1 0-96z`,
detail: `M64 34l12 30-12 30-12-30z`, mark: `M64 56a8 8 0 1 1 0 16 8 8 0 0 1 0-16z` },
spectacles: { base: `M20 56a20 20 0 1 1 0 20 20 20 0 0 1 0-20z M88 56a20 20 0 1 1 0 20 20 20 0 0 1 0-20z`,
detail: `M40 64h48`, stroke: ["detail"] },
camera: { base: `M18 44h92v56H18z M44 44l8-12h24l8 12`, detail: `M64 56a18 18 0 1 1 0 36 18 18 0 0 1 0-36z`,
mark: `M96 52h10v8H96z` },
binocs: { base: `M22 40h34v66H22z M72 40h34v66H72z`, detail: `M56 58h16v12H56z`,
mark: `M28 50h22v10H28z M78 50h22v10H78z` },
rope: { base: `M32 20c40 0 24 44 64 44M32 64c40 0 24 44 64 44`, stroke: ["base"] },
book: { base: `M24 20h38a14 14 0 0 1 14 14v74a14 14 0 0 0-14-12H24z M104 20H66a14 14 0 0 0-14 14v74a14 14 0 0 1 14-12h38z`,
detail: `M32 40h30M32 56h30M66 40h30M66 56h30`, stroke: ["detail"] },
folder: { base: `M14 30h40l10 14h50v66H14z`, detail: `M14 62h100 M28 78h60`, stroke: ["detail"] },
vial: { base: `M50 12h28v12l-8 6v72a10 10 0 0 1-12 0V30l-8-6z`, detail: `M58 70h12v24H58z` },
syringe: { base: `M18 92l40-40 18 18-40 40z M62 44l22-22 18 18-22 22z`,
detail: `M96 16l16 16 M30 96l-12 12`, stroke: ["detail"] },
mask_gas: { base: `M64 16c26 0 40 14 40 34 0 22-18 34-24 44-4 8-28 8-32 0-6-10-24-22-24-44 0-20 14-34 40-34z`,
detail: `M44 46a12 12 0 1 1 0 20 12 12 0 0 1 0-20z M84 46a12 12 0 1 1 0 20 12 12 0 0 1 0-20z`,
mark: `M54 84h20v14H54z` },
geiger: { base: `M26 46h58v50H26z M84 58h20v26H84z`, detail: `M36 56h38v14H36z`,
mark: `M40 80h30`, stroke: ["mark"] },
tape: { base: `M64 22a42 42 0 1 1 0 84 42 42 0 0 1 0-84z`, detail: `M64 50a14 14 0 1 1 0 28 14 14 0 0 1 0-28z`,
mark: `M22 64h84`, stroke: ["mark"] },
pocketwatch: { base: `M64 26a42 42 0 1 1 0 84 42 42 0 0 1 0-84z M56 8h16v18H56z`,
detail: `M64 44v24l16 10`, stroke: ["detail"] },
handaxe_tool: { base: `M40 92l40-56 12 8-40 56z`, detail: `M76 28l18-8 6 16-18 8z` },
chip: { base: `M34 34h60v60H34z`, detail: `M48 48h32v32H48z`,
mark: `M34 46H20M34 64H20M34 82H20M94 46h14M94 64h14M94 82h14`, stroke: ["mark"] },
drone: { base: `M52 52h24v24H52z`, detail: `M20 28a14 14 0 1 1 0 4zM94 28a14 14 0 1 1 0 4zM20 96a14 14 0 1 1 0 4zM94 96a14 14 0 1 1 0 4z`,
mark: `M34 40l18 12M94 40L76 52M34 88l18-12M94 88L76 76`, stroke: ["mark"] },
visor: { base: `M18 48h92v26a16 16 0 0 1-16 16H34a16 16 0 0 1-16-16z`,
detail: `M28 56h72v14H28z`, mark: `M36 60h20v6H36z` },
cannister: { base: `M44 20h40v88H44z`, detail: `M52 34h24v18H52z`, mark: `M44 62h40M44 78h40`, stroke: ["mark"] },
net: { base: `M20 20h88v88H20z`, detail: `M20 48h88M20 76h88M48 20v88M76 20v88`, stroke: ["detail"] },
manacles: { base: `M36 44a22 22 0 1 1 0 40 22 22 0 0 1 0-40z M92 44a22 22 0 1 1 0 40 22 22 0 0 1 0-40z`,
detail: `M58 62h12`, stroke: ["detail"] },
/* ---------------- powers: the Quiet Arts ---------------- */
thread: { base: `M20 100c30-70 58 40 88-30`, stroke: ["base"],
detail: `M20 100a6 6 0 1 1 0 2zM108 70a6 6 0 1 1 0 2z` },
echo: { base: `M64 64a10 10 0 1 1 0 2z`, detail: `M64 40a24 24 0 1 1 0 48 24 24 0 0 1 0-48zM64 22a42 42 0 1 1 0 84 42 42 0 0 1 0-84z`,
stroke: ["detail"] },
hollow: { base: `M64 12l48 28v48l-48 28-48-28V40z`, detail: `M64 40l22 13v26L64 92 42 79V53z` },
witness: { base: `M8 64c26-32 82-32 112 0-30 32-86 32-112 0z`, detail: `M64 44a20 20 0 1 1 0 40 20 20 0 0 1 0-40z`,
mark: `M64 56a8 8 0 1 1 0 16 8 8 0 0 1 0-16z` },
unmake: { base: `M28 28l72 72M100 28l-72 72`, stroke: ["base"],
detail: `M64 44a20 20 0 1 1 0 40 20 20 0 0 1 0-40z` },
ledger: { base: `M28 16h72v96H28z`, detail: `M40 36h48M40 52h48M40 68h30`, stroke: ["detail"],
mark: `M76 76l10 10 18-22`, },
/* ---------------- the rest, carried over ---------------- */
eye: { base: `M8 64c26-32 82-32 112 0-30 32-86 32-112 0z`, detail: `M64 44a20 20 0 1 1 0 40 20 20 0 0 1 0-40z` },
ear: { base: `M46 20c30-8 46 12 44 34-2 24-22 24-24 40s-16 20-26 12c-12-10-6-24 2-30 10-8-16-46 4-56z`, stroke: ["base"] },
brain: { base: `M40 28c-14 6-18 22-10 32-8 12-2 28 12 30 4 12 20 16 28 8 10 8 24 2 26-10 12-8 10-26-2-32 2-14-12-26-26-22-6-8-18-10-28-6z`,
detail: `M64 24v78`, stroke: ["base", "detail"] },
hand: { base: `M40 60V28a8 8 0 0 1 16 0v26 M56 54V20a8 8 0 0 1 16 0v34 M72 56V28a8 8 0 0 1 16 0v34c0 30-12 46-32 46s-32-14-32-34v-8a8 8 0 0 1 16 0`, stroke: ["base"] },
boot: { base: `M36 16h26v50l34 20v26H36z`, detail: `M36 76h26`, stroke: ["detail"] },
mask: { base: `M22 34h84v34c0 26-20 46-42 52-22-6-42-26-42-52z`, detail: `M44 56h14M70 56h14`, stroke: ["detail"] },
ring: { base: `M64 16a48 48 0 1 1 0 96 48 48 0 0 1 0-96z`, detail: `M64 42a22 22 0 1 1 0 44 22 22 0 0 1 0-44z` },
triune: { base: `M34 22a14 14 0 1 1 0 28 14 14 0 0 1 0-28z M94 22a14 14 0 1 1 0 28 14 14 0 0 1 0-28z M30 68h68v18H30z`,
detail: `M34 50l14 18h32l14-18 M38 86v30M64 86v30M90 86v30`, stroke: ["detail"] },
figure: { base: `M64 14a18 18 0 1 1 0 36 18 18 0 0 1 0-36z M40 56h48v40H40z`,
detail: `M46 96l-6 24M82 96l6 24`, stroke: ["detail"] },
target: { base: `M64 12a52 52 0 1 1 0 104 52 52 0 0 1 0-104z`,
detail: `M64 40a24 24 0 1 1 0 48 24 24 0 0 1 0-48z`, mark: `M64 58a6 6 0 1 1 0 12 6 6 0 0 1 0-12z` },
wave: { base: `M10 64c14-26 28-26 42 0s28 26 42 0 22-14 24-6`, stroke: ["base"] },
door: { base: `M30 16h68v96H30z`, detail: `M84 60a5 5 0 1 1 0 10 5 5 0 0 1 0-10z` }
};
const ERAS = { antique: 1, modern: 1, future: 1, anomalous: 1 };
function layers(glyph, ink, mid, rim) {
const g = typeof G[glyph] === "string" ? { base: G[glyph] } : (G[glyph] ?? G.disc);
const stroke = new Set(g.stroke ?? []);
const draw = (d, fill, isStroke, w = 9) => !d ? "" : isStroke
? `<path d="${d}" fill="none" stroke="${fill}" stroke-width="${w}" stroke-linecap="round" stroke-linejoin="round"/>`
: `<path d="${d}" fill="${fill}" stroke="none"/>`;
return [
draw(g.base, ink, stroke.has("base")),
draw(g.detail, mid, stroke.has("detail"), 6),
draw(g.mark, rim, stroke.has("mark"), 6)
].join("\n");
}
function svg(glyph, hue, { dark = false, era = "modern" } = {}) {
const muted = era === "antique";
const bg1 = hsl(hue, muted ? 38 : 62, dark ? 22 : (muted ? 30 : 34));
const bg2 = hsl(hue, muted ? 44 : 68, dark ? 14 : 22);
const ink = hsl(hue, muted ? 55 : 90, muted ? 82 : 88);
const mid = hsl(hue, muted ? 40 : 70, muted ? 52 : 46);
const rim = hsl(hue, 80, 64);
// era tab: a corner treatment, so the century reads before the name does
let corner = "", scan = "", rimPath =
`<rect x="5" y="5" width="${S - 10}" height="${S - 10}" rx="16" fill="none" stroke="${rim}" stroke-width="4"/>`;
if (era === "antique") corner = `<path d="M0 0h30L0 30z" fill="${bg2}"/>`;
if (era === "future") {
corner = `<path d="M${S - 32} 0H${S}v32z" fill="${rim}" opacity="0.55"/>`;
scan = `<path d="M12 ${S - 26}h${S - 24}" stroke="${rim}" stroke-width="2" opacity="0.7"/>`;
}
if (era === "anomalous") {
rimPath = `<rect x="5" y="5" width="${S - 10}" height="${S - 10}" rx="16" fill="none"
stroke="${rim}" stroke-width="4" stroke-dasharray="26 13" stroke-dashoffset="7"/>`;
}
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${S} ${S}" width="${S}" height="${S}">
<rect width="${S}" height="${S}" rx="20" fill="${bg1}"/>
${corner}
${rimPath}
${layers(glyph, ink, mid, rim)}
${scan}
</svg>`;
}
export async function writeIcon(name, glyph, family, opts) {
const fam = FAMILY[family] ?? FAMILY.gear;
await mkdir(OUT, { recursive: true });
const hue = (fam.h + (opts?.shift ?? 0) + 360) % 360;
await writeFile(path.join(OUT, `${name}.svg`), svg(glyph, hue, opts), "utf8");
return `systems/ringbrp/icons/${name}.svg`;
}
export { FAMILY, G, ERAS };
/* ---------------- main ---------------- */
import { ICON_MAP } from "./icons.mjs";
if (import.meta.url === `file://${process.argv[1]}`) {
let n = 0, byEra = {};
for (const [name, spec] of Object.entries(ICON_MAP)) {
const [glyph, family, shift, era = "modern"] = spec;
await writeIcon(name, glyph, family, { shift, era });
byEra[era] = (byEra[era] ?? 0) + 1;
n++;
}
console.log(`Custodians | ${n} icons written to icons/`);
console.log(" by era: " + Object.entries(byEra).map(([k, v]) => `${k} ${v}`).join(" · "));
}