Tessarune
Dungeon Mechanisms · spike_trap.js

Spike trap

32×32 · 207 lines of JavaScript

A 32×32 floor tile: a riveted iron plate set into the floor, a lit frame round a recessed panel pierced by 8 staggered holes. The spikes are one rig, shaded cones that slide out of their holes, so every state is the same spikes at a different height. “idle” (8 frames) hides them while a faint glint winks from hole to hole; “trigger” (8 frames, once) shoots them past full height with a puff of dust and settles; “armed” (8 frames) runs a glint along the tips; “retract” (6 frames, once) sinks them back.

Spike trap — Rendered output6×
Rendered by this program, variant “trigger” · 32×32 · 8 frames
Dungeon MechanismsGet 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.

spike_trap.js · 1–9
// Spike trap — 32x32 floor tile, top-down 3/4, key light top-left.// A riveted iron plate set into the floor: a lit frame round a recessed panel pierced by 8 holes (3-2-3, staggered).// The spikes are one rig: each spike is a shaded cone whose visible length h slides out of its hole, so every// state is the same spikes at a different h, occluded back row to front row.// Variants://   idle     loop 8f     — spikes hidden; a faint glint winks from hole to hole//   trigger  one-shot 8f — spikes shoot up past full height, dust puffs out from the plate, spikes settle (== armed f0)//   armed    loop 8f     — spikes up; a glint runs along the tips left to right//   retract  one-shot 6f — spikes sink back into the holes (== idle f0)

The last lines. submit declares the canvas and lists the variants the renderer exports.

