Files
RingBRP/tools/make-dice-textures.mjs
T
slaguru666andClaude Opus 5 d0144f744e The Custodians v0.6.1 — full system
A Foundry VTT game system: Basic Roleplaying d100 in a drab British agency that
deals with crossings, where reality keeps a record of what belongs where and can be
made to get it wrong.

Contents:
- rules.mjs as the single authority for every rule formula, with 40 rules and 132
  spot-checked values. Nothing may restate a formula it defines.
- 25 postings, each with a key characteristic, trained skills, talents and its own
  kit; a 175-item catalogue across four eras; 231 generated icons.
- Reaction Lanes initiative, re-rolled every round including the first.
- Coherence, and the Quiet Arts, which only ever arrive from a crossing gone wrong.
- Graded defences: only a critical stops a critical.
- Borrowed Authority — access as a record rather than a hacking roll.
- A case generator, and a rules journal generated from rules.mjs so the book cannot
  drift from the code.

Built compendia are committed so a remote Foundry needs only the files.
node_modules is not; `npm install` restores it for rebuilding content.

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

177 lines
6.5 KiB
JavaScript

/**
* Dice textures for The Custodians, generated rather than drawn.
*
* Written as raw RGBA and encoded to PNG with node:zlib, so the repo gains no
* image dependency and the textures can be regenerated deterministically. Every
* pattern is tileable — Dice So Nice wraps these across a die face, and a seam
* shows up immediately on a d20.
*
* The set is the setting: the Custodians keep a filing system, and the game is
* about that filing system being wrong. So the dice carry a ruled grid, and the
* further an agent drifts from their own reality the more the grid stops agreeing
* with itself.
*
* node tools/make-dice-textures.mjs
*/
import { deflateSync } from "node:zlib";
import { writeFile, mkdir } from "node:fs/promises";
const SIZE = 256;
/* ---------- a minimal PNG encoder: RGBA, no interlace ---------- */
function crc32(buf) {
let c, table = crc32.table;
if (!table) {
table = crc32.table = new Int32Array(256);
for (let n = 0; n < 256; n++) {
c = n;
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
table[n] = c;
}
}
c = -1;
for (let i = 0; i < buf.length; i++) c = table[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
return (c ^ -1) >>> 0;
}
function chunk(type, data) {
const len = Buffer.alloc(4); len.writeUInt32BE(data.length);
const td = Buffer.concat([Buffer.from(type, "ascii"), data]);
const crc = Buffer.alloc(4); crc.writeUInt32BE(crc32(td));
return Buffer.concat([len, td, crc]);
}
function png(width, height, rgba) {
const ihdr = Buffer.alloc(13);
ihdr.writeUInt32BE(width, 0); ihdr.writeUInt32BE(height, 4);
ihdr[8] = 8; ihdr[9] = 6; ihdr[10] = 0; ihdr[11] = 0; ihdr[12] = 0;
// each scanline is prefixed with filter byte 0
const raw = Buffer.alloc(height * (width * 4 + 1));
for (let y = 0; y < height; y++) {
raw[y * (width * 4 + 1)] = 0;
rgba.copy(raw, y * (width * 4 + 1) + 1, y * width * 4, (y + 1) * width * 4);
}
return Buffer.concat([
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
chunk("IHDR", ihdr),
chunk("IDAT", deflateSync(raw, { level: 9 })),
chunk("IEND", Buffer.alloc(0))
]);
}
/* ---------- deterministic noise, so a rebuild is byte-identical ---------- */
let seed = 20260810;
const rnd = () => (seed = (seed * 1664525 + 1013904223) >>> 0) / 4294967296;
const canvas = () => Buffer.alloc(SIZE * SIZE * 4);
const put = (buf, x, y, r, g, b, a = 255) => {
const i = ((y & (SIZE - 1)) * SIZE + (x & (SIZE - 1))) * 4;
buf[i] = r; buf[i + 1] = g; buf[i + 2] = b; buf[i + 3] = a;
};
const fill = (buf, r, g, b, a = 255) => {
for (let i = 0; i < buf.length; i += 4) { buf[i] = r; buf[i + 1] = g; buf[i + 2] = b; buf[i + 3] = a; }
};
/* ============================================================
1. REGISTRY — the ruled grid of a filing system, kept true.
============================================================ */
function registry() {
const b = canvas();
fill(b, 24, 38, 141); // ultramarine ground
for (let i = 0; i < SIZE; i++) {
for (const step of [32]) {
for (let k = 0; k < SIZE; k += step) {
put(b, i, k, 102, 116, 216); // --rb-line
put(b, k, i, 102, 116, 216);
}
}
for (let k = 16; k < SIZE; k += 32) { // fainter half-lines
put(b, i, k, 60, 78, 175);
put(b, k, i, 60, 78, 175);
}
}
// registry ticks at the intersections
for (let x = 0; x < SIZE; x += 64) for (let y = 0; y < SIZE; y += 64) {
for (let d = -3; d <= 3; d++) { put(b, x + d, y, 242, 234, 114); put(b, x, y + d, 242, 234, 114); }
}
return b;
}
/* ============================================================
2. DRIFT — the same grid, no longer agreeing with itself.
Used for agents who have come loose from their own reality.
============================================================ */
function drift() {
const b = canvas();
fill(b, 20, 30, 120);
const wobble = y => Math.round(6 * Math.sin(y / 14) + 3 * Math.sin(y / 5.5));
for (let i = 0; i < SIZE; i++) {
for (let k = 0; k < SIZE; k += 32) {
put(b, i + wobble(i), k, 122, 108, 200);
put(b, k, i + wobble(i), 122, 108, 200);
}
}
for (let n = 0; n < 900; n++) { // grain where it has torn
const x = (rnd() * SIZE) | 0, y = (rnd() * SIZE) | 0;
put(b, x, y, 200, 120, 150, 255);
}
for (let x = 0; x < SIZE; x += 64) for (let y = 0; y < SIZE; y += 64) {
const ox = ((rnd() * 7) | 0) - 3, oy = ((rnd() * 7) | 0) - 3;
for (let d = -3; d <= 3; d++) { put(b, x + d + ox, y + oy, 255, 122, 92); put(b, x + ox, y + d + oy, 255, 122, 92); }
}
return b;
}
/* ============================================================
3. DISPLACED — the grid is gone. What is left is not filed.
============================================================ */
function displaced() {
const b = canvas();
fill(b, 12, 14, 60);
for (let n = 0; n < 5200; n++) {
const x = (rnd() * SIZE) | 0, y = (rnd() * SIZE) | 0;
const v = 40 + ((rnd() * 90) | 0);
put(b, x, y, v, v * 0.5, v * 0.7);
}
// a few surviving fragments of rule, going nowhere
for (let n = 0; n < 26; n++) {
const x = (rnd() * SIZE) | 0, y = (rnd() * SIZE) | 0, len = 6 + ((rnd() * 22) | 0);
const horiz = rnd() > 0.5;
for (let d = 0; d < len; d++) {
if (horiz) put(b, x + d, y, 150, 120, 190); else put(b, x, y + d, 150, 120, 190);
}
}
return b;
}
/* ============================================================
4. AGENCY — brushed plate, for the agency-issue set.
============================================================ */
function agency() {
const b = canvas();
for (let y = 0; y < SIZE; y++) {
for (let x = 0; x < SIZE; x++) {
const grain = 6 * Math.sin(x / 1.7 + y / 90) + (rnd() * 7 - 3.5);
const v = 30 + grain;
put(b, x, y, 22 + v, 33 + v, 95 + v);
}
}
for (let k = 0; k < SIZE; k += 48) for (let i = 0; i < SIZE; i++) put(b, i, k, 90, 104, 190);
return b;
}
const TEXTURES = {
"custodians-registry": registry,
"custodians-drift": drift,
"custodians-displaced": displaced,
"custodians-agency": agency
};
await mkdir(new URL("../dice/", import.meta.url), { recursive: true });
for (const [name, make] of Object.entries(TEXTURES)) {
const buf = png(SIZE, SIZE, make());
await writeFile(new URL(`../dice/${name}.png`, import.meta.url), buf);
console.log(` ${name}.png ${SIZE}x${SIZE} ${(buf.length / 1024).toFixed(1)} kB`);
}
console.log("Custodians | dice textures written");