Tavern table
A 48×48 floor prop in top-down 3/4: a round plank table on a turned pedestal with three splayed feet and a three-legged stool tucked in at each side. On top are a tallow candle in a brass dish, a plate with a bread roll and cheese, and two dark-stave tankards with brass bands and foaming heads; the candle pools warm light on the planks. “idle” (12 frames, loop) flickers the candle, breathes its light pool and bubbles the foam; “clink” (8 frames, once) lifts the tankards off the table, leaving their shadows behind, leans them in to clink with a splash of foam and sets them back down.
6×
Get the animated sprites: Tavern & Market on itch.ioVol.3 · 12 tavern and market props · 32 animations
The header. The comment at the top of the file is the brief: size, view, light direction and every variant.
// Tavern table — 48x48 floor prop, top-down 3/4, key light top-left.// A round plank table on a turned pedestal with three splayed feet, a three-legged stool tucked in at each side.// On the top: a tallow candle in a brass dish (back), a plate with a bread roll and cheese (front), and two dark-stave tankards with brass bands and foaming heads (front left / front right).// The candle pools warm light on the planks around it.// Variants:// idle loop 12f — the candle flickers and its light pool breathes with it, the foam heads bubble and settle// clink one-shot 8f — the tankards lift off the table (their shadows stay behind), lean in and clink, a splash of// foam flies up, then they set back down; the last frame == idle frame 0The last lines. submit declares the canvas and lists the variants the renderer exports.
submit(Table, { size: [W, H], origin: "top-left", kind: "icon", scale: 1, anchor: { x: CX, y: GROUND }, variants: [ { name: "idle", args: ["idle"], animate: { fps: 10, duration: N_IDLE / 10, loop: true } }, { name: "clink", args: ["clink"], animate: { fps: 10, duration: N_CLINK / 10, loop: false } }, ],});Show the whole program (293 lines)
// Tavern table — 48x48 floor prop, top-down 3/4, key light top-left.// A round plank table on a turned pedestal with three splayed feet, a three-legged stool tucked in at each side.// On the top: a tallow candle in a brass dish (back), a plate with a bread roll and cheese (front), and two dark-stave tankards with brass bands and foaming heads (front left / front right).// The candle pools warm light on the planks around it.// Variants:// idle loop 12f — the candle flickers and its light pool breathes with it, the foam heads bubble and settle// clink one-shot 8f — the tankards lift off the table (their shadows stay behind), lean in and clink, a splash of// foam flies up, then they set back down; the last frame == idle frame 0const W = 48, H = 48, CX = 24, GROUND = 45;const N_IDLE = 12, N_CLINK = 8;const TOP = { cx: 24, cy: 24, rx: 16, ry: 7.5 };const CANDLE = { x: 24, top: 12, base: 19 }; // wax column x-1..x+1, dish under baseconst PLATE = { cx: 24, cy: 28.5 };const MUG_L = { x0: 14, yb: 27, side: -1, seed: 0.0 };const MUG_R = { x0: 29, yb: 27, side: 1, seed: 0.45 };const WOOD = ["w0", "w1", "w2", "w3", "w4"];// ---- candle flame (the skull_candles idiom: odd-width rows on a staircase axis that bends with lean) --------function flick(t, s) { const dh = Math.round(0.75 * Math.sin(TAU * (2 * t + s)) + 0.45 * Math.sin(TAU * (3 * t + 2.3 * s))); const lean = 0.7 * Math.sin(TAU * (t + s)) + 0.45 * Math.sin(TAU * (3 * t + 1.7 * s)); return { dh, lean };}function candleFlame(g, fx, fy, h, lean) { for (let i = 0; i < h; i++) { const k = (i + 0.5) / h, y = fy - i; const ax = fx + Math.round(lean * k * k); if (k > 0.12 && k < 0.62) { const edge = k > 0.18 && k < 0.5 ? "fl1" : "fl0"; g.pixel(ax - 1, y, edge); g.pixel(ax + 1, y, edge); } g.pixel(ax, y, k < 0.12 ? "fl1" : k < 0.5 ? "fl3" : k < 0.68 ? "fl2" : k < 0.86 ? "fl1" : "fl0"); }}// ---- stools ------------------------------------------------------------------------------------------------function stool(g, name, cx) { const cy = 35, rx = 4.6, ry = 1.7; g.part(name, () => { // three legs: left and right splay out, the back one peeks out between them g.line([cx, 37], [cx, 42], "w0"); g.line([cx - 4, 36], [cx - 5, 44], "w2"); g.line([cx + 4, 36], [cx + 5, 44], "w1"); g.line([cx - 4, 40], [cx + 4, 40], "w1"); // stretcher rung g.pixel(cx - 3, 40, "w2"); // seat: a flat round slab — a lit top face and a two-row edge band g.field({ x: [cx - 5, cx + 5], y: [cy - 2, cy + 4] }, (x, y) => { const nx = (x + 0.5 - cx) / rx; if (Math.abs(nx) > 1) return null; const s = Math.sqrt(1 - nx * nx); if (y + 0.5 < cy - ry * s || y + 0.5 > cy + ry * s + 2) return null; if (y + 0.5 > cy + ry * s) return y + 0.5 > cy + ry * s + 1 ? (nx < -0.3 ? "w1" : "w0") : nx < -0.4 ? "w2" : "w1"; const ny = (y + 0.5 - cy) / ry; return -0.6 * nx - 0.5 * ny > 0.5 ? "w4" : nx > 0.45 ? "w2" : "w3"; }); });}// ---- table -------------------------------------------------------------------------------------------------function pedestal(g) { g.part("pedestal", () => { // three splayed feet g.fillPoly([[22, 39], [16, 42], [15, 44], [22, 42]], "w2"); g.line([22, 39], [16, 42], "w3"); g.fillPoly([[26, 39], [32, 42], [33, 44], [26, 42]], "w1"); g.line([26, 39], [32, 42], "w2"); g.rect({ x: [23, 25], y: [40, 45] }, "w2", { fill: "w2" }); g.line([23, 40], [23, 45], "w3"); g.line([25, 41], [25, 45], "w1"); // turned column with a bead at mid height, in the table's shadow at the top g.field({ x: [21, 27], y: [32, 40] }, (x, y) => { const hw = y === 35 || y === 36 ? 3 : 2; const dx = x - 24; if (dx < -hw || dx > hw) return null; const u = (dx + hw) / (2 * hw); if (y <= 33) return u < 0.45 ? "w1" : "w0"; if (y === 36) return u < 0.3 ? "w2" : "w1"; // underside of the bead return u < 0.2 ? "w4" : u < 0.45 ? "w3" : u < 0.8 ? "w2" : "w1"; }); });}function poolAt(x, y, R) { // candle light on the planks: a hot core, a solid ring, a checker-dithered rim const d = Math.hypot(x + 0.5 - CANDLE.x, (y + 0.5 - CANDLE.base - 1) * 2.1); return d < R * 0.45 ? 2 : d < R - 1.4 ? 1 : d < R && chk(x, y) ? 1 : 0;}function tableTop(g, R) { const { cx, cy, rx, ry } = TOP; g.part("tabletop", () => { g.field({ x: [cx - rx, cx + rx], y: [cy - ry, cy + ry + 2] }, (x, y) => { const nx = (x + 0.5 - cx) / rx; if (Math.abs(nx) > 1) return null; const s = Math.sqrt(1 - nx * nx); if (y + 0.5 < cy - ry * s || y + 0.5 > cy + ry * s + 2) return null; const ny = (y + 0.5 - cy) / ry; if (nx * nx + ny * ny <= 1) { // the top: planks running front to back, the top-left rim catches the key light let k = nx > 0.5 ? 2 : 3; const rim = -0.62 * nx - 0.5 * ny > 0.66; if (rim) k = 4; const seam = (x + 48) % 5 === 1 && Math.abs(ny) < 0.93 && !rim; if (seam) return nx > 0.5 ? "w1" : poolAt(x, y, R) ? "w3" : "w2"; return WOOD[Math.min(4, k + poolAt(x, y, R))]; } return y + 0.5 > cy + ry * s + 1 ? (nx < -0.4 ? "w1" : "w0") : nx < -0.5 ? "w2" : "w1"; // near edge }); });}// ---- things on the table -----------------------------------------------------------------------------------// g.outlined only inks where the neighbour is empty, so an object standing ON the table would melt into the// planks. Table objects are drawn into a small layer first, then emitted with their own ink ring.function layer() { const m = new Map(); const put = (x, y, k) => { if (x >= 0 && y >= 0 && x < W && y < H) m.set(y * W + x, k); }; return { m, put };}function emit(g, name, m, ringKey, lowOnly) { const ring = new Map(); let x0 = W, x1 = -1, y0 = H, y1 = -1; const grow = (x, y) => { x0 = Math.min(x0, x); x1 = Math.max(x1, x); y0 = Math.min(y0, y); y1 = Math.max(y1, y); }; for (const i of m.keys()) { const x = i % W, y = (i - x) / W; grow(x, y); const nb = lowOnly ? [[x + 1, y], [x, y + 1]] : [[x + 1, y], [x - 1, y], [x, y + 1], [x, y - 1]]; for (const [nx, ny] of nb) { if (nx < 0 || ny < 0 || nx >= W || ny >= H || m.has(ny * W + nx)) continue; ring.set(ny * W + nx, ringKey); grow(nx, ny); } } if (x1 < 0) return; g.part(name, () => g.field({ x: [x0, x1], y: [y0, y1] }, (x, y) => m.get(y * W + x) ?? ring.get(y * W + x) ?? null));}function candle(g, t) { const { x, top, base } = CANDLE; const L = layer(), put = L.put; // brass dish: a shallow ellipse, lit rim at the back-left for (let yy = base - 1; yy <= base + 1; yy++) for (let xx = x - 3; xx <= x + 3; xx++) { const nx = (xx + 0.5 - x - 0.5) / 3.5, ny = (yy + 0.5 - base - 0.5) / 1.5; if (nx * nx + ny * ny > 1) continue; put(xx, yy, yy === base - 1 ? (xx < x ? "g3" : "g2") : yy === base + 1 ? (xx < x ? "g1" : "g0") : xx < x - 1 ? "g2" : "g1"); } // tallow column: lit left, dimmer near the dish, one fat drip on the lit side const cols = ["n5", "w4", "w3"]; for (let i = 0; i < 3; i++) for (let y = top + 1; y <= base - 1; y++) put(x - 1 + i, y, y > base - 3 && i > 0 ? "w3" : cols[i]); put(x - 1, top, "n5"); put(x, top, "n5"); put(x + 1, top, "w4"); put(x, top + 1, "fl3"); // flame light through the tallow for (let y = top + 2; y <= top + 4; y++) put(x - 2, y, "n5"); put(x - 2, top + 5, "w4"); emit(g, "candle", L.m, "ink"); const f = flick(t, 0.2); g.part("flame", () => { g.pixel(x, top - 1, "ink"); candleFlame(g, x, top - 2, 6 + f.dh, f.lean); });}function plate(g) { const { cx, cy } = PLATE; const P = layer(); for (let y = Math.floor(cy - 2); y <= cy + 2; y++) for (let x = cx - 6; x <= cx + 6; x++) { const nx = (x + 0.5 - cx) / 5.6, ny = (y + 0.5 - cy) / 2.1; const r = nx * nx + ny * ny; if (r > 1) continue; P.put(x, y, r > 0.55 ? (ny > 0.3 ? "n3" : nx < 0 ? "n5" : "n4") : nx < 0.45 ? "n5" : "n4"); // rim / well, lit on the left } emit(g, "plate", P.m, "ink"); // a bread roll (left) and a wedge of cheese (right); they cast a grey shadow on the plate const F = layer(), put = F.put; for (const [x, y, k] of [ [21, 26, "w4"], [22, 26, "w3"], [20, 27, "w4"], [21, 27, "w3"], [22, 27, "w2"], [23, 27, "w3"], [20, 28, "w3"], [21, 28, "w2"], [22, 28, "w3"], [23, 28, "w2"], [26, 27, "g3"], [27, 27, "g2"], [25, 28, "g3"], [26, 28, "g2"], [27, 28, "g1"], ]) put(x, y, k); emit(g, "food", F.m, "n3", true);}// a dark-stave tankard with brass bands. x0 = left column of the body, yb = bottom row, side = handle side,// tilt leans the top toward +x (px at the rim), dome = extra foam height (0..1), bubble = which bubble showsfunction mug(g, name, m, dx, dy, tilt, dome, bubble) { const x0 = m.x0 + dx, yb = m.yb + dy, side = m.side; const sh = (y) => Math.round((tilt * (yb - y)) / 8); const BODY = ["w2", "w3", "w2", "w1", "w0"]; const BAND = ["g2", "g3", "g2", "g1", "g0"]; const L = layer(); const px = (c, y, k) => L.put(x0 + c + sh(y), y, k); // handle (outer side): a C loop; its hole is left empty so the ring fills it with ink const hc = side < 0 ? -1 : 5, oc = side < 0 ? -2 : 6; const lit = side < 0 ? ["w3", "w2"] : ["w1", "w0"]; px(hc, yb - 5, lit[0]); px(oc, yb - 4, lit[0]); px(oc, yb - 3, lit[1]); px(hc, yb - 2, lit[1]); // body for (let y = yb - 6; y <= yb; y++) for (let c = 0; c < 5; c++) { let k = y === yb - 5 || y === yb - 1 ? BAND[c] : BODY[c]; if (y === yb) k = c < 2 ? "w1" : "w0"; px(c, y, k); } // foam head: a shaded lip at the rim, a lit top, a dome; one spill + drip down the lit side for (let c = 0; c < 5; c++) px(c, yb - 7, c === 0 ? "n4" : c < 3 ? "n3" : "n2"); for (let c = 0; c < 5; c++) px(c, yb - 8, c < 2 ? "n5" : c < 4 ? "n4" : "n3"); for (let c = 1; c < 4; c++) px(c, yb - 9, c < 3 ? "n5" : "n4"); if (dome) { px(1, yb - 10, "n5"); px(2, yb - 10, "n4"); } px(-1, yb - 8, "n4"); px(-1, yb - 7, "n5"); px(1, yb - 6, "n5"); px(1, yb - 5, "n4"); // spill and drip if (bubble >= 0) { const [bc, br] = [[2, 9], [3, 8], [1, 8]][bubble]; px(bc, yb - br, "n3"); } emit(g, name, L.m, "ink");}function mugShadow(g, m, dx, dy) { // the tankard's shadow stays on the planks and shrinks as it lifts away g.part("mugShadow", () => { const x0 = m.x0 + dx, y = m.yb + 1; if (dy === 0) { g.line([x0, y], [x0 + 5, y], "w0"); return; } const w = dy > -5 ? 4 : 2; for (let x = x0 + 2 - (w >> 1); x <= x0 + 2 + (w >> 1); x++) { g.pixel(x, y - 1, "w1"); if (chk(x, y)) g.pixel(x, y, "w0"); } });}// ---- sprite ------------------------------------------------------------------------------------------------function foamState(t, m) { const p = (t + m.seed) % 1; return { dome: p < 0.5 ? 1 : 0, bubble: p < 0.17 ? 0 : p < 0.34 ? 1 : p > 0.67 && p < 0.84 ? 2 : -1 };}function scene(g, t, pose) { contactShadow(g, CX, GROUND, 11, 1); contactShadow(g, 8, GROUND, 5, 1); contactShadow(g, 40, GROUND, 5, 1); g.outlined("ink", () => stool(g, "stoolL", 8)); g.outlined("ink", () => stool(g, "stoolR", 40)); g.outlined("ink", () => pedestal(g)); const f = flick(t, 0.2); g.outlined("ink", () => tableTop(g, 7.5 + 0.9 * f.dh)); mugShadow(g, MUG_L, 0, pose.L[1]); mugShadow(g, MUG_R, 0, pose.R[1]); plate(g); candle(g, t); const fl = foamState(t, MUG_L), fr = foamState(t, MUG_R); const still = pose.dome !== undefined; mug(g, "mugL", MUG_L, pose.L[0], pose.L[1], pose.L[2], still ? pose.dome : fl.dome, still ? -1 : fl.bubble); mug(g, "mugR", MUG_R, pose.R[0], pose.R[1], pose.R[2], still ? pose.dome : fr.dome, still ? -1 : fr.bubble);}const REST = { L: [0, 0, 0], R: [0, 0, 0] };// clink keys: [dx, dy, tilt] per mugconst CLINK = [ null, { L: [1, -3, 0], R: [-1, -3, 0] }, { L: [3, -7, 1], R: [-3, -7, -1] }, { L: [5, -9, 2], R: [-5, -9, -2], dome: 0 }, // clink { L: [4, -10, 1], R: [-4, -10, -1], dome: 1 }, // rebound, foam in the air { L: [2, -6, 0], R: [-2, -6, 0], dome: 1 }, { L: [0, -2, 0], R: [0, -2, 0] }, null,];function splash(g, f) { const cx = 24, cy = 7; // foam: small clumps (a cream pixel with a grey underside) bursting up from the rims, then falling const CLUMPS = { 3: [[0, -1], [-2, -1], [2, -2], [-1, -3], [1, -4]], 4: [[0, -6], [-3, -5], [3, -4], [-5, -2], [5, -3], [-1, -3], [2, -1]], 5: [[-4, -4], [4, -3], [-7, 1], [7, 0], [0, -5]], 6: [[-8, 5], [8, 4], [-1, 0]], }[f]; if (!CLUMPS) return; g.detached("splash", () => { CLUMPS.forEach(([dx, dy], i) => { const x = cx + dx, y = cy + dy; g.pixel(x, y, "n5"); if (f <= 4 && i % 2 === 0) { g.pixel(x + 1, y, "n5"); g.pixel(x + 1, y + 1, "n3"); } g.pixel(x, y + 1, f >= 5 ? "n3" : "n4"); }); });}function clinkStar(g) { g.detached("clinkStar", () => { for (const [x, y, k] of [[24, 5, "g3"], [24, 4, "n5"], [17, 10, "g3"], [31, 10, "g3"], [16, 10, "g2"], [32, 10, "g2"], [20, 6, "g2"], [28, 6, "g2"]]) g.pixel(x, y, k); });}const Table = sprite(pal, (g, t, mode) => { if (mode === "idle") { scene(g, t, REST); return; } const f = Math.min(N_CLINK - 1, Math.round(t * N_CLINK)); const pose = CLINK[f]; if (!pose) { scene(g, 0, REST); return; } scene(g, 0, pose); if (f === 3) clinkStar(g); splash(g, f);});submit(Table, { size: [W, H], origin: "top-left", kind: "icon", scale: 1, anchor: { x: CX, y: GROUND }, variants: [ { name: "idle", args: ["idle"], animate: { fps: 10, duration: N_IDLE / 10, loop: true } }, { name: "clink", args: ["clink"], animate: { fps: 10, duration: N_CLINK / 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.