Orb pedestal
A 32×48 floor prop: a carved stone column, with a stepped plinth, a round rune-cut shaft with a brass band and a flared dish with a shallow socket, under a floating orb of glass and light. The orb lights what faces it: the socket pool, the far wall of the dish and the rune channel in the shaft. “idle” (12 frames) bobs the orb 1px as its swirl turns and motes rise; “take” (8 frames, once) flares and lifts the orb, collapses it to a streak and bursts it into sparkles; “empty” (8 frames) is the bare socket. “idle-violet” and “idle-green” swap the glow.
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.
// Orb pedestal — 32x48 floor prop, top-down 3/4, key light top-left.// A carved stone column (stepped plinth, round rune-cut shaft with a brass band, a flared dish with a shallow socket)// under a floating glass-and-light orb. The orb lights what faces it: the socket pool, the far wall of the dish and// the rune channel in the shaft all take the glow ramp, so the colour variants re-light the stone too.// Variants:// idle loop, 12f — orb bobs 1px, two-arm swirl turns half a turn, socket pool breathes, motes rise// take one-shot, 8f — orb flares, lifts, collapses to a streak and bursts into sparkles; ends == empty f0// empty loop, 8f — bare socket, dead rune, a faint residual glint in the socket// idle-violet / idle-green — glow ramp swapped (no new colours)The last lines. submit declares the canvas and lists the variants the renderer exports.
submit(OrbPedestal, { size: [32, 48], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: N_IDLE / 10, loop: true }, anchor: { x: 16, y: GROUND }, variants: [ { name: "idle", args: ["idle"] }, { name: "take", args: ["take"], animate: { fps: 10, duration: N_TAKE / 10, loop: false } }, { name: "empty", args: ["empty"], animate: { fps: 10, duration: N_EMPTY / 10, loop: true } }, { name: "idle-violet", args: ["idle"], repalette: swap("glow", "v") }, { name: "idle-green", args: ["idle"], repalette: swap("glow", "e") }, ],});Show the whole program (241 lines)
// Orb pedestal — 32x48 floor prop, top-down 3/4, key light top-left.// A carved stone column (stepped plinth, round rune-cut shaft with a brass band, a flared dish with a shallow socket)// under a floating glass-and-light orb. The orb lights what faces it: the socket pool, the far wall of the dish and// the rune channel in the shaft all take the glow ramp, so the colour variants re-light the stone too.// Variants:// idle loop, 12f — orb bobs 1px, two-arm swirl turns half a turn, socket pool breathes, motes rise// take one-shot, 8f — orb flares, lifts, collapses to a streak and bursts into sparkles; ends == empty f0// empty loop, 8f — bare socket, dead rune, a faint residual glint in the socket// idle-violet / idle-green — glow ramp swapped (no new colours)const CX = 16, GROUND = 45;const N_IDLE = 12, N_TAKE = 8, N_EMPTY = 8;const ORB_R = 6.5, ORB_Y = 8; // orb centre (continuous coords) at bob 0const BOWL = { cx: 16, cy: 18, rx: 5, ry: 2 };const GL = ["glow0", "glow1", "glow2", "glow3"];const up = (k, n) => (k + n > 3 ? "n5" : GL[Math.max(0, k + n)]);// ---- stone -----------------------------------------------------------------------------------------------// a block seen top-down 3/4: lit top face, a lit front edge on the left, front face darkening to the rightfunction block(g, x0, x1, yTop, yFront, yBot, o = {}) { const w = x1 - x0; g.rect({ x: [x0, x1], y: [yTop, yFront - 1] }, "n3", { fill: "n3" }); if (o.farEdge !== false) g.line([x0 + 1, yTop], [x1 - 2, yTop], "n4"); g.line([x0, yFront - 1], [x0 + Math.round(w * 0.6), yFront - 1], "n4"); g.rect({ x: [x0, x1], y: [yFront, yBot] }, "n2", { fill: "n2" }); g.rect({ x: [x1 - Math.round(w * 0.22), x1], y: [yFront, yBot] }, "n1", { fill: "n1" }); g.line([x0, yFront], [x0, yBot], "n3"); if (yBot - yFront >= 2) g.line([x0 + 1, yBot], [x1, yBot], "n1");}function plinth(g) { g.part("plinth", () => { block(g, 5, 26, 40, 42, 44); // dentil carving along the front: countable notches for (let x = 8; x <= 23; x += 3) { g.pixel(x, 43, "n1"); g.pixel(x + 1, 43, x < 20 ? "n3" : "n2"); } g.line([24, 40], [25, 41], "n2"); // chipped corner g.pixel(10, 41, "n2"); g.pixel(11, 41, "n2"); // crack on the step g.line([6, 41], [8, 41], "e1"); g.pixel(7, 40, "e2"); g.pixel(6, 42, "e1"); g.pixel(5, 43, "e0"); // moss in the corner block(g, 8, 23, 36, 38, 39); });}function shaft(g) { // round column: cylinder shading across its width, a collar under the dish and a torus at the foot const R = (y) => (y <= 26 ? 3.5 : y <= 32 ? 3.5 : y === 33 ? 4.5 : 5.5); g.part("shaft", () => { g.field({ x: [9, 22], y: [24, 35] }, (x, y) => { const r = R(y), u = (x + 0.5 - CX) / r; if (Math.abs(u) > 1) return null; if (y === 33 || y === 34) return u < -0.5 ? "n4" : u < 0.1 ? "n3" : u < 0.6 ? "n2" : "n1"; // torus if (y === 35) return u < 0.4 ? "n1" : "n0"; if (y === 26) return u < -0.6 ? "g3" : u < 0 ? "g2" : u < 0.6 ? "g1" : "g0"; // brass band if (y === 27) return u < 0.3 ? "n1" : "n0"; // its shadow return u < -0.6 ? "n3" : u < 0.05 ? "n2" : u < 0.6 ? "n1" : "n0"; }); });}function rune(g, lvl) { // a carved channel down the column's face; its light is the orb's (lvl -1 = dead) g.part("rune", () => { const c = lvl < 0 ? "n0" : GL[Math.min(3, lvl)], d = lvl < 0 ? "n1" : GL[Math.max(0, lvl - 1)]; g.line([15, 28], [15, 32], c); g.pixel(14, 29, d); g.pixel(16, 29, d); g.pixel(14, 31, d); g.pixel(16, 31, d); if (lvl >= 2) g.pixel(15, 30, up(lvl, 1)); if (lvl === 0) g.pixel(15, 30, "glow1"); // a last ember of light in the cut });}// dish: a flared stone cup, top ellipse centred at DISH.cyconst DISH = { cy: 19.5, rx: 9, ry: 3, irx: 7, iry: 2 };const dishR = (y) => (y < 22 ? 9 : y === 22 ? 8 : y === 23 ? 6.5 : y === 24 ? 5 : 4.5);function dish(g) { g.part("dish", () => { g.field({ x: [6, 25], y: [16, 25] }, (x, y) => { const nx = (x + 0.5 - CX) / DISH.rx, ny = (y + 0.5 - DISH.cy) / DISH.ry; if (nx * nx + ny * ny <= 1) return null; // top face drawn below if (y + 0.5 < DISH.cy) return null; const r = dishR(y), u = (x + 0.5 - CX) / r; if (Math.abs(u) > 1) return null; if (y === 22 && Math.abs(u) < 0.95) return u < -0.3 ? "n2" : "n1"; // bead under the lip if (y >= 23 && (x === 13 || x === 18)) return x === 13 ? "n1" : "n0"; // flutes return u < -0.55 ? "n3" : u < 0.1 ? "n2" : u < 0.6 ? "n1" : "n0"; }); g.field({ x: [6, 25], y: [16, 23] }, (x, y) => { const nx = (x + 0.5 - CX) / DISH.rx, ny = (y + 0.5 - DISH.cy) / DISH.ry; if (nx * nx + ny * ny > 1) return null; const ix = (x + 0.5 - CX) / DISH.irx, iy = (y + 0.5 - (DISH.cy - 0.3)) / DISH.iry; if (ix * ix + iy * iy <= 1) return null; // the hollow, drawn by socket() const a = -0.7 * nx - 0.35 * ny; return a > 0.4 ? "n4" : a > -0.1 ? "n3" : a > -0.55 ? "n2" : "n1"; }); });}function socket(g, pool) { // the hollow of the dish; pool = light level of the orb reflected in it (-1 = dark) const cy = DISH.cy - 0.3, rx = DISH.irx, ry = DISH.iry; g.part("socket", () => g.field({ x: [CX - rx, CX + rx - 1], y: [Math.floor(cy - ry), Math.ceil(cy + ry)] }, (x, y) => { const nx = (x + 0.5 - CX) / rx, ny = (y + 0.5 - cy) / ry; const r = nx * nx + ny * ny; if (r > 1) return null; if (ny < -0.3 || r > 0.75) { // inner wall; right side lit if (pool >= 2 && ny < -0.3 && Math.abs(nx) < 0.4) return "glow1"; // orb light on the far wall return nx > 0.45 ? "n2" : nx > -0.2 ? "n1" : "n0"; } if (pool < 0) return "n0"; const k = pool - (Math.abs(nx) > 0.45 ? 1 : 0); return k < 0 ? "n0" : GL[Math.min(3, k)]; }));}// ---- orb ---------------------------------------------------------------------------------------------------function orbTone(nx, ny, t, lvl) { const r = Math.hypot(nx, ny); if (r > 1) return null; const sx = nx + 0.45, sy = ny + 0.45; if (sx * sx * 1.4 + sy * sy < 0.03) return "n5"; // window specular, upper left if (r > 0.86) return up(0, lvl); // dark rim of the glass const d = 0.5 * nx + 0.86 * ny; // light leaving the far (lower) side if (r > 0.64 && d > 0.62) return up(3, lvl); const cx = nx - 0.05, cy = ny - 0.05; if (cx * cx + cy * cy < 0.06) return up(3, lvl); // hot core const th = Math.atan2(ny, nx); const m = Math.cos(2 * th + 4.6 * r - TAU * t); // two arms, half a turn per cycle if (r > 0.22 && m > 0.5) return up(2, lvl); if (d < -0.55) return up(0, lvl); // upper glass reflects the dark room return up(1, lvl);}function orb(g, cx, cy, R, t, lvl) { g.detached("orb", () => g.outlined("ink", () => g.field({ x: [Math.floor(cx - R), Math.ceil(cx + R)], y: [Math.floor(cy - R), Math.ceil(cy + R)] }, (x, y) => orbTone((x + 0.5 - cx) / R, (y + 0.5 - cy) / R, t, lvl))));}function motes(g, t, n) { // sparks of light leave the dish rim and drift up beside the orb (head + fading tail) g.detached("motes", () => { for (let i = 0; i < n; i++) { const p = (t + i / n) % 1; const side = i % 2 ? 1 : -1; const x = Math.round(CX - 0.5 + side * (8.5 - p * 2.5)); const y = 17 - Math.round(p * 14); if (p > 0.8) continue; g.pixel(x, y, p < 0.45 ? "glow3" : "glow2"); if (p < 0.6) g.pixel(x, y + 1, p < 0.3 ? "glow2" : "glow1"); } });}function sparkle(g, x, y, s, c) { // s = arm length 0..2 ; a 4-point star g.pixel(x, y, s > 0 ? "n5" : c); for (let i = 1; i <= s; i++) { const k = i === s ? c : "n5"; g.pixel(x - i, y, k); g.pixel(x + i, y, k); g.pixel(x, y - i, k); g.pixel(x, y + i, k); }}// ---- scene ------------------------------------------------------------------------------------------------function pedestal(g, s) { contactShadow(g, CX, GROUND, 12, 2); g.outlined("ink", () => { plinth(g); shaft(g); dish(g); }); rune(g, s.rune); socket(g, s.pool);}const OrbPedestal = sprite(pal, (g, t, state) => { if (state === "idle") { const bob = Math.round(0.5 - 0.5 * Math.cos(TAU * t)); const breathe = bob ? 3 : 2; pedestal(g, { rune: 2 + (t > 0.3 && t < 0.7 ? 1 : 0) - (t < 0.1 || t > 0.9 ? 1 : 0), pool: breathe }); orb(g, CX, ORB_Y + bob, ORB_R, t, 0); motes(g, t, 3); return; } if (state === "take") { const f = Math.min(N_TAKE - 1, Math.round(t * N_TAKE)); // f: 0 idle | 1 flare | 2 lift+brighter | 3 high, shrinking | 4 streak | 5 burst | 6 fading | 7 == empty f0 const KEYS = [ { rune: 1, pool: 2 }, { rune: 3, pool: 3 }, { rune: 3, pool: 3 }, { rune: 2, pool: 2 }, { rune: 1, pool: 1 }, { rune: 0, pool: 0 }, { rune: 0, pool: -1 }, ]; if (f === N_TAKE - 1) { drawEmpty(g, 0); return; } pedestal(g, KEYS[f]); if (f === 0) { orb(g, CX, ORB_Y, ORB_R, 0, 0); motes(g, 0, 3); return; } g.detached("flare", () => { if (f === 1) { orb(g, CX, ORB_Y, ORB_R, 0.1, 1); for (const [x, y] of [[8, 3], [24, 5], [10, 15], [23, 13]]) sparkle(g, x, y, 1, "glow2"); } if (f === 2) { orb(g, CX, ORB_Y - 2, 5.5, 0.2, 2); g.line([15, 13], [15, 15], "glow2"); for (const [x, y] of [[6, 2], [26, 3], [7, 12], [25, 10]]) sparkle(g, x, y, 1, "glow3"); } if (f === 3) { orb(g, CX, 4, 3.5, 0.3, 2); sparkle(g, 15, 0, 1, "glow2"); g.line([15, 9], [15, 13], "glow2"); g.line([16, 9], [16, 12], "glow1"); } if (f === 4) { g.line([15, 0], [15, 6], "n5"); g.line([16, 1], [16, 5], "glow3"); g.line([14, 2], [14, 4], "glow2"); g.line([15, 7], [15, 11], "glow2"); g.pixel(15, 13, "glow1"); for (const [x, y] of [[10, 4], [21, 3], [12, 9], [19, 8]]) g.pixel(x, y, "glow3"); } if (f === 5) { for (const [x, y, s] of [[15, 1, 1], [9, 3, 1], [22, 2, 1], [12, 8, 0], [19, 7, 0], [6, 8, 0], [25, 7, 0], [16, 5, 0]]) sparkle(g, x, y, s, s ? "glow2" : "glow3"); } if (f === 6) { for (const [x, y, c] of [[8, 7, "glow2"], [23, 6, "glow2"], [5, 13, "glow2"], [26, 12, "glow3"], [14, 4, "glow3"]]) g.pixel(x, y, c); } }); return; } drawEmpty(g, t);});function drawEmpty(g, t) { const f = Math.round(t * N_EMPTY) % N_EMPTY; pedestal(g, { rune: f === 4 || f === 5 ? 0 : -1, pool: -1 }); // residual glint: an ember of light left in the socket, flickering on then a small star on the lit lip g.detached("glint", () => { const e = [0, 1, 1, 2, 3, 2, 1, 0][f]; if (e > 0) g.pixel(17, 19, GL[e]); if (e > 2) g.pixel(16, 19, "glow1"); if (f === 5) sparkle(g, 12, 19, 1, "n4"); if (f === 6) g.pixel(12, 19, "n5"); });}submit(OrbPedestal, { size: [32, 48], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: N_IDLE / 10, loop: true }, anchor: { x: 16, y: GROUND }, variants: [ { name: "idle", args: ["idle"] }, { name: "take", args: ["take"], animate: { fps: 10, duration: N_TAKE / 10, loop: false } }, { name: "empty", args: ["empty"], animate: { fps: 10, duration: N_EMPTY / 10, loop: true } }, { name: "idle-violet", args: ["idle"], repalette: swap("glow", "v") }, { name: "idle-green", args: ["idle"], repalette: swap("glow", "e") }, ],});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.