spike_trap.js · 197–207
submit(SpikeTrap, {  size: [32, 32], origin: "top-left", kind: "icon", scale: 1,  animate: { fps: 10, duration: N_LOOP / 10, loop: true },  anchor: { x: CX, y: GROUND },  variants: [    { name: "idle", args: ["idle"] },    { name: "trigger", args: ["trigger"], animate: { fps: 10, duration: N_TRIG / 10, loop: false } },    { name: "armed", args: ["armed"] },    { name: "retract", args: ["retract"], animate: { fps: 10, duration: N_RET / 10, loop: false } },  ],});
Show the whole program (207 lines)
spike_trap.js · 207 lines
// Spike trap — 32x32 floor tile, top-down 3/4, key light top-left.// A riveted iron plate set into the floor: a lit frame round a recessed panel pierced by 8 holes (3-2-3, staggered).// The spikes are one rig: each spike is a shaded cone whose visible length h slides out of its hole, so every// state is the same spikes at a different h, occluded back row to front row.// Variants://   idle     loop 8f     — spikes hidden; a faint glint winks from hole to hole//   trigger  one-shot 8f — spikes shoot up past full height, dust puffs out from the plate, spikes settle (== armed f0)//   armed    loop 8f     — spikes up; a glint runs along the tips left to right//   retract  one-shot 6f — spikes sink back into the holes (== idle f0)const CX = 16, GROUND = 29;const PLATE = { x0: 2, x1: 30, y0: 8, y1: 26 };      // top face; the front face is 2px below itconst FULL = 7;                                        // full spike length (px above the hole)const N_LOOP = 8, N_TRIG = 8, N_RET = 6;// holes: [cx, top row y] back row first; 3-2-3 staggeredconst ROWS = [  { y: 11, xs: [8, 16, 24] },  { y: 16, xs: [12, 20] },  { y: 21, xs: [8, 16, 24] },];const HOLES = [];for (const r of ROWS) for (const x of r.xs) HOLES.push({ x, y: r.y });// rank of each hole in a diagonal sweep from the top-left (glint order)const RANK = new Array(HOLES.length);HOLES.map((h, i) => [h.x + 1.4 * h.y, i]).sort((a, b) => a[0] - b[0]).forEach((a, r) => { RANK[a[1]] = r; });// ---- plate --------------------------------------------------------------------------------------function plate(g) {  const { x0, x1, y0, y1 } = PLATE;  g.part("plate", () => {    g.field({ x: [x0, x1], y: [y0, y1 + 2] }, (x, y) => {      if (y > y1) {                                      // front face (thickness): lit at the left end        if (y === y1 + 2) return x < x0 + 3 ? "n1" : "n0";        return x < x0 + 3 ? "n2" : "n1";      }      const dx0 = x - x0, dx1 = x1 - x, dy0 = y - y0, dy1 = y1 - y;      if (dx0 < 2 || dx1 < 2 || dy0 < 2 || dy1 < 2) {        // raised frame: top & left bands face the light, the right and front bands turn away        if (dy0 === 0) return dx1 < 2 ? "n3" : "n4";        if (dx0 === 0) return dy1 < 2 ? "n3" : "n4";        if (dx1 === 0) return "n1";        if (dy1 === 0) return "n1";        if (dx0 === 1 || dy0 === 1) return dx1 < 2 ? "n2" : "n3";        return "n2";      }      // recessed panel: the frame shades its top and left walls, the lower lip catches the light      if (dy0 === 2) return "n0";      if (dx0 === 2) return "n0";      if (dy1 === 2) return "n3";      if (dx1 === 2) return "n2";      // a soft falloff: the panel is a touch brighter toward the top-left      return dx0 + 1.4 * dy0 < 15 ? "n2" : "n1";    });    // rivets on the frame corners and mid-sides    for (const [x, y] of [[x0 + 1, y0 + 1], [x1 - 2, y0 + 1], [x0 + 1, y1 - 1], [x1 - 2, y1 - 1], [CX - 1, y0 + 1], [CX - 1, y1 - 1]]) {      g.pixel(x, y, x < CX && y < y1 - 1 ? "n5" : "n4");      g.pixel(x + 1, y, "n1");    }  });  g.part("stain", () => {    // old blood dried round one hole on the front row    for (const [x, y, k] of [[17, 25, "r0"], [18, 25, "w0"], [14, 25, "w0"], [19, 22, "r0"], [19, 23, "w0"]]) g.pixel(x, y, k);  });}function holes(g, glint) {  g.part("holes", () => {    HOLES.forEach((h, i) => {      const { x, y } = h;      g.pixel(x - 1, y, "ink"); g.pixel(x, y, "ink"); g.pixel(x + 1, y, "n0");      g.pixel(x - 1, y + 1, "n0"); g.pixel(x, y + 1, "ink"); g.pixel(x + 1, y + 1, "n0");      g.line([x - 1, y + 2], [x + 1, y + 2], "n3");        // lower lip catches the light      const k = glint ? glint(i) : 0;                      // a hidden tip catching the light      if (k > 0) g.pixel(x, y + 1, k > 1 ? "n4" : "n3");    });  });}// ---- spikes -------------------------------------------------------------------------------------// cone profile from the tip down: keys for columns cx-1, cx, cx+1 (null = empty); lit on the leftfunction spikeRow(k) {  if (k === 0) return [null, "n5", null];  if (k === 1) return [null, "n4", null];  if (k < 3) return ["n4", "n2", null];  return ["n4", "n3", "n1"];}function spikePixels(cx, hy, h, tip, blood) {  // h = visible length above the hole; hy = the hole's top row (the spike stands in its lower row)  const base = hy + 1, n = Math.round(h), out = [];  for (let r = 0; r < n; r++) {    const y = base - (n - 1) + r, row = spikeRow(r);    const collar = r === n - 1 && n > 2;                   // where it leaves the hole: a darker band    for (let c = 0; c < 3; c++) {      let key = row[c];      if (!key) continue;      if (collar) key = c === 0 ? "n2" : c === 1 ? "n1" : "n0";      if (r <= 1 && tip) key = tip;      if (blood && r >= 2 && r <= 3) key = c === 0 ? "r1" : "r0";      if (blood && r === 4 && c === 1) key = "r0";      out.push([cx + c - 1, y, key]);    }  }  return { base, px: out };}function spikes(g, hs, tipAt) {  // each spike carries its own ink outline, drawn back row to front row, so a front spike is cut out against the  // ones behind it (g.outlined only rings empty pixels; here the spikes stand over the plate)  g.part("spikes", () => HOLES.forEach((hole, i) => {    const h = hs(i);    if (Math.round(h) <= 0) return;    const { base, px } = spikePixels(hole.x, hole.y, h, tipAt ? tipAt(i) : null, i === 6);    const has = new Set(px.map(([x, y]) => x + "," + y));    for (const [x, y] of px) for (const [dx, dy] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) {      const qx = x + dx, qy = y + dy;      if (qy > base || has.has(qx + "," + qy)) continue;      g.pixel(qx, qy, "ink");    }    for (const [x, y, k] of px) g.pixel(x, y, k);  }));}function star(g, x, y, size) {  g.pixel(x, y, "n5");  for (const [dx, dy] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) g.pixel(x + dx, y + dy, size > 1 ? "n5" : "n4");  if (size > 1) for (const [dx, dy] of [[2, 0], [-2, 0], [0, -2]]) g.pixel(x + dx, y + dy, "n4");}// ---- dust ---------------------------------------------------------------------------------------// puffs thrown out from under the plate's edges; stage 1..4: burst, billow, thin out, last motesconst PUFFS = [[3, 21, -1], [28, 19, 1], [7, 28, -1], [24, 28, 1]];const CLOUD = [  null,  ["  54 ", " 4443"],  [" 544  ", "544443", " 3443 "],  [" 4 4  ", "4 4 43", " 3 3  "],  ["  3   ", "3    3", "   3  "],];function dust(g, stage) {  if (stage <= 0 || stage > 4) return;  const rows = CLOUD[stage];  g.detached("dust", () => {    for (const [px, py, s] of PUFFS) {      const w = rows[0].length, x0 = Math.max(0, Math.min(31 - w, px + s * (stage - 1) - (w >> 1)));      const y0 = py - rows.length + 1 - Math.floor((stage - 1) / 2);      rows.forEach((row, j) => {        for (let i = 0; i < row.length; i++) {          const c = row[i];          if (c !== " ") g.pixel(x0 + i, y0 + j, "n" + c);        }      });    }  });}// ---- states -------------------------------------------------------------------------------------// trigger: spikes (h), dust stage per frameconst TRIG = [[0, 0], [9, 1], [6, 2], [8, 3], [7, 4], [7, 0], [7, 0], [7, 0]];const RET = [7, 6, 4, 1, 0, 0];const sweep = (rank) => 1 + Math.floor((rank * 7) / HOLES.length);   // frame (1..7) in which a rank is litfunction holeGlint(f) {  // frame 0 rests (so the one-shots can join it); frames 1..7 the glint crosses the holes diagonally  if (f < 1) return null;  return (i) => (sweep(RANK[i]) === f ? 2 : sweep(RANK[i]) === f - 1 ? 1 : 0);}const SpikeTrap = sprite(pal, (g, t, state) => {  contactShadow(g, CX, GROUND, 15, 2);  g.outlined("ink", () => plate(g));  if (state === "idle") {    const f = Math.round(t * N_LOOP) % N_LOOP;    holes(g, holeGlint(f));    return;  }  holes(g, null);  if (state === "armed") {    const f = Math.round(t * N_LOOP) % N_LOOP;    // frame 0 rests; frames 1..7 the glint walks the tips by rank    const lit = (i) => f >= 1 && sweep(RANK[i]) === f;    spikes(g, () => FULL, (i) => (lit(i) ? "n5" : null));    g.detached("glint", () => {      HOLES.forEach((h, i) => { if (f >= 1 && sweep(RANK[i]) === f && (RANK[i] === 7 || sweep(RANK[i] + 1) !== f)) star(g, h.x, h.y + 1 - (FULL - 1), 1); });    });    return;  }  if (state === "trigger") {    const f = Math.min(N_TRIG - 1, Math.round(t * N_TRIG));    const [h, d] = TRIG[f];    spikes(g, () => h);    dust(g, d);    return;  }  const f = Math.min(N_RET - 1, Math.round(t * N_RET));  spikes(g, () => RET[f]);});submit(SpikeTrap, {  size: [32, 32], origin: "top-left", kind: "icon", scale: 1,  animate: { fps: 10, duration: N_LOOP / 10, loop: true },  anchor: { x: CX, y: GROUND },  variants: [    { name: "idle", args: ["idle"] },    { name: "trigger", args: ["trigger"], animate: { fps: 10, duration: N_TRIG / 10, loop: false } },    { name: "armed", args: ["armed"] },    { name: "retract", args: ["retract"], animate: { fps: 10, duration: N_RET / 10, loop: false } },  ],});

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.