Tessarune
Dungeon Mechanisms · lever.js

床のレバー

32×32 · JavaScript 335 行

32×32 の床のレバーで、段のある石の台に鉄の取り付け板と 2 枚の側板があり、鉄の軸と革の握りと真鍮の玉でできた柄が真鍮の軸受けで動きます。台の左手前の角には小さな真鍮のランプがあります。4 方向は一つのレバーを床の上で回したもので、どれも世界の側の左上から光を当てるので、右向きは左向きの裏返しではありません。各方向に off(8 コマ、真鍮に光が走る)、pull(8 コマの一回再生、構えて倒し、止め金を越えて火花が散り、ランプが灯る)、on(8 コマ、ランプが暖かく光る)があります。

床のレバー — 描画結果6×
このプログラムで描画(変種「pull-down」) · 32×32 · 8 コマ
Dungeon Mechanisms動くスプライト素材: Dungeon Mechanisms(itch.io)Vol.2・罠と扉と壊れる物 12 点

冒頭のコメント。これが指示書です。サイズ、視点、光の向き、すべての変種が書かれています。

lever.js · 1–10
// Floor lever — 32x32, top-down 3/4, key light top-left.// A stepped stone plinth carries an iron mounting plate with two cheek plates; the handle (iron shaft, leather grip,// brass knob) pivots between them on brass bosses, and a small brass lamp sits on the plinth's front-left corner.// Built like chest.js: every pixel casts the oblique view ray (screen y = BASE - z - K*depth) into a few convex// solids, so the four facings are one lever turned on the floor, each lit top-left in WORLD space (right is not a// mirrored left). The handle is pulled toward its front: "off" leans away from the front, "on" leans toward it.// Variants (f = down / up / left / right)://   off-f   loop, 8f     — a glint runs over the brass and flashes on the knob//   pull-f  one-shot, 8f — wind-up, swing through, overshoot past the stop (sparks), settle; the lamp lights -> == on f0//   on-f    loop, 8f     — the lamp glows and breathes, a warm halo on the plinth

最後の数行。submit でキャンバスの大きさと、描画エンジンが書き出す変種の一覧を指定します。

