Dungeon Mechanisms · pressure_plate.js
感圧板
32×32 の床の罠で、4 枚の切石を組んだ枠の受け口に、枠より 2px 高い暗いスレートの板が収まり、紋(輪の中に algiz の字)が彫られています。up(8 コマ)は紋の溝がかすかに脈打ちます。press(6 コマの一回再生)は板が沈んで枠と面一になり、すき間から土ぼこりが噴き、紋が光の柱になって光ります。down(8 コマ)は紋が光り続け、輪を光が回り、光の粒が昇ります。release(6 コマの一回再生)は板が跳ね上がって戻ります。down-violet は光を紫に差し替えたものです。
6×
動くスプライト素材: Dungeon Mechanisms(itch.io)Vol.2・罠と扉と壊れる物 12 点
冒頭のコメント。これが指示書です。サイズ、視点、光の向き、すべての変種が書かれています。
pressure_plate.js · 1–10
// Pressure plate — 32x32 floor trap, top-down 3/4, key light top-left.// A dressed-stone frame (four mitred slabs) around a socket; a darker slate plate sits in the socket, 2px proud of// the frame, with a carved rune (a ring round an algiz stroke). Stepping on it sinks the plate flush: the plate's// front face disappears, its top drops 2px and the dark back gap of the socket opens behind it.// Variants:// up loop 8f — plate raised, the rune's grooves faintly pulse// press one-shot 6f — plate sinks 2px, dust squirts from the gaps, the rune flares (light pillar) -> == down f0// down loop 8f — plate flush, rune glowing steadily, a glint runs round the ring, motes rise// release one-shot 6f — rune dims, plate springs up 1px past rest and settles -> == up f0// down-violet — the glow ramp swapped to violet最後の数行。submit でキャンバスの大きさと、描画エンジンが書き出す変種の一覧を指定します。
pressure_plate.js · 236–247
submit(Plate, { size: [32, 32], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: 0.8, loop: true }, anchor: { x: 16, y: GROUND }, variants: [ { name: "up", args: ["up"] }, { name: "press", args: ["press"], animate: { fps: 10, duration: 0.6, loop: false } }, { name: "down", args: ["down"] }, { name: "release", args: ["release"], animate: { fps: 10, duration: 0.6, loop: false } }, { name: "down-violet", args: ["down"], repalette: swap("glow", "v") }, ],});プログラム全体を表示 (247 行)
pressure_plate.js · 247 行
// Pressure plate — 32x32 floor trap, top-down 3/4, key light top-left.// A dressed-stone frame (four mitred slabs) around a socket; a darker slate plate sits in the socket, 2px proud of// the frame, with a carved rune (a ring round an algiz stroke). Stepping on it sinks the plate flush: the plate's// front face disappears, its top drops 2px and the dark back gap of the socket opens behind it.// Variants:// up loop 8f — plate raised, the rune's grooves faintly pulse// press one-shot 6f — plate sinks 2px, dust squirts from the gaps, the rune flares (light pillar) -> == down f0// down loop 8f — plate flush, rune glowing steadily, a glint runs round the ring, motes rise// release one-shot 6f — rune dims, plate springs up 1px past rest and settles -> == up f0// down-violet — the glow ramp swapped to violetconst W = 32, GROUND = 29;const FR = { x0: 1, x1: 30, y0: 6, y1: 26, face: 2 }; // frame top face rows y0..y1, front face below itconst OP = { x0: 5, x1: 26, y0: 9, y1: 23 }; // socket opening on the frame's top faceconst PL = { x0: 6, x1: 25, y0: 10, y1: 23 }; // plate footprint (flush position)const RY = 2; // rune row offset inside the plate's top face// the rune: o = ring, a = diamond, * = core (18 x 9 grid; symmetric about the plate centre)const RUNE = [ "......oooooo......", "....oo......oo....", "...o....aa....o...", "..o....a**a....o..", "o.o...a****a...o.o", "..o....a**a....o..", "...o....aa....o...", "....oo......oo....", "......oooooo......",];const GY = 0; // grid row offset of RUNE[0]const RING = []; // ring pixels in angular order, for the glint{ RUNE.forEach((row, y) => [...row].forEach((c, x) => { if (c === "o") RING.push([x, y, Math.atan2((y - 4) * 1.6, x - 8.5)]); })); RING.sort((a, b) => a[2] - b[2]);}// ---- one frame into a key buffer: {part, key}function build(h, rune) { const B = new Array(W * W).fill(null); const set = (x, y, part, key) => { if (x >= 0 && y >= 0 && x < W && y < W) B[y * W + x] = { part, key }; }; // frame: a ring of dressed floor stones, top face then a 2px front face for (let y = FR.y0; y <= FR.y1; y++) for (let x = FR.x0; x <= FR.x1; x++) { if ((x === FR.x0 || x === FR.x1) && y === FR.y0) continue; // chamfered back corners if (x >= OP.x0 && x <= OP.x1 && y >= OP.y0 && y <= OP.y1) { // the socket: its back wall (faces the camera) is a dim row, the rest a black gap set(x, y, "socket", y === OP.y0 ? "n0" : "ink"); continue; } let k = "n2"; if (y === FR.y0 || x === FR.x0 || (y === FR.y0 + 1 && (x === FR.x0 + 1))) k = "n3"; // outer north / west edges lit if (x === FR.x1 && y > FR.y0) k = "n1"; // outer east edge if (y === OP.y1 + 1 && x >= OP.x0 && x <= OP.x1) k = "n3"; // front rim's inner lip faces north if (x === OP.x1 + 1 && y >= OP.y0 && y <= OP.y1) k = "n3"; // right rim's inner lip faces west if (y === OP.y0 - 1 && x >= OP.x0 - 1 && x <= OP.x1) k = "n1"; // back rim's inner lip faces south if (x === OP.x0 - 1 && y >= OP.y0 - 1 && y <= OP.y1) k = "n1"; // left rim's inner lip faces east set(x, y, "frame", k); } // block joints across the rims (dark joint, the next stone's lit edge beside it) const jointV = (x, y0, y1) => { for (let y = y0; y <= y1; y++) { set(x, y, "frame", "n1"); if (B[y * W + x + 1] && B[y * W + x + 1].key === "n2") set(x + 1, y, "frame", "n3"); } }; const jointH = (y, x0, x1) => { for (let x = x0; x <= x1; x++) { set(x, y, "frame", "n1"); if (B[(y + 1) * W + x] && B[(y + 1) * W + x].key === "n2") set(x, y + 1, "frame", "n3"); } }; jointV(11, FR.y0 + 1, OP.y0 - 2); jointV(21, FR.y0 + 1, OP.y0 - 2); jointV(9, OP.y1 + 2, FR.y1); jointV(18, OP.y1 + 2, FR.y1); jointV(25, OP.y1 + 2, FR.y1); jointH(15, FR.x0 + 1, OP.x0 - 2); jointH(18, OP.x1 + 2, FR.x1 - 1); // chips (countable marks) set(3, 21, "frame", "n1"); set(4, 21, "frame", "n3"); set(28, 11, "frame", "n1"); set(28, 12, "frame", "n3"); set(14, 25, "frame", "n1"); set(15, 25, "frame", "n1"); // frame front face for (let i = 1; i <= FR.face; i++) for (let x = FR.x0; x <= FR.x1; x++) { if ((x === FR.x0 || x === FR.x1) && i === FR.face) continue; // chamfered front corners let k = i === 1 ? "n1" : "n0"; if (x === FR.x0 + (i === FR.face ? 1 : 0)) k = i === 1 ? "n2" : "n1"; if (x === 9 || x === 18 || x === 25) k = "ink"; // the joints run down the face set(x, FR.y1 + i, "frame", k); } // plate: top face shifted up by h, front face h rows tall (inside the socket, so its sides never show) const top = PL.y0 - h, bot = PL.y1 - h; for (let y = top; y <= bot; y++) for (let x = PL.x0; x <= PL.x1; x++) { let k = "n3"; if (x === PL.x1) k = "n2"; if (x === PL.x0 || y === top) k = "n4"; if (y === top && x === PL.x1) k = "n3"; // carved corner brackets one pixel in const cx = x - PL.x0 <= 3 ? x - PL.x0 : PL.x1 - x <= 3 ? PL.x1 - x : -1; const cy = y - top <= 3 ? y - top : bot - y <= 3 ? bot - y : -1; if (cx >= 1 && cy >= 1 && ((cx === 1 && cy <= 3) || (cy === 1 && cx <= 3))) k = "n2"; set(x, y, "plate", k); } for (let i = 1; i <= h; i++) for (let x = PL.x0; x <= PL.x1; x++) { set(x, bot + i, "plate", i === 1 ? (x === PL.x0 ? "n2" : "n1") : x === PL.x0 ? "n1" : "n0"); } // rune grooves, then the groove's lower wall: lit stone when dark, a dim glow when the rune is lit const RW = RUNE[0].length, RH = RUNE.length; const isG = (x, y) => y >= 0 && y < RH && x >= 0 && x < RW && RUNE[y][x] !== "."; const inside = (x, y) => Math.abs(x - 8.5) + Math.abs(y - 4) * 1.6 < 5.5; // the diamond and its surround for (let y = 0; y < RH; y++) for (let x = 0; x < RW; x++) { const px = PL.x0 + 1 + x, py = top + RY + GY + y; const c = RUNE[y][x]; if (c !== ".") { set(px, py, "rune", rune.key(c, x, y)); continue; } const adj = isG(x - 1, y) || isG(x + 1, y) || isG(x, y - 1) || isG(x, y + 1); if (rune.halo === "all" && adj) set(px, py, "rune", rune.haloKey); else if (isG(x, y - 1) && !inside(x, y)) { if (rune.halo) set(px, py, "rune", rune.haloKey); else set(px, py, "plate", "n4"); } } return B;}const PARTS = ["frame", "socket", "plate", "rune"];function draw(g, B) { g.outlined("ink", () => { for (const part of PARTS) g.part(part, () => g.field({ x: [0, W - 1], y: [0, W - 1] }, (x, y) => { const b = B[y * W + x]; return b && b.part === part ? b.key : null; })); });}// ---- rune looksconst lvKey = (lv) => ["glow0", "glow1", "glow2", "glow3"][Math.max(0, Math.min(3, lv))];// a look = levels for ring / diamond / core; halo: null (dark grooves), "low" (dim glow on the grooves' lower wall)// or "all" (a solid glow around every groove, for the flash)function look(ring, dia, core, halo, haloLv, glint) { return { key: (c, x, y) => (c === "o" && glint && glint(x, y) ? "glow3" : lvKey(c === "o" ? ring : c === "a" ? dia : core)), halo: halo || null, haloKey: lvKey(haloLv || 0), };}function runeUp(t) { // faint pulse: dark grooves; the core warms first, then the diamond const p = 0.5 - 0.5 * Math.cos(TAU * t); return look(0, p > 0.6 ? 1 : 0, p > 0.85 ? 2 : p > 0.3 ? 1 : 0);}function runeDown(t) { // steady: ring and diamond glow2, core white-hot, ticks glow1; a glint runs round the ring const i = Math.floor(t * RING.length) % RING.length; const on = new Set([i, (i + 1) % RING.length].map((k) => RING[k][0] + "," + RING[k][1])); return look(2, 2, 3, "low", 1, (x, y) => on.has(x + "," + y));}// ---- effectsfunction pillar(g, top, amount) { // a shaft of light straight up out of the rune: solid at the flash, thinning into dither as it fades if (amount <= 0) return; g.detached("flare", () => { const base = top + RY + 4; const cols = [[13, 0.5, "glow2"], [14, 0.8, "glow2"], [15, 1, "glow3"], [16, 1, "glow3"], [17, 0.8, "glow2"], [18, 0.5, "glow2"]]; for (const [x, s, k0] of cols) { const len = Math.round(amount * s * (base + 1)); for (let k = 0; k < len; k++) { const y = base - k; if (y < 0) break; const u = k / len; if ((amount < 0.9 || u > 0.55 || s < 0.6) && !chk(x, y)) continue; // the fringe is always dithered g.pixel(x, y, u > 0.8 ? "glow1" : u > 0.55 && k0 === "glow3" ? "glow2" : k0); } } // sparks flung sideways from the flash if (amount > 0.5) for (const [dx, dy] of [[-6, -4], [7, -3], [-9, 1], [10, 2]]) { const x = 15.5 + dx * (amount > 0.9 ? 1 : 1.4), y = base - 3 + dy * (amount > 0.9 ? 1 : 1.4) - (amount > 0.9 ? 0 : 2); g.pixel(Math.round(x), Math.round(y), amount > 0.9 ? "glow3" : "glow2"); } });}// dust: [x, y, dx, dy] — puffs squirting out of the side gaps and over the front rimconst DUST = [[3, 13, -1, -0.6], [4, 20, -1, 0.2], [28, 13, 1, -0.6], [27, 20, 1, 0.2], [10, 25, -0.8, 0.3], [21, 25, 0.8, 0.3]];function dust(g, k) { if (k <= 0 || k > 3) return; g.detached("dust", () => { for (const [x0, y0, dx, dy] of DUST) { const x = Math.max(1, Math.min(30, Math.round(x0 + dx * (k - 1) * 1.3))), y = Math.round(y0 + dy * (k - 1) * 1.5 - (k - 1) * 0.7); const s = dx < 0 ? -1 : 1; if (k === 1) { // a round puff: lit crown, grey body, shaded underside g.pixel(x, y - 1, "n5"); g.pixel(x + s, y - 1, "n4"); g.pixel(x, y, "n4"); g.pixel(x + s, y, "n3"); } else if (k === 2) { // it spreads out and thins g.pixel(x - 1, y, "n4"); g.pixel(x, y, "n4"); g.pixel(x + 1, y, "n3"); g.pixel(x + s, y - 1, "n4"); } else { // last wisps g.pixel(x, y - 1, "n3"); if (x + 2 * s >= 0 && x + 2 * s <= 31) g.pixel(x + 2 * s, y, "n3"); } } });}function motes(g, top, t) { g.detached("motes", () => { for (let k = 0; k < 3; k++) { const p = (t + k / 3) % 1; const x = [12, 16, 19][k] + Math.round(Math.sin(TAU * (p + k * 0.37))); const y = top + RY + 3 - Math.round(p * 11); if (p < 0.8) g.pixel(x, y, p < 0.4 ? "glow3" : "glow2"); } });}// ---- statesconst PRESS = [ // h, rune, pillar, dust stage [2, () => runeUp(0), 0, 0], [1, () => look(1, 1, 2, "low", 0), 0, 0], [0, () => look(3, 3, 3, "all", 2), 1, 1], // impact: the rune flares white, dust squirts from the gaps [0, () => look(3, 3, 3, "all", 1), 0.7, 2], [0, () => look(2, 3, 3, "low", 1), 0.35, 3], [0, () => runeDown(0), 0, 0], // == down, frame 0];const RELEASE = [ [0, () => runeDown(0)], // == down, frame 0 [0, () => look(1, 2, 2, "low", 0)], [1, () => look(1, 1, 1)], [3, () => look(0, 0, 1)], // springs up a pixel past rest [2, () => runeUp(0)], [2, () => runeUp(0)], // == up, frame 0];const Plate = sprite(pal, (g, t, state) => { contactShadow(g, 16, GROUND, 15, 2); if (state === "up") { draw(g, build(2, runeUp(t))); return; } if (state === "down") { draw(g, build(0, runeDown(t))); motes(g, PL.y0, t); return; } if (state === "press") { const f = Math.min(PRESS.length - 1, Math.round(t * PRESS.length)); const [h, rune, amount, d] = PRESS[f]; draw(g, build(h, rune())); pillar(g, PL.y0 - h, amount); dust(g, d); if (f === PRESS.length - 1) motes(g, PL.y0, 0); return; } const f = Math.min(RELEASE.length - 1, Math.round(t * RELEASE.length)); const [h, rune] = RELEASE[f]; draw(g, build(h, rune())); if (f === 0) motes(g, PL.y0, 0);});submit(Plate, { size: [32, 32], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: 0.8, loop: true }, anchor: { x: 16, y: GROUND }, variants: [ { name: "up", args: ["up"] }, { name: "press", args: ["press"], animate: { fps: 10, duration: 0.6, loop: false } }, { name: "down", args: ["down"] }, { name: "release", args: ["release"], animate: { fps: 10, duration: 0.6, loop: false } }, { name: "down-violet", args: ["down"], repalette: swap("glow", "v") }, ],});掲載されたコードでは、ビルドスクリプトが自動付与する共通部分を省略しています。具体的には 1 行のランタイムヘッダー、共有パレット、および汎用ヘルパー関数(contactShadow、litIndex、flame、hash、chk、TAU)です。表示されている行番号は実際のファイルと一致しています。これらのプログラムは AIエージェントが生成し、描画されたコマの仕上がりを確認しながら修正を加えたものです。
エージェントに、こうしたプログラムを書かせる。
Tessarune は描画 API のリファレンス、25 冊の制作マニュアル、結果を確かめるための描画エンジンをエージェントに渡します。