Tessarune
Dungeon Mechanisms · orb_pedestal.js

Orb pedestal

32×48 · 241 lines of JavaScript

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.

Orb pedestal — Rendered output6×
Rendered by this program, variant “idle” · 32×48 · 12 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.

orb_pedestal.js · 1–9
// 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.

orb_pedestal.js · 230–241
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.js · 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.