Portcullis
A 48×48 camera-facing prop: an iron grate with pointed teeth sliding in a slot of the same dressed-stone arch and ashlar wall stub as the dungeon door, with the dark passage showing through the gaps. The bars are shaded as round iron, the arch shades the top of the grate and the bars stripe the passage floor with shadow. “closed” (8 frames) runs a glint along the middle cross bar; “raise” (10 frames, once) hauls the grate up in jerks as the chain slips twice; “raised” (8 frames) leaves only the teeth below the arch; “drop” (6 frames, once) slams it down with a bounce and a dust puff.
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.
// Portcullis — 48x48, top-down 3/4, key light top-left. Camera-facing only.// An iron grate with pointed teeth, sliding in a slot of the same dressed-stone arch and ashlar wall stub as the// dungeon door (so the two drop into one wall). Built the same way: the wall, frame and keystone are solids, the grate// is a patterned plane in the slot behind the wall face; every pixel casts the view ray, a second ray toward the key// light casts the shadows (the arch shades the top of the grate, the bars stripe the passage floor). Through the gaps// you see the dark passage beyond. Bars are shaded as round iron (lit left / top edge, dark right edge / underside).// Variants:// closed loop, 8f — a glint runs along the middle cross bar// raise one-shot, 10f — the grate is hauled up into the arch in jerks (the chain slips twice), grit falls from the slot// raised loop, 8f — only the teeth hang below the arch crown; a glint walks the tips, a grain of grit drops// drop one-shot, 6f — the grate slams down, bounces, a dust puff rolls out at its foot; ends == closedThe last lines. submit declares the canvas and lists the variants the renderer exports.
submit(Portcullis, { size: [48, 48], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: 0.8, loop: true }, anchor: { x: 24, y: 45 }, variants: [ { name: "closed", args: ["closed"] }, { name: "raise", args: ["raise"], animate: { fps: 10, duration: 1.0, loop: false } }, { name: "raised", args: ["raised"] }, { name: "drop", args: ["drop"], animate: { fps: 10, duration: 0.6, loop: false } }, ],});Show the whole program (303 lines)
// Portcullis — 48x48, top-down 3/4, key light top-left. Camera-facing only.// An iron grate with pointed teeth, sliding in a slot of the same dressed-stone arch and ashlar wall stub as the// dungeon door (so the two drop into one wall). Built the same way: the wall, frame and keystone are solids, the grate// is a patterned plane in the slot behind the wall face; every pixel casts the view ray, a second ray toward the key// light casts the shadows (the arch shades the top of the grate, the bars stripe the passage floor). Through the gaps// you see the dark passage beyond. Bars are shaded as round iron (lit left / top edge, dark right edge / underside).// Variants:// closed loop, 8f — a glint runs along the middle cross bar// raise one-shot, 10f — the grate is hauled up into the arch in jerks (the chain slips twice), grit falls from the slot// raised loop, 8f — only the teeth hang below the arch crown; a glint walks the tips, a grain of grit drops// drop one-shot, 6f — the grate slams down, bounces, a dust puff rolls out at its foot; ends == closedconst K = 0.4, CX = 24, BASE = 46; // screen rise per unit of depth (as chest.js), wall base rowconst UW = 24, WT = 10, WH = 38; // wall stub: half-length, thickness, heightconst SPR = 20, RO = 9, RF = 13, FP = 1.5; // arch: springing height, opening radius, frame outer radius, frame projectionconst CH = WH / 7; // ashlar course heightconst VG = 1.2; // the grate's plane (in the slot, just behind the wall face)const BARS_U = [-8, -4, 0, 4, 8], BAR_W = 0.85; // uprights (centres, half-width)const BARS_Z = [5.5, 11, 16.5, 22, 27.5, 33, 38.5, 44], BAR_H = 0.72; // cross bars, heights above the tipsconst TOOTH = 4; // tooth length (the uprights taper to points below the first cross bar)const LIFT = 17; // raised: tips hang at z = 17, the teeth and one cross bar under the arch crown (29)const RAISE_L = [0, 0.6, 2.4, 4.6, 3.9, 7.4, 11, 10.2, 14, LIFT]; // hauled in jerks; the chain slips at 4 and 7const DROP_L = [LIFT, 8, 0, 1.5, 0, 0]; // falls, slams, bounces, rests == closedconst LV = norm3([-0.6, -0.25, 0.76]); // toward the key light (world: X right, Y away, Z up)const LS = norm3([-0.45, -0.6, 0.66]); // cast-shadow direction: the same light, pulled forward so recess shadows stay shortconst STONE = ["n0", "n1", "n2", "n3", "n4"], IRON = ["n0", "n1", "n2", "n3", "n4"];const WALLB = [-UW, UW, 0, WT, 0, WH];const KEYB = [-2.3, 2.3, -FP - 0.8, 0, SPR + RO - 0.5, SPR + RF + 0.8];const SILLB = [-RO, RO, -FP, WT, -2, 0]; // the passage floor under the arch (seen only through it)function norm3(v) { const m = Math.hypot(v[0], v[1], v[2]); return [v[0] / m, v[1] / m, v[2] / m]; }const neg = (n) => [-n[0], -n[1], -n[2]];const clamp = (v, a, b) => Math.max(a, Math.min(b, v));const proj = (q) => [CX + q[0], BASE - q[2] - K * q[1]];// ---- ray vs solids. Intervals are {tin, tout, nin, nout} (outward normals) or null.function slab(o, d, i, lo, hi, st) { const di = d[i], oi = o[i]; if (di < 1e-9 && di > -1e-9) return oi >= lo && oi <= hi; let t1 = (lo - oi) / di, t2 = (hi - oi) / di, s1 = -1, s2 = 1; if (t1 > t2) { const k = t1; t1 = t2; t2 = k; s1 = 1; s2 = -1; } if (t1 > st.tin) { st.tin = t1; st.nin = [0, 0, 0]; st.nin[i] = s1; } if (t2 < st.tout) { st.tout = t2; st.nout = [0, 0, 0]; st.nout[i] = s2; } return st.tin <= st.tout;}function box(o, d, b) { const st = { tin: -1e9, tout: 1e9, nin: [0, 0, 0], nout: [0, 0, 0] }; for (let i = 0; i < 3; i++) if (!slab(o, d, i, b[2 * i], b[2 * i + 1], st)) return null; return st;}// D-shaped prism along v: |u| <= w, z >= 0, v0 <= v <= v1, and (z <= SPR or inside the circle r about (0, SPR))function inD(u, z, w, r) { return Math.abs(u) <= w && z >= 0 && (z <= SPR || u * u + (z - SPR) * (z - SPR) <= r * r); }function dshape(o, d, w, r, v0, v1) { const st = { tin: -1e9, tout: 1e9, nin: [0, 0, 0], nout: [0, 0, 0] }; if (!slab(o, d, 0, -w, w, st) || !slab(o, d, 1, v0, v1, st) || !slab(o, d, 2, 0, 1e9, st)) return null; let hA = -1e9, hB = 1e9, cA = 1, cB = 0; if (d[2] < 1e-9 && d[2] > -1e-9) { if (o[2] > SPR) { hA = 1; hB = 0; } } else { const th = (SPR - o[2]) / d[2]; if (d[2] > 0) hB = th; else hA = th; } const A = d[0] * d[0] + d[2] * d[2], ou = o[0], oz = o[2] - SPR; if (A < 1e-12) { if (ou * ou + oz * oz <= r * r) { cA = -1e9; cB = 1e9; } } else { const B = 2 * (ou * d[0] + oz * d[2]), C = ou * ou + oz * oz - r * r, D = B * B - 4 * A * C; if (D >= 0) { const s = Math.sqrt(D); cA = (-B - s) / (2 * A); cB = (-B + s) / (2 * A); } } const a1 = Math.max(st.tin, hA), b1 = Math.min(st.tout, hB), a2 = Math.max(st.tin, cA), b2 = Math.min(st.tout, cB); const ok1 = a1 <= b1, ok2 = a2 <= b2; if (!ok1 && !ok2) return null; const tin = ok1 && ok2 ? Math.min(a1, a2) : ok1 ? a1 : a2; const tout = ok1 && ok2 ? Math.max(b1, b2) : ok1 ? b1 : b2; const capN = (t) => { const u = o[0] + t * d[0], z = o[2] + t * d[2] - SPR, m = Math.hypot(u, z) || 1; return [u / m, 0, z / m]; }; return { tin, tout, nin: tin === st.tin ? st.nin : ok2 && tin === cA ? capN(tin) : [0, 0, 1], nout: tout === st.tout ? st.nout : ok2 && tout === cB ? capN(tout) : [0, 0, 1], };}// first surface of (A minus B) past epsfunction minus(a, b, eps) { if (!a || a.tout < eps) return null; let best = null; if (a.tin > eps && !(b && a.tin >= b.tin && a.tin <= b.tout)) best = { t: a.tin, n: a.nin, inner: false }; if (b && b.tout > eps && b.tout >= a.tin && b.tout < a.tout && (!best || b.tout < best.t)) best = { t: b.tout, n: neg(b.nout), inner: true }; return best;}// the grate pattern at (u, zr = height above the tooth tips): null, or {kind, k, n (surface normal)}function grateAt(u, zr) { if (zr < 0 || Math.abs(u) > RO + 0.5) return null; for (let i = 0; i < BARS_Z.length; i++) { const y = (zr - BARS_Z[i]) / BAR_H; if (Math.abs(y) <= 1) { let rivet = false; for (const uc of BARS_U) if (Math.abs(u - uc) < 0.5 && Math.abs(y) < 0.6) rivet = true; return { kind: "cross", k: i, rivet, n: [0, -Math.sqrt(1 - 0.6 * y * y), 0.75 * y] }; } } for (let i = 0; i < BARS_U.length; i++) { const hw = zr < TOOTH ? 0.12 + (BAR_W - 0.12) * (zr / TOOTH) : BAR_W, x = (u - BARS_U[i]) / hw; if (Math.abs(x) <= 1) return { kind: "bar", k: 10 + i, tooth: zr < TOOTH + 0.5, n: [0.75 * x, -Math.sqrt(1 - 0.6 * x * x), 0] }; } return null;}function grateHit(o, d, L, eps) { const t = (VG - o[1]) / d[1]; if (t <= eps) return null; const u = o[0] + t * d[0], z = o[2] + t * d[2], gr = grateAt(u, z - L); return gr ? { t, n: gr.n, gr, zr: z - L, u } : null;}function scene(o, d, L, eps, primary, skipGrate) { let best = null; const take = (h, id, extra) => { if (h && h.t > eps && (!best || h.t < best.t)) best = Object.assign({ t: h.t, n: h.n, id, inner: h.inner }, extra); }; take(minus(box(o, d, WALLB), dshape(o, d, RF, RF, -1e9, 1e9), eps), "wall"); take(minus(dshape(o, d, RF, RF, -FP, WT), dshape(o, d, RO, RO, -1e9, 1e9), eps), "frame"); const kb = box(o, d, KEYB); if (kb) take({ t: kb.tin, n: kb.nin }, "key"); let gate = true; // primary rays see the grate and the passage only through the arch if (primary) { const t0 = (-FP - o[1]) / d[1]; gate = inD(o[0] + t0 * d[0], o[2] + t0 * d[2], RO, RO); } if (gate && !skipGrate) { const g = grateHit(o, d, L, eps); if (g) take(g, "grate", { gr: g.gr, zr: g.zr, gu: g.u }); } if (primary && gate) { const sb = box(o, d, SILLB); if (sb) take({ t: sb.tin, n: sb.nin }, "frame", { inner: true }); const c = dshape(o, d, RO, RO, WT, 1e9); if (c) take({ t: c.tout, n: neg(c.nout) }, "corridor"); } return best;}function classify(h, p) { const u = p[0], v = p[1], z = p[2], n = h.n; if (h.id === "wall") { if (n[2] > 0.5) return { part: "wall", mat: "stone", s: "wt", lift: 0.45, k: Math.floor((u + UW + 3) / 9) + 50 * Math.floor(v / 5.01) }; const c = Math.floor(z / CH), j = Math.floor((u + UW + 7.5 + (c & 1) * 4.5) / 9); return { part: "wall", mat: "stone", s: "wf", k: c * 64 + j }; } if (h.id === "frame" && !h.inner) { const k = z <= SPR ? (u < 0 ? 0 : 20) + Math.floor(z / (SPR / 3)) : 50 + Math.floor(Math.atan2(z - SPR, u) / (Math.PI / 7)); return { part: "arch", mat: "stone", s: n[1] < -0.5 ? "ff" : "fr", k, lift: 0.95 }; } if (h.id === "key") return { part: "arch", mat: "stone", s: "key", k: 0, lift: 0.9, carve: n[1] < -0.5 && Math.abs(Math.abs(u) + Math.abs(z - 31.3) - 1.3) < 0.45 }; if (h.id === "frame") return { part: "passage", mat: "reveal", s: n[2] > 0.5 ? "floor" : "side", k: 0, v }; if (h.id === "corridor") return { part: "passage", mat: "void", s: "void", k: 0, v }; const gr = h.gr; return { part: "grate", mat: "iron", s: gr.kind, k: gr.k, rivet: gr.rivet, tooth: gr.tooth, u: h.gu, zr: h.zr };}function cast(L) { const buf = new Array(48 * 48).fill(null); for (let py = 0; py < 48; py++) for (let px = 0; px < 48; px++) { const o = [px + 0.5 - CX, -30, BASE - (py + 0.5) + 30 * K], d = [0, 1, -K]; const h = scene(o, d, L, 0, true, false); if (!h) continue; const p = [o[0] + h.t * d[0], o[1] + h.t * d[1], o[2] + h.t * d[2]]; const c = classify(h, p); c.d = h.n[0] * LV[0] + h.n[1] * LV[1] + h.n[2] * LV[2]; c.t = h.t; c.v = p[1]; if (c.d > 0 && c.mat !== "void") { // the grate is a thin plane: start its shadow ray from just in front of it const q = [p[0] + h.n[0] * 0.06, p[1] + h.n[1] * 0.06 - (h.id === "grate" ? 0.3 : 0), p[2] + h.n[2] * 0.06]; c.sh = !!scene(q, LS, L, 1e-4, false, h.id === "grate"); } buf[py * 48 + px] = c; } return { buf, proj };}function tone(buf, fx) { const keys = new Array(48 * 48).fill(null); for (let i = 0; i < 48 * 48; i++) { const b = buf[i]; if (!b) continue; const px = i % 48, py = (i - px) / 48; const up = py > 0 ? buf[i - 48] : null, dn = py < 47 ? buf[i + 48] : null; const lf = px > 0 ? buf[i - 1] : null, rt = px < 47 ? buf[i + 1] : null; const seam = (q) => q && q.s === b.s && q.k !== b.k; let key; if (b.mat === "stone") { let val = 1.3 + 1.25 * b.d + (b.lift || 0); const h = hash(b.k * 1.7 + b.s.charCodeAt(1), 3.1); if (h < 0.22) val -= 0.45; else if (h > 0.8) val += 0.4; if (b.sh) val -= 0.9; let lv = Math.floor(val); if (seam(rt) || seam(dn)) lv = -1; else if (seam(up) || seam(lf)) lv += 1; else if (b.part === "arch" && ((up && up.part !== "arch") || (lf && lf.part !== "arch"))) lv += 1; if (!up && lv >= 0) lv += 1; if (b.carve) lv = Math.min(lv, 1); key = lv < 0 ? (val < 1.2 ? "ink" : "n0") : STONE[clamp(lv, 0, 4)]; } else if (b.mat === "reveal") { const dv = b.v + FP; key = b.sh ? (dv < 4 ? "n0" : "ink") : dv < 2.5 ? "n1" : dv < 6 ? "n0" : "ink"; } else if (b.mat === "void") { const dv = b.v - WT; key = dv < 2 ? "n0" : dv < 6 && chk(px, py) ? "n0" : "ink"; } else if (b.mat === "iron") { let val = 1.1 + 3.0 * b.d; if (b.sh) val -= 0.7; let lv = Math.floor(val); const same = (q) => q && q.mat === "iron" && q.k === b.k; if (b.s === "bar" && !same(lf)) lv += 1; // round uprights: a lit left rim if (b.s === "cross" && !same(up)) lv += 1; // cross bars: a lit top rim if (b.rivet) lv = Math.max(lv + 1, 3); const g = fx.glint ? fx.glint(b.u, b.zr, b) : 0; lv += g; key = g >= 2 ? "n5" : IRON[clamp(lv, 0, 4)]; } keys[i] = key; } return keys;}const PARTS = ["wall", "arch", "passage", "grate"];function drawGate(g, L, fx) { const fr = cast(L), buf = fr.buf, keys = tone(buf, fx); let x0 = 47, x1 = 0, y0 = 47, y1 = 0; const seen = new Set(); for (let i = 0; i < 48 * 48; i++) if (buf[i]) { const x = i % 48, y = (i - x) / 48; x0 = Math.min(x0, x); x1 = Math.max(x1, x); y0 = Math.min(y0, y); y1 = Math.max(y1, y); seen.add(buf[i].part); } g.outlined("ink", () => { for (const part of PARTS) if (seen.has(part)) g.part(part, () => g.field({ x: [x0, x1], y: [y0, y1] }, (x, y) => { const b = buf[y * 48 + x]; return b && b.part === part ? keys[y * 48 + x] : null; })); }); return fr;}// a local point is drawn only where it is in front of what the ray already hit (rays never move in u)function put(g, fr, q, key) { const [sx, sy] = fr.proj(q), x = Math.floor(sx), y = Math.floor(sy); if (x < 0 || y < 0 || x > 47 || y > 47) return; const b = fr.buf[y * 48 + x]; if (b && b.v < q[1]) return; g.pixel(x, y, key);}// grit sifting down from the slot in the arch crown: [u, phase]const GRIT = [[-2.5, 0], [1.5, 0.35], [4, 0.7], [-5, 0.55]];const Portcullis = sprite(pal, (g, t, state) => { if (state === "closed") { // a glint runs along the top edge of the middle cross bar, left to right, then rests const f = Math.round(t * 8), gx = -11 + (f - 1) * 4; const glint = f >= 1 && f <= 6 ? (u, zr, b) => (b.s === "cross" && b.k === 2 && zr > BARS_Z[2] ? (Math.abs(u - gx) < 1 ? 2 : Math.abs(u - gx) < 2.6 ? 1 : 0) : 0) : null; drawGate(g, 0, { glint }); return; } if (state === "raise") { const f = Math.min(9, Math.round(t * 10)), L = RAISE_L[f]; const fr = drawGate(g, L, {}); g.detached("grit", () => { for (const [u0, ph] of GRIT) { const k = f - 2 - Math.round(ph * 4); if (k < 0 || k > 4) continue; const z = 27 - k * (2 + k * 1.2); if (z > 0.5) put(g, fr, [u0, -0.3, z], k < 2 ? "n4" : "n3"); } if (f === 4 || f === 7) for (const s of [-1, 1]) put(g, fr, [s * 7.8, -0.3, SPR + 5.2], "n3"); // the chain slips: a jolt of grit at the slot }); return; } if (state === "raised") { const f = Math.round(t * 8); // a glint walks along the tooth tips, and a grain of grit drops from between the teeth const glint = f >= 1 && f <= 5 ? (u, zr, b) => (b.tooth && Math.abs(u - BARS_U[f - 1]) < 1 ? 1 : 0) : null; const fr = drawGate(g, LIFT, { glint }); g.detached("grit", () => { const k = f - 2; if (k >= 0 && k <= 3) put(g, fr, [-2, -0.3, LIFT - 1 - k * (2 + k * 1.5)], k < 2 ? "n4" : "n3"); }); return; } // drop const f = Math.min(5, Math.round(t * 6)), L = DROP_L[f]; const fr = drawGate(g, L, {}); if (f === 1) g.detached("smear", () => { // motion trail above the falling teeth for (const u of BARS_U) for (let z = L + TOOTH + 1; z < L + TOOTH + 6; z += 1.4) put(g, fr, [u, VG - 0.2, z], "n2"); }); if (f >= 2 && f <= 4) g.detached("puff", () => { // a dust puff bursts out along the threshold from under the teeth, rolls outward and rises, then thins out const k = f - 2, half = [9, 12, 14][k], top = [3, 3, 3][k], lift = [0, 1, 2][k]; for (let row = 0; row < top; row++) { const w = half - row * 3 - Math.round(2 * hash(row, k)), y = 45 - lift - row; for (let x = 24 - w; x <= 24 + w; x++) { const r = Math.abs(x - 24) / w; // denser at the core, ragged toward the rolling ends if (!chk(x, y) || (k === 1 && r > 0.6 && (x + row) % 4 === 0) || (k === 2 && (x + row) % 4 !== 1)) continue; g.pixel(x, y, k === 0 ? (r < 0.5 ? "n4" : "n3") : k === 1 ? (r < 0.4 ? "n4" : "n3") : "n2"); } } });});submit(Portcullis, { size: [48, 48], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: 0.8, loop: true }, anchor: { x: 24, y: 45 }, variants: [ { name: "closed", args: ["closed"] }, { name: "raise", args: ["raise"], animate: { fps: 10, duration: 1.0, loop: false } }, { name: "raised", args: ["raised"] }, { name: "drop", args: ["drop"], animate: { fps: 10, duration: 0.6, 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.