lever.js · 326–335
submit(Lever, {  size: [32, 32], origin: "top-left", kind: "icon", scale: 1,  animate: { fps: 10, duration: N / 10, loop: true },  anchor: { x: 16, y: GROUND },  variants: [    ...FACES.map((f) => ({ name: `off-${f}`, args: ["off", f] })),    ...FACES.map((f) => ({ name: `pull-${f}`, args: ["pull", f], animate: { fps: 10, duration: N / 10, loop: false } })),    ...FACES.map((f) => ({ name: `on-${f}`, args: ["on", f] })),  ],});
プログラム全体を表示 (335 行)
lever.js · 335 行
// Floor lever — 32x32, top-down 3/4, key light top-left.// A stepped stone plinth carries an iron mounting plate with two cheek plates; the handle (iron shaft, leather grip,// brass knob) pivots between them on brass bosses, and a small brass lamp sits on the plinth's front-left corner.// Built like chest.js: every pixel casts the oblique view ray (screen y = BASE - z - K*depth) into a few convex// solids, so the four facings are one lever turned on the floor, each lit top-left in WORLD space (right is not a// mirrored left). The handle is pulled toward its front: "off" leans away from the front, "on" leans toward it.// Variants (f = down / up / left / right)://   off-f   loop, 8f     — a glint runs over the brass and flashes on the knob//   pull-f  one-shot, 8f — wind-up, swing through, overshoot past the stop (sparks), settle; the lamp lights -> == on f0//   on-f    loop, 8f     — the lamp glows and breathes, a warm halo on the plinthconst CX = 16, GROUND = 29, K = 0.5, SZ = 32;const PZ = 7.5, LEN = 9.5;                         // pivot height, shaft length (grip ends at LEN, knob beyond)const OFF = 32, ON = -32;                          // handle angle, degrees (+ = leaning to the back)const N = 8;const LV = norm3([-0.6, -0.25, 0.76]);            // toward the key light (world: X right, Y away, Z up)const STONE = ["n1", "n2", "n3", "n4"], IRON = ["n0", "n1", "n2", "n3"], GOLD = ["g0", "g1", "g2", "g3"], LEATHER = ["w0", "w1", "w2", "w3"];function norm3(v) { const m = Math.hypot(v[0], v[1], v[2]); return [v[0] / m, v[1] / m, v[2] / m]; }// lever-local (x along the pivot axis, y front(-)..back(+), z up) <-> world, per facingconst FACINGS = {  down: { toL: (v) => [v[0], v[1], v[2]], toW: (v) => [v[0], v[1], v[2]], shadow: 10 },  up: { toL: (v) => [-v[0], -v[1], v[2]], toW: (v) => [-v[0], -v[1], v[2]], shadow: 10 },  right: { toL: (v) => [v[1], -v[0], v[2]], toW: (v) => [-v[1], v[0], v[2]], shadow: 8 },  left: { toL: (v) => [-v[1], v[0], v[2]], toW: (v) => [v[1], -v[0], v[2]], shadow: 8 },};// ---- solids: boxes [x0,x1,y0,y1,z0,z1], optionally cut by a quadric {c, inv2}const quad = (c, r) => ({ c, inv2: r.map((v) => (isFinite(v) ? 1 / (v * v) : 0)) });const ANY = [-99, 99, -99, 99, -99, 99];const LO = [-8.5, 8.5, -7.5, 6.5, 0, 2];           // plinth, lower stepconst HI = [-7.5, 7.5, -6.5, 5.5, 2, 5];           // plinth, upper blockconst PLATE = [-4.4, 4.4, -4.2, 4.2, 5, 6];        // iron mounting plate// the pivot axis runs front-to-back (local y): the handle swings sideways, and the front cheek faces the frontconst CHEEK_Q = quad([0, 0, PZ], [3.4, Infinity, 3.4]);const CHEEKS = [[-3.4, 3.4, -3.9, -2.5, 6, 12], [-3.4, 3.4, 2.5, 3.9, 6, 12]];// brass pivot caps: flattened domes bulging out of each cheekconst BOSSES_Q = [quad([0, -3.9, PZ], [1.35, 0.9, 1.35]), quad([0, 3.9, PZ], [1.35, 0.9, 1.35])];const SHAFT = [-1.05, 1.05, -1.05, 1.05, -1, LEN - 4.5], SHAFT_Q = quad([0, 0, 0], [1.05, 1.05, Infinity]);const GRIP = [-1.4, 1.4, -1.4, 1.4, LEN - 4.5, LEN], GRIP_Q = quad([0, 0, 0], [1.4, 1.4, Infinity]);const KNOB_Q = quad([0, 0, LEN + 1.3], [2.05, 2.05, 2.05]);// indicator lamp: a squat brass cup with a glass bead on topconst LAMP_C = [-5.6, -4.9, 5], LAMP_Q = quad(LAMP_C, [1.9, 1.9, 1.3]), LENS_Q = quad([LAMP_C[0], LAMP_C[1], 6.1], [1.15, 1.15, 1.0]);let TIN = 0, TOUT = 0, CIN = -1, COUT = -1;function convex(o, d, b, q) {  let tin = -1e9, tout = 1e9, cin = -1, cout = -1;  for (let i = 0; i < 3; i++) {    const lo = b[2 * i], hi = b[2 * i + 1], di = d[i], oi = o[i];    if (di < 1e-9 && di > -1e-9) { if (oi < lo || oi > hi) return false; continue; }    let t1 = (lo - oi) / di, t2 = (hi - oi) / di, c1 = 2 * i, c2 = 2 * i + 1;    if (t1 > t2) { const k = t1; t1 = t2; t2 = k; c1 = 2 * i + 1; c2 = 2 * i; }    if (t1 > tin) { tin = t1; cin = c1; }    if (t2 < tout) { tout = t2; cout = c2; }    if (tin > tout) return false;  }  if (q) {    const r = q.inv2, c = q.c;    const ox = o[0] - c[0], oy = o[1] - c[1], oz = o[2] - c[2];    const A = d[0] * d[0] * r[0] + d[1] * d[1] * r[1] + d[2] * d[2] * r[2];    const B = 2 * (ox * d[0] * r[0] + oy * d[1] * r[1] + oz * d[2] * r[2]);    const C = ox * ox * r[0] + oy * oy * r[1] + oz * oz * r[2] - 1;    const disc = B * B - 4 * A * C;    if (disc < 0) return false;    const sq = Math.sqrt(disc), r1 = (-B - sq) / (2 * A), r2 = (-B + sq) / (2 * A);    if (r1 > tin) { tin = r1; cin = 6; }    if (r2 < tout) { tout = r2; cout = 6; }    if (tin > tout) return false;  }  TIN = tin; TOUT = tout; CIN = cin; COUT = cout;  return true;}function normalOf(code, p, q) {  if (code < 6) { const n = [0, 0, 0]; n[code >> 1] = code & 1 ? 1 : -1; return n; }  return norm3([(p[0] - q.c[0]) * q.inv2[0], (p[1] - q.c[1]) * q.inv2[1], (p[2] - q.c[2]) * q.inv2[2]]);}// screen offset of the facing: the plinth's nearest bottom edge lands on GROUNDfunction baseOf(F) {  let ext = 0;  for (const x of [LO[0], LO[1]]) for (const y of [LO[2], LO[3]]) ext = Math.max(ext, -F.toW([x, y, 0])[1]);  return GROUND - K * ext;}// ---- one frame: cast every pixel, classify the hitfunction castLever(face, th) {  const F = FACINGS[face], BASE = baseOf(F);  const a = (th * Math.PI) / 180, ca = Math.cos(a), sa = Math.sin(a);  // the handle frame: rotated about the local y axis by th (+ leans toward local +x)  const toH = (v, z0) => [v[0] * ca - (v[2] - z0) * sa, v[1], v[0] * sa + (v[2] - z0) * ca];   // lever-local -> handle  const fromHn = (n) => [n[0] * ca + n[2] * sa, n[1], -n[0] * sa + n[2] * ca];                 // handle normal -> local  const d = F.toL([0, 1, -K]), dh = toH(d, 0);  const buf = new Array(SZ * SZ).fill(null);  for (let py = 0; py < SZ; py++) for (let px = 0; px < SZ; px++) {    const Z0 = BASE - (py + 0.5);    const o = F.toL([px + 0.5 - CX, -40, Z0 + 40 * K]), oh = toH(o, PZ);    let bt = 1e9, bid = null, bh = false, bcode = -1, bq = null;    const hit = (id, h, box, q) => {      if (convex(h ? oh : o, h ? dh : d, box, q) && TIN < bt) { bt = TIN; bid = id; bh = h; bcode = CIN; bq = q; }    };    hit("lo", false, LO, null); hit("hi", false, HI, null); hit("plate", false, PLATE, null);    for (const c of CHEEKS) hit("cheek", false, c, CHEEK_Q);    for (const q of BOSSES_Q) hit("boss", false, ANY, q);    hit("lamp", false, ANY, LAMP_Q); hit("lens", false, ANY, LENS_Q);    hit("shaft", true, SHAFT, SHAFT_Q); hit("grip", true, GRIP, GRIP_Q); hit("knob", true, ANY, KNOB_Q);    if (!bid) continue;    const oo = bh ? oh : o, dd = bh ? dh : d;    const p = [oo[0] + bt * dd[0], oo[1] + bt * dd[1], oo[2] + bt * dd[2]];    const n = normalOf(bcode, p, bq);    const nw = F.toW(bh ? fromHn(n) : n);    const c = classify(bid, p, n);    c.t = bt; c.d = nw[0] * LV[0] + nw[1] * LV[1] + nw[2] * LV[2]; c.top = n[2] > 0.5 && !bh; c.nw = nw;    buf[py * SZ + px] = c;  }  const proj = (q) => { const w = F.toW(q); return [CX + w[0] - 0.5, BASE - w[2] - K * w[1] - 0.5]; };  const projH = (qh, thDeg) => {    const b = (thDeg * Math.PI) / 180;    return proj([qh[0] * Math.cos(b) + qh[2] * Math.sin(b), qh[1], PZ - qh[0] * Math.sin(b) + qh[2] * Math.cos(b)]);  };  return { buf, proj, projH };}function classify(id, p, n) {  const [x, y, z] = p;  if (id === "lo" || id === "hi") {    // a vertical joint on each face of the upper block, one step off centre    const face = n[2] > 0.5 ? "top" : Math.abs(n[1]) > 0.5 ? "fb" : "side";    const joint = id === "hi" && face !== "top" && Math.abs((face === "fb" ? x : y) - 2.2) < 0.45;    // countable marks on the top: a hairline crack by the right rim, a chipped back-left corner    const seg = (ax, ay, bx, by) => { const vx = bx - ax, vy = by - ay, u = Math.max(0, Math.min(1, ((x - ax) * vx + (y - ay) * vy) / (vx * vx + vy * vy))); return Math.hypot(x - ax - u * vx, y - ay - u * vy); };    const crack = id === "hi" && face === "top" && (seg(7.5, -2.5, 5.8, -1.2) < 0.5 || seg(5.8, -1.2, 5.6, 0.8) < 0.5 || Math.hypot(x + 6.6, y - 4.6) < 0.7);    return { part: "plinth", mat: "stone", id, face, joint: joint || crack };  }  if (id === "plate") {    const slot = n[2] > 0.5 && Math.abs(y) < 2.5 && Math.abs(x) < 3.4;    const rivet = n[2] > 0.5 && Math.abs(Math.abs(x) - 3.6) < 0.5 && Math.abs(Math.abs(y) - 3.5) < 0.5;    return { part: "plate", mat: slot ? "slot" : "iron", rivet };  }  if (id === "cheek") return { part: "cheeks", mat: "iron" };  if (id === "boss") return { part: "bosses", mat: "brass", round: true };  if (id === "lens") return { part: "lamp", mat: "lens" };  if (id === "shaft") return { part: "shaft", mat: "iron" };  if (id === "grip") return { part: "grip", mat: "leather", k: Math.floor((z - (LEN - 4.5)) / 1.5), z };  if (id === "knob") return { part: "knob", mat: "brass", round: true };  return { part: "lamp", mat: "brass", round: true };}function level(d) { return d > 0.78 ? 3 : d > 0.45 ? 2 : d > 0.1 ? 1 : 0; }function tone(buf, fx) {  const keys = new Array(SZ * SZ).fill(null);  for (let i = 0; i < SZ * SZ; i++) {    const b = buf[i]; if (!b) continue;    const px = i % SZ, py = (i - px) / SZ;    const up = py > 0 ? buf[i - SZ] : null, dn = py < SZ - 1 ? buf[i + SZ] : null;    const lf = px > 0 ? buf[i - 1] : null, rt = px < SZ - 1 ? buf[i + 1] : null;    const samePart = (q) => q && q.part === b.part;    let key;    if (b.mat === "stone") {      let lv = b.face === "top" ? 2 : level(b.d);      const isStone = (q) => q && q.part === "plinth";      if (b.face === "top") {        if (b.id === "hi" && (!isStone(up) || !isStone(lf))) lv = 3;          // the upper block's back and left rims catch the light        if (b.id === "lo" && !isStone(lf)) lv = 3;        if (b.id === "hi" && !isStone(rt)) lv = 1;                             // right rim turns away        if (lf && !isStone(lf) && lf.t !== undefined && b.id === "hi" && isStone(up)) lv = 1;   // fittings cast a sliver of shadow to the right      } else {        if (up && up.face === "top" && up.id === b.id) lv = Math.min(3, lv + 1);                   // bevel under the lip        if (!dn || !isStone(dn) || (b.id === "hi" && dn.id === "lo")) lv = Math.max(0, lv - 1);   // foot of each block      }      if (b.joint) lv = 0;      if (fx.pool && b.face === "top") lv = Math.min(3, lv + fx.pool(px, py));      key = STONE[lv];    } else if (b.mat === "iron") {      let lv = level(b.d);      if (!samePart(lf)) lv = Math.min(3, lv + 1);      else if (!samePart(rt)) lv = Math.max(0, lv - 1);      if (!samePart(up) && b.part !== "shaft") lv = Math.min(3, lv + 1);      lv = Math.max(0, Math.min(3, lv));      key = b.rivet ? "n4" : lv === 3 && b.d > 0.85 ? "n4" : IRON[lv];    } else if (b.mat === "slot") {      key = up && up.mat === "slot" ? "ink" : "n0";    } else if (b.mat === "leather") {      let lv = b.d > 0.6 ? 3 : b.d > 0.3 ? 2 : b.d > 0.0 ? 1 : 0;      const wrap = (q) => q && q.part === "grip" && q.k !== b.k;      if (wrap(up) || wrap(lf)) lv = Math.max(0, lv - 1);      // the wrap's overlapping edge      key = LEATHER[lv];    } else if (b.mat === "brass") {      let lv;      if (b.round) lv = b.d > 0.82 ? 3 : b.d > 0.5 ? 2 : b.d > 0.15 ? 1 : 0;      else {        const same = (q) => q && q.part === b.part;        lv = b.d > 0.5 ? 2 : 1;        if (!same(up) || !same(lf)) lv = 3; else if (!same(dn) || !same(rt)) lv = 0;      }      if (b.pin) lv = 0;      if (fx.glint) lv = Math.min(3, lv + fx.glint(px, py));      key = GOLD[lv];    } else if (b.mat === "lens") {      // a glass bead: dark ruby when off (one cream catch-light), amber to white-hot when lit      const lit = fx.lamp || 0;           // 0 off, 1 catching, 2 lit, 3 flash      const spec = b.d > 0.9, rim = b.d < 0.35;      if (lit === 0) key = spec ? "n4" : rim ? "r0" : "r1";      else if (lit === 1) key = spec ? "g3" : rim ? "r1" : "r2";      else if (lit === 2) key = spec || !rim ? "g3" : "g2";      else key = rim ? "g3" : "n5";    }    keys[i] = key;  }  return keys;}const PARTS = ["plinth", "plate", "lamp", "cheeks", "bosses", "shaft", "grip", "knob"];function drawLever(g, face, th, fx) {  const fr = castLever(face, th);  if (fx.glow > 0) {    // the lit lamp pools warm light on the stone round it: solid near, dithered at the edge    const [lx, ly] = fr.proj([LAMP_C[0], LAMP_C[1], LAMP_C[2]]);    const r = 2.6 + 1.6 * fx.glow;    fx.pool = (x, y) => {      const q = Math.hypot((x + 0.5 - lx) / 1.3, y + 0.5 - ly);      return q < r ? 1 : 0;    };  }  const keys = tone(fr.buf, fx);  const seen = new Set();  for (const b of fr.buf) if (b) seen.add(b.part);  g.outlined("ink", () => {    for (const part of PARTS) if (seen.has(part)) g.part(part, () => g.field({ x: [0, SZ - 1], y: [0, SZ - 1] }, (x, y) => {      const b = fr.buf[y * SZ + x];      return b && b.part === part ? keys[y * SZ + x] : null;    }));  });  return fr;}// ---- effectsfunction visible(buf, x, y) { return x >= 0 && y >= 0 && x < SZ && y < SZ && !buf[y * SZ + x]; }function star(g, fr, x, y, size, keys) {  const put = (px, py, k) => { if (px >= 0 && py >= 0 && px < SZ && py < SZ) g.pixel(px, py, k); };  put(x, y, keys[0]);  if (size > 0) for (const [dx, dy] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) put(x + dx, y + dy, keys[1]);  if (size > 1) for (const [dx, dy] of [[2, 0], [-2, 0], [0, 2], [0, -2]]) put(x + dx, y + dy, keys[2]);}// swing smear: dithered arc of the knob's path between two angles, only where nothing solid is drawnfunction smear(g, fr, th0, th1) {  g.detached("smear", () => {    const n = Math.ceil(Math.abs(th1 - th0) / 3);    for (let i = 1; i < n; i++) {      const th = th0 + ((th1 - th0) * i) / n;      for (const r of [LEN + 1.3, LEN - 1.5]) {        const [sx, sy] = fr.projH([0, 0, r], th);        const x = Math.round(sx), y = Math.round(sy);        if (visible(fr.buf, x, y) && (chk(x, y) || r > LEN)) g.pixel(x, y, i / n > 0.5 ? "n4" : "n3");      }    }  });}// ---- statesconst PULL = [  // angle, lamp, smear-from  [OFF, 0, null],  [OFF + 7, 0, null],          // wind-up: a hand takes it back a little first  [14, 0, OFF + 7],  [-16, 0, 14],  [-40, 1, -16],               // past the stop: sparks at the slot, the lamp catches  [-26, 3, null],              // rebounds, the lamp flashes  [-34, 2, null],  [ON, 2, null],               // == on, frame 0];const Lever = sprite(pal, (g, t, state, face) => {  contactShadow(g, 16, GROUND, FACINGS[face].shadow, 2);  if (state === "off") {    // a glint band sweeps the brass over the first half, then a star flashes on the knob    const gp = -4 + (t / 0.5) * 44;    const glint = t < 0.5 ? (x, y) => (Math.abs(x + y * 0.8 - gp) < 1.2 ? 1 : 0) : null;    const fr = drawLever(g, face, OFF, { glint, lamp: 0 });    const f = Math.round(t * N);    if (f === 4 || f === 5) {      const [sx, sy] = fr.projH([-0.9, -0.9, LEN + 2.4], OFF);      g.detached("glint", () => star(g, fr, Math.round(sx), Math.round(sy), f === 4 ? 1 : 0, ["n5", "g3", "g2"]));    }    return;  }  if (state === "pull") {    const f = Math.min(N - 1, Math.round(t * N));    const [th, lamp, from] = PULL[f];    const fr = drawLever(g, face, th, { lamp, glow: lamp >= 2 ? (lamp === 3 ? 1 : 0.5) : 0 });    if (from !== null) smear(g, fr, from, th);    if (f === 4) {      // the shaft slams the stop: sparks jump from the slot      const [sx, sy] = fr.proj([-3.2, 0, 6.6]);      g.detached("sparks", () => {        for (const [dx, dy, k] of [[-2, -2, "g3"], [2, -2, "g3"], [-3, 0, "g2"], [3, -1, "g2"], [0, -3, "n5"]]) {          const x = Math.round(sx) + dx, y = Math.round(sy) + dy;          if (visible(fr.buf, x, y)) g.pixel(x, y, k);        }      });    }    if (lamp === 3) {      const [lx, ly] = fr.proj([LAMP_C[0], LAMP_C[1], LAMP_C[2] + 2.6]);      g.detached("flash", () => star(g, fr, Math.round(lx), Math.round(ly) - 1, 1, ["n5", "g3", "g2"]));    }    return;  }  const breath = 0.5 - 0.5 * Math.cos(TAU * t);  const fr = drawLever(g, face, ON, { lamp: 2, glow: 0.5 + 0.5 * breath });  // the bead breathes: a gleam rises off it at the top of each breath  const [lx, ly] = fr.proj([LAMP_C[0], LAMP_C[1], 7.4]);  g.detached("gleam", () => {    const x = Math.round(lx), y = Math.round(ly);    const put = (px, py, k) => { const b = fr.buf[py * SZ + px]; if (!b || b.part === "plinth") g.pixel(px, py, k); };   // never over the handle    if (breath > 0.3) put(x, y - 1, breath > 0.75 ? "g3" : "g2");    if (breath > 0.75) { put(x, y - 3, "g2"); put(x - 2, y - 1, "g1"); put(x + 2, y - 1, "g1"); }  });});const FACES = ["down", "up", "left", "right"];submit(Lever, {  size: [32, 32], origin: "top-left", kind: "icon", scale: 1,  animate: { fps: 10, duration: N / 10, loop: true },  anchor: { x: 16, y: GROUND },  variants: [    ...FACES.map((f) => ({ name: `off-${f}`, args: ["off", f] })),    ...FACES.map((f) => ({ name: `pull-${f}`, args: ["pull", f], animate: { fps: 10, duration: N / 10, loop: false } })),    ...FACES.map((f) => ({ name: `on-${f}`, args: ["on", f] })),  ],});

掲載されたコードでは、ビルドスクリプトが自動付与する共通部分を省略しています。具体的には 1 行のランタイムヘッダー、共有パレット、および汎用ヘルパー関数(contactShadow、litIndex、flame、hash、chk、TAU)です。表示されている行番号は実際のファイルと一致しています。これらのプログラムは AIエージェントが生成し、描画されたコマの仕上がりを確認しながら修正を加えたものです。

エージェントに、こうしたプログラムを書かせる。

Tessarune は描画 API のリファレンス、25 冊の制作マニュアル、結果を確かめるための描画エンジンをエージェントに渡します。