Pressure plate
A 32×32 floor trap: a dressed-stone frame of four mitred slabs around a socket, and a darker slate plate sitting 2px proud of the frame, carved with a rune (a ring round an algiz stroke). “up” (8 frames) pulses the rune’s grooves faintly; “press” (6 frames, once) sinks the plate flush, squirts dust from the gaps and flares the rune into a pillar of light; “down” (8 frames) keeps the rune glowing as a glint runs round the ring and motes rise; “release” (6 frames, once) springs the plate back up. “down-violet” swaps the glow to violet.
6×
Get the animated sprites: Dungeon Mechanisms on itch.ioVol.2 · 12 traps, doors and breakables
The header. The comment at the top of the file is the brief: size, view, light direction and every variant.
// Pressure plate — 32x32 floor trap, top-down 3/4, key light top-left.// A dressed-stone frame (four mitred slabs) around a socket; a darker slate plate sits in the socket, 2px proud of// the frame, with a carved rune (a ring round an algiz stroke). Stepping on it sinks the plate flush: the plate's// front face disappears, its top drops 2px and the dark back gap of the socket opens behind it.// Variants:// up loop 8f — plate raised, the rune's grooves faintly pulse// press one-shot 6f — plate sinks 2px, dust squirts from the gaps, the rune flares (light pillar) -> == down f0// down loop 8f — plate flush, rune glowing steadily, a glint runs round the ring, motes rise// release one-shot 6f — rune dims, plate springs up 1px past rest and settles -> == up f0// down-violet — the glow ramp swapped to violetThe last lines. submit declares the canvas and lists the variants the renderer exports.
submit(Plate, { size: [32, 32], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: 0.8, loop: true }, anchor: { x: 16, y: GROUND }, variants: [ { name: "up", args: ["up"] }, { name: "press", args: ["press"], animate: { fps: 10, duration: 0.6, loop: false } }, { name: "down", args: ["down"] }, { name: "release", args: ["release"], animate: { fps: 10, duration: 0.6, loop: false } }, { name: "down-violet", args: ["down"], repalette: swap("glow", "v") }, ],});Show the whole program (247 lines)
// Pressure plate — 32x32 floor trap, top-down 3/4, key light top-left.// A dressed-stone frame (four mitred slabs) around a socket; a darker slate plate sits in the socket, 2px proud of// the frame, with a carved rune (a ring round an algiz stroke). Stepping on it sinks the plate flush: the plate's// front face disappears, its top drops 2px and the dark back gap of the socket opens behind it.// Variants:// up loop 8f — plate raised, the rune's grooves faintly pulse// press one-shot 6f — plate sinks 2px, dust squirts from the gaps, the rune flares (light pillar) -> == down f0// down loop 8f — plate flush, rune glowing steadily, a glint runs round the ring, motes rise// release one-shot 6f — rune dims, plate springs up 1px past rest and settles -> == up f0// down-violet — the glow ramp swapped to violetconst W = 32, GROUND = 29;const FR = { x0: 1, x1: 30, y0: 6, y1: 26, face: 2 }; // frame top face rows y0..y1, front face below itconst OP = { x0: 5, x1: 26, y0: 9, y1: 23 }; // socket opening on the frame's top faceconst PL = { x0: 6, x1: 25, y0: 10, y1: 23 }; // plate footprint (flush position)const RY = 2; // rune row offset inside the plate's top face// the rune: o = ring, a = diamond, * = core (18 x 9 grid; symmetric about the plate centre)const RUNE = [ "......oooooo......", "....oo......oo....", "...o....aa....o...", "..o....a**a....o..", "o.o...a****a...o.o", "..o....a**a....o..", "...o....aa....o...", "....oo......oo....", "......oooooo......",];const GY = 0; // grid row offset of RUNE[0]const RING = []; // ring pixels in angular order, for the glint{ RUNE.forEach((row, y) => [...row].forEach((c, x) => { if (c === "o") RING.push([x, y, Math.atan2((y - 4) * 1.6, x - 8.5)]); })); RING.sort((a, b) => a[2] - b[2]);}// ---- one frame into a key buffer: {part, key}function build(h, rune) { const B = new Array(W * W).fill(null); const set = (x, y, part, key) => { if (x >= 0 && y >= 0 && x < W && y < W) B[y * W + x] = { part, key }; }; // frame: a ring of dressed floor stones, top face then a 2px front face for (let y = FR.y0; y <= FR.y1; y++) for (let x = FR.x0; x <= FR.x1; x++) { if ((x === FR.x0 || x === FR.x1) && y === FR.y0) continue; // chamfered back corners if (x >= OP.x0 && x <= OP.x1 && y >= OP.y0 && y <= OP.y1) { // the socket: its back wall (faces the camera) is a dim row, the rest a black gap set(x, y, "socket", y === OP.y0 ? "n0" : "ink"); continue; } let k = "n2"; if (y === FR.y0 || x === FR.x0 || (y === FR.y0 + 1 && (x === FR.x0 + 1))) k = "n3"; // outer north / west edges lit if (x === FR.x1 && y > FR.y0) k = "n1"; // outer east edge if (y === OP.y1 + 1 && x >= OP.x0 && x <= OP.x1) k = "n3"; // front rim's inner lip faces north if (x === OP.x1 + 1 && y >= OP.y0 && y <= OP.y1) k = "n3"; // right rim's inner lip faces west if (y === OP.y0 - 1 && x >= OP.x0 - 1 && x <= OP.x1) k = "n1"; // back rim's inner lip faces south if (x === OP.x0 - 1 && y >= OP.y0 - 1 && y <= OP.y1) k = "n1"; // left rim's inner lip faces east set(x, y, "frame", k); } // block joints across the rims (dark joint, the next stone's lit edge beside it) const jointV = (x, y0, y1) => { for (let y = y0; y <= y1; y++) { set(x, y, "frame", "n1"); if (B[y * W + x + 1] && B[y * W + x + 1].key === "n2") set(x + 1, y, "frame", "n3"); } }; const jointH = (y, x0, x1) => { for (let x = x0; x <= x1; x++) { set(x, y, "frame", "n1"); if (B[(y + 1) * W + x] && B[(y + 1) * W + x].key === "n2") set(x, y + 1, "frame", "n3"); } }; jointV(11, FR.y0 + 1, OP.y0 - 2); jointV(21, FR.y0 + 1, OP.y0 - 2); jointV(9, OP.y1 + 2, FR.y1); jointV(18, OP.y1 + 2, FR.y1); jointV(25, OP.y1 + 2, FR.y1); jointH(15, FR.x0 + 1, OP.x0 - 2); jointH(18, OP.x1 + 2, FR.x1 - 1); // chips (countable marks) set(3, 21, "frame", "n1"); set(4, 21, "frame", "n3"); set(28, 11, "frame", "n1"); set(28, 12, "frame", "n3"); set(14, 25, "frame", "n1"); set(15, 25, "frame", "n1"); // frame front face for (let i = 1; i <= FR.face; i++) for (let x = FR.x0; x <= FR.x1; x++) { if ((x === FR.x0 || x === FR.x1) && i === FR.face) continue; // chamfered front corners let k = i === 1 ? "n1" : "n0"; if (x === FR.x0 + (i === FR.face ? 1 : 0)) k = i === 1 ? "n2" : "n1"; if (x === 9 || x === 18 || x === 25) k = "ink"; // the joints run down the face set(x, FR.y1 + i, "frame", k); } // plate: top face shifted up by h, front face h rows tall (inside the socket, so its sides never show) const top = PL.y0 - h, bot = PL.y1 - h; for (let y = top; y <= bot; y++) for (let x = PL.x0; x <= PL.x1; x++) { let k = "n3"; if (x === PL.x1) k = "n2"; if (x === PL.x0 || y === top) k = "n4"; if (y === top && x === PL.x1) k = "n3"; // carved corner brackets one pixel in const cx = x - PL.x0 <= 3 ? x - PL.x0 : PL.x1 - x <= 3 ? PL.x1 - x : -1; const cy = y - top <= 3 ? y - top : bot - y <= 3 ? bot - y : -1; if (cx >= 1 && cy >= 1 && ((cx === 1 && cy <= 3) || (cy === 1 && cx <= 3))) k = "n2"; set(x, y, "plate", k); } for (let i = 1; i <= h; i++) for (let x = PL.x0; x <= PL.x1; x++) { set(x, bot + i, "plate", i === 1 ? (x === PL.x0 ? "n2" : "n1") : x === PL.x0 ? "n1" : "n0"); } // rune grooves, then the groove's lower wall: lit stone when dark, a dim glow when the rune is lit const RW = RUNE[0].length, RH = RUNE.length; const isG = (x, y) => y >= 0 && y < RH && x >= 0 && x < RW && RUNE[y][x] !== "."; const inside = (x, y) => Math.abs(x - 8.5) + Math.abs(y - 4) * 1.6 < 5.5; // the diamond and its surround for (let y = 0; y < RH; y++) for (let x = 0; x < RW; x++) { const px = PL.x0 + 1 + x, py = top + RY + GY + y; const c = RUNE[y][x]; if (c !== ".") { set(px, py, "rune", rune.key(c, x, y)); continue; } const adj = isG(x - 1, y) || isG(x + 1, y) || isG(x, y - 1) || isG(x, y + 1); if (rune.halo === "all" && adj) set(px, py, "rune", rune.haloKey); else if (isG(x, y - 1) && !inside(x, y)) { if (rune.halo) set(px, py, "rune", rune.haloKey); else set(px, py, "plate", "n4"); } } return B;}const PARTS = ["frame", "socket", "plate", "rune"];function draw(g, B) { g.outlined("ink", () => { for (const part of PARTS) g.part(part, () => g.field({ x: [0, W - 1], y: [0, W - 1] }, (x, y) => { const b = B[y * W + x]; return b && b.part === part ? b.key : null; })); });}// ---- rune looksconst lvKey = (lv) => ["glow0", "glow1", "glow2", "glow3"][Math.max(0, Math.min(3, lv))];// a look = levels for ring / diamond / core; halo: null (dark grooves), "low" (dim glow on the grooves' lower wall)// or "all" (a solid glow around every groove, for the flash)function look(ring, dia, core, halo, haloLv, glint) { return { key: (c, x, y) => (c === "o" && glint && glint(x, y) ? "glow3" : lvKey(c === "o" ? ring : c === "a" ? dia : core)), halo: halo || null, haloKey: lvKey(haloLv || 0), };}function runeUp(t) { // faint pulse: dark grooves; the core warms first, then the diamond const p = 0.5 - 0.5 * Math.cos(TAU * t); return look(0, p > 0.6 ? 1 : 0, p > 0.85 ? 2 : p > 0.3 ? 1 : 0);}function runeDown(t) { // steady: ring and diamond glow2, core white-hot, ticks glow1; a glint runs round the ring const i = Math.floor(t * RING.length) % RING.length; const on = new Set([i, (i + 1) % RING.length].map((k) => RING[k][0] + "," + RING[k][1])); return look(2, 2, 3, "low", 1, (x, y) => on.has(x + "," + y));}// ---- effectsfunction pillar(g, top, amount) { // a shaft of light straight up out of the rune: solid at the flash, thinning into dither as it fades if (amount <= 0) return; g.detached("flare", () => { const base = top + RY + 4; const cols = [[13, 0.5, "glow2"], [14, 0.8, "glow2"], [15, 1, "glow3"], [16, 1, "glow3"], [17, 0.8, "glow2"], [18, 0.5, "glow2"]]; for (const [x, s, k0] of cols) { const len = Math.round(amount * s * (base + 1)); for (let k = 0; k < len; k++) { const y = base - k; if (y < 0) break; const u = k / len; if ((amount < 0.9 || u > 0.55 || s < 0.6) && !chk(x, y)) continue; // the fringe is always dithered g.pixel(x, y, u > 0.8 ? "glow1" : u > 0.55 && k0 === "glow3" ? "glow2" : k0); } } // sparks flung sideways from the flash if (amount > 0.5) for (const [dx, dy] of [[-6, -4], [7, -3], [-9, 1], [10, 2]]) { const x = 15.5 + dx * (amount > 0.9 ? 1 : 1.4), y = base - 3 + dy * (amount > 0.9 ? 1 : 1.4) - (amount > 0.9 ? 0 : 2); g.pixel(Math.round(x), Math.round(y), amount > 0.9 ? "glow3" : "glow2"); } });}// dust: [x, y, dx, dy] — puffs squirting out of the side gaps and over the front rimconst DUST = [[3, 13, -1, -0.6], [4, 20, -1, 0.2], [28, 13, 1, -0.6], [27, 20, 1, 0.2], [10, 25, -0.8, 0.3], [21, 25, 0.8, 0.3]];function dust(g, k) { if (k <= 0 || k > 3) return; g.detached("dust", () => { for (const [x0, y0, dx, dy] of DUST) { const x = Math.max(1, Math.min(30, Math.round(x0 + dx * (k - 1) * 1.3))), y = Math.round(y0 + dy * (k - 1) * 1.5 - (k - 1) * 0.7); const s = dx < 0 ? -1 : 1; if (k === 1) { // a round puff: lit crown, grey body, shaded underside g.pixel(x, y - 1, "n5"); g.pixel(x + s, y - 1, "n4"); g.pixel(x, y, "n4"); g.pixel(x + s, y, "n3"); } else if (k === 2) { // it spreads out and thins g.pixel(x - 1, y, "n4"); g.pixel(x, y, "n4"); g.pixel(x + 1, y, "n3"); g.pixel(x + s, y - 1, "n4"); } else { // last wisps g.pixel(x, y - 1, "n3"); if (x + 2 * s >= 0 && x + 2 * s <= 31) g.pixel(x + 2 * s, y, "n3"); } } });}function motes(g, top, t) { g.detached("motes", () => { for (let k = 0; k < 3; k++) { const p = (t + k / 3) % 1; const x = [12, 16, 19][k] + Math.round(Math.sin(TAU * (p + k * 0.37))); const y = top + RY + 3 - Math.round(p * 11); if (p < 0.8) g.pixel(x, y, p < 0.4 ? "glow3" : "glow2"); } });}// ---- statesconst PRESS = [ // h, rune, pillar, dust stage [2, () => runeUp(0), 0, 0], [1, () => look(1, 1, 2, "low", 0), 0, 0], [0, () => look(3, 3, 3, "all", 2), 1, 1], // impact: the rune flares white, dust squirts from the gaps [0, () => look(3, 3, 3, "all", 1), 0.7, 2], [0, () => look(2, 3, 3, "low", 1), 0.35, 3], [0, () => runeDown(0), 0, 0], // == down, frame 0];const RELEASE = [ [0, () => runeDown(0)], // == down, frame 0 [0, () => look(1, 2, 2, "low", 0)], [1, () => look(1, 1, 1)], [3, () => look(0, 0, 1)], // springs up a pixel past rest [2, () => runeUp(0)], [2, () => runeUp(0)], // == up, frame 0];const Plate = sprite(pal, (g, t, state) => { contactShadow(g, 16, GROUND, 15, 2); if (state === "up") { draw(g, build(2, runeUp(t))); return; } if (state === "down") { draw(g, build(0, runeDown(t))); motes(g, PL.y0, t); return; } if (state === "press") { const f = Math.min(PRESS.length - 1, Math.round(t * PRESS.length)); const [h, rune, amount, d] = PRESS[f]; draw(g, build(h, rune())); pillar(g, PL.y0 - h, amount); dust(g, d); if (f === PRESS.length - 1) motes(g, PL.y0, 0); return; } const f = Math.min(RELEASE.length - 1, Math.round(t * RELEASE.length)); const [h, rune] = RELEASE[f]; draw(g, build(h, rune())); if (f === 0) motes(g, PL.y0, 0);});submit(Plate, { size: [32, 32], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: 0.8, loop: true }, anchor: { x: 16, y: GROUND }, variants: [ { name: "up", args: ["up"] }, { name: "press", args: ["press"], animate: { fps: 10, duration: 0.6, loop: false } }, { name: "down", args: ["down"] }, { name: "release", args: ["release"], animate: { fps: 10, duration: 0.6, loop: false } }, { name: "down-violet", args: ["down"], repalette: swap("glow", "v") }, ],});These excerpts omit the boilerplate prepended by the asset pack build script: a single-line runtime header, the shared palette, and common helper utilities (contactShadow, litIndex, flame, hash, chk, TAU). The displayed line numbers match the original source files. Each program was authored by an AI coding agent and adjusted after visual inspection of the rendered frames.
Have your agent write programs like this.
Tessarune gives it the drawing API reference, 25 production manuals and a renderer to check its work against.