Dungeon Mechanisms · dungeon_door.js
地下牢の扉
48×48 の正面向きの小物で、切石のアーチに扉を吊り、その周りに短い切石の壁を付けています。壁はキャンバスの端まで続くので、タイルセットの壁にそのまま置けます。一つのプログラムで、鉄の帯の蝶番と引き輪の付いた厚い板の扉と、鋲留めの鉄板に真鍮の錠前板の付いた鉄の扉の 2 種を描きます。どちらにも closed(8 コマ、金具に光が走る)、open(8 コマの一回再生、扉が内側へ開いて行き過ぎて戻り、まぐさから砂がこぼれる)、opened(8 コマ、暗い通路にほこりが漂う)があります。
6×
動くスプライト素材: Dungeon Mechanisms(itch.io)Vol.2・罠と扉と壊れる物 12 点
冒頭のコメント。これが指示書です。サイズ、視点、光の向き、すべての変種が書かれています。
dungeon_door.js · 1–14
// Dungeon door — 48x48, top-down 3/4, key light top-left. Camera-facing (set in a wall that faces the camera).// A door hung in a dressed-stone arch that sits in a short stub of ashlar wall (the stub runs to the canvas edges so// the piece drops into a tileset wall; the portcullis uses the same arch and wall). Two door styles in one program:// wood — a heavy plank door with iron strap hinges and a pull ring// iron — a vault door of riveted iron plates, heavy strap hinges and a brass lock plate with a keyhole// Built like chest.js: ONE door in ONE wall, modelled as a few solids (the wall box minus the arch hole, the// protruding arch frame, the keystone, and the leaf: a D-shaped slab on a vertical hinge). Every pixel casts the view// ray into them, and a second ray toward the key light decides the cast shadows (the arch shades the recessed leaf,// the jambs shade the passage). Lighting is in world space, so the leaf darkens as it turns away from the light.// Variants (iron: the same names with an -iron suffix):// closed loop, 8f — wood: a glint crosses the pull ring · iron: a glint travels across the rivets// open one-shot, 8f — the leaf swings inward on its hinge, overshoots and settles; grit falls from the lintel// (iron: slower to start, heavier, a smaller overshoot). The last frame == opened frame 0.// opened loop, 8f — the dark passage beyond, dust motes drifting in the doorway最後の数行。submit でキャンバスの大きさと、描画エンジンが書き出す変種の一覧を指定します。
dungeon_door.js · 400–412
submit(Door, { size: [48, 48], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: N / 10, loop: true }, anchor: { x: 24, y: 45 }, variants: [ { name: "closed", args: ["closed", "wood"] }, { name: "open", args: ["open", "wood"], animate: ONE_SHOT }, { name: "opened", args: ["opened", "wood"] }, { name: "closed-iron", args: ["closed", "iron"] }, { name: "open-iron", args: ["open", "iron"], animate: ONE_SHOT }, { name: "opened-iron", args: ["opened", "iron"] }, ],});プログラム全体を表示 (412 行)
dungeon_door.js · 412 行
// Dungeon door — 48x48, top-down 3/4, key light top-left. Camera-facing (set in a wall that faces the camera).// A door hung in a dressed-stone arch that sits in a short stub of ashlar wall (the stub runs to the canvas edges so// the piece drops into a tileset wall; the portcullis uses the same arch and wall). Two door styles in one program:// wood — a heavy plank door with iron strap hinges and a pull ring// iron — a vault door of riveted iron plates, heavy strap hinges and a brass lock plate with a keyhole// Built like chest.js: ONE door in ONE wall, modelled as a few solids (the wall box minus the arch hole, the// protruding arch frame, the keystone, and the leaf: a D-shaped slab on a vertical hinge). Every pixel casts the view// ray into them, and a second ray toward the key light decides the cast shadows (the arch shades the recessed leaf,// the jambs shade the passage). Lighting is in world space, so the leaf darkens as it turns away from the light.// Variants (iron: the same names with an -iron suffix):// closed loop, 8f — wood: a glint crosses the pull ring · iron: a glint travels across the rivets// open one-shot, 8f — the leaf swings inward on its hinge, overshoots and settles; grit falls from the lintel// (iron: slower to start, heavier, a smaller overshoot). The last frame == opened frame 0.// opened loop, 8f — the dark passage beyond, dust motes drifting in the doorwayconst K = 0.4, CX = 24, BASE = 46; // down view: screen rise per unit of depth (as chest.js), wall base rowconst UW = 24, WT = 10, WH = 38; // wall stub: half-length, thickness, heightconst SPR = 20, RO = 9, RF = 13, FP = 1.5; // arch: springing height, opening radius, frame outer radius, frame projectionconst RL = 8.7, RLC = RO - 0.25, DV = 0.5, DT = 2; // leaf: half-width, arch radius, set-back behind the wall face, thicknessconst HU = -RL, HV = DV + DT; // hinge axis: the viewer's left edge, at the leaf's back faceconst CH = WH / 7; // ashlar course heightconst PW = (2 * RL) / 4; // plank widthconst STRAPS = [5, 15.5]; // strap hinge heightsconst RU = 3.6, RZ = 9.6, RR = 2.1; // pull ring centre and radius (leaf coordinates)const REST = 78, N = 8;const OPEN_TH = { wood: [0, 3, 22, 50, 72, 85, 75, REST], // latch jolt, swing, overshoot, settle == opened iron: [0, 1, 5, 16, 33, 55, 81, REST], // strains to start, gathers weight, a small overshoot, settles};const IRON_STRAP_END = 0.8, IRON_KNOB = 1.8; // iron door: strap hinges end in a round knob at mid-doorconst LOCK = [3.2, 7.0, 8.2, 14.6], KEYHOLE = [5.1, 12.2]; // brass lock plate (u0, u1, z0, z1) and keyhole centrelet STYLE = "wood";const LV = norm3([-0.6, -0.25, 0.76]); // toward the key light (world: X right, Y away, Z up)const LS = norm3([-0.45, -0.6, 0.66]); // cast-shadow direction: the same key light, pulled forward so recess shadows stay shortconst BRASS = ["g0", "g1", "g2", "g3"], WOOD = ["w0", "w1", "w2", "w3", "w4"], STONE = ["n0", "n1", "n2", "n3", "n4"], IRON = ["n0", "n1", "n2", "n3", "n4"];const WALLB = [-UW, UW, 0, WT, 0, WH];const KEYB = [-2.3, 2.3, -FP - 0.8, 0, SPR + RO - 0.5, SPR + RF + 0.8];const SILLB = [-RO, RO, -FP, WT, -2, 0]; // the passage floor under the arch (seen only through it)function norm3(v) { const m = Math.hypot(v[0], v[1], v[2]); return [v[0] / m, v[1] / m, v[2] / m]; }const neg = (n) => [-n[0], -n[1], -n[2]];const clamp = (v, a, b) => Math.max(a, Math.min(b, v));// door-local coordinates: u along the wall (viewer's right +), v into the wall (room side < 0), z up. The camera is// chest.js's oblique view: a view ray per pixel, and the projection of a local point back to the screen.const rayAt = (sx, sy) => ({ o: [sx - CX, -30, BASE - sy + 30 * K], d: [0, 1, -K] });const proj = (q) => [CX + q[0], BASE - q[2] - K * q[1]];// ---- ray vs solids. Intervals are {tin, tout, nin, nout} (outward normals) or null.function slab(o, d, i, lo, hi, st) { const di = d[i], oi = o[i]; if (di < 1e-9 && di > -1e-9) return oi >= lo && oi <= hi; let t1 = (lo - oi) / di, t2 = (hi - oi) / di, s1 = -1, s2 = 1; if (t1 > t2) { const k = t1; t1 = t2; t2 = k; s1 = 1; s2 = -1; } if (t1 > st.tin) { st.tin = t1; st.nin = [0, 0, 0]; st.nin[i] = s1; } if (t2 < st.tout) { st.tout = t2; st.nout = [0, 0, 0]; st.nout[i] = s2; } return st.tin <= st.tout;}function box(o, d, b) { const st = { tin: -1e9, tout: 1e9, nin: [0, 0, 0], nout: [0, 0, 0] }; for (let i = 0; i < 3; i++) if (!slab(o, d, i, b[2 * i], b[2 * i + 1], st)) return null; return st;}// D-shaped prism along v: |u| <= w, z >= 0, v0 <= v <= v1, and (z <= SPR or inside the circle r about (0, SPR))function inD(u, z, w, r) { return Math.abs(u) <= w && z >= 0 && (z <= SPR || u * u + (z - SPR) * (z - SPR) <= r * r); }function dshape(o, d, w, r, v0, v1) { const st = { tin: -1e9, tout: 1e9, nin: [0, 0, 0], nout: [0, 0, 0] }; if (!slab(o, d, 0, -w, w, st) || !slab(o, d, 1, v0, v1, st) || !slab(o, d, 2, 0, 1e9, st)) return null; let hA = -1e9, hB = 1e9, cA = 1, cB = 0; if (d[2] < 1e-9 && d[2] > -1e-9) { if (o[2] > SPR) { hA = 1; hB = 0; } } else { const th = (SPR - o[2]) / d[2]; if (d[2] > 0) hB = th; else hA = th; } const A = d[0] * d[0] + d[2] * d[2], ou = o[0], oz = o[2] - SPR; if (A < 1e-12) { if (ou * ou + oz * oz <= r * r) { cA = -1e9; cB = 1e9; } } else { const B = 2 * (ou * d[0] + oz * d[2]), C = ou * ou + oz * oz - r * r, D = B * B - 4 * A * C; if (D >= 0) { const s = Math.sqrt(D); cA = (-B - s) / (2 * A); cB = (-B + s) / (2 * A); } } const a1 = Math.max(st.tin, hA), b1 = Math.min(st.tout, hB), a2 = Math.max(st.tin, cA), b2 = Math.min(st.tout, cB); const ok1 = a1 <= b1, ok2 = a2 <= b2; if (!ok1 && !ok2) return null; const tin = ok1 && ok2 ? Math.min(a1, a2) : ok1 ? a1 : a2; const tout = ok1 && ok2 ? Math.max(b1, b2) : ok1 ? b1 : b2; const capN = (t) => { const u = o[0] + t * d[0], z = o[2] + t * d[2] - SPR, m = Math.hypot(u, z) || 1; return [u / m, 0, z / m]; }; return { tin, tout, nin: tin === st.tin ? st.nin : ok2 && tin === cA ? capN(tin) : [0, 0, 1], nout: tout === st.tout ? st.nout : ok2 && tout === cB ? capN(tout) : [0, 0, 1], };}// first surface of (A minus B) past epsfunction minus(a, b, eps) { if (!a || a.tout < eps) return null; let best = null; if (a.tin > eps && !(b && a.tin >= b.tin && a.tin <= b.tout)) best = { t: a.tin, n: a.nin, inner: false }; if (b && b.tout > eps && b.tout >= a.tin && b.tout < a.tout && (!best || b.tout < best.t)) best = { t: b.tout, n: neg(b.nout), inner: true }; return best;}// the leaf's frame: rotate about the hinge axis by th degrees (free edge swings into +v)function leafFrame(th) { const a = (th * Math.PI) / 180, ca = Math.cos(a), sa = Math.sin(a); return { pt: (p) => { const ru = p[0] - HU, rv = p[1] - HV; return [HU + ru * ca + rv * sa, HV - ru * sa + rv * ca, p[2]]; }, dir: (v) => [v[0] * ca + v[1] * sa, -v[0] * sa + v[1] * ca, v[2]], back: (n) => [n[0] * ca - n[1] * sa, n[0] * sa + n[1] * ca, n[2]], };}function scene(o, d, lf, eps, primary, skipLeaf) { let best = null; const take = (h, id, extra) => { if (h && h.t > eps && (!best || h.t < best.t)) best = Object.assign({ t: h.t, n: h.n, id, inner: h.inner }, extra); }; take(minus(box(o, d, WALLB), dshape(o, d, RF, RF, -1e9, 1e9), eps), "wall"); take(minus(dshape(o, d, RF, RF, -FP, WT), dshape(o, d, RO, RO, -1e9, 1e9), eps), "frame"); const kb = box(o, d, KEYB); if (kb) take({ t: kb.tin, n: kb.nin }, "key"); let gate = true; // primary rays see the leaf and the passage only through the arch if (primary) { const t0 = (-FP - o[1]) / d[1]; gate = inD(o[0] + t0 * d[0], o[2] + t0 * d[2], RO, RO); } if (gate && !skipLeaf) { const ol = lf.pt(o), dl = lf.dir(d), h = dshape(ol, dl, RL, RLC, DV, DV + DT); if (h && h.tin > eps) take({ t: h.tin, n: lf.back(h.nin) }, "leaf", { lp: [ol[0] + h.tin * dl[0], ol[1] + h.tin * dl[1], ol[2] + h.tin * dl[2]], ln: h.nin }); } if (primary && gate) { const sb = box(o, d, SILLB); if (sb) take({ t: sb.tin, n: sb.nin }, "frame", { inner: true }); const c = dshape(o, d, RO, RO, WT, 1e9); if (c) take({ t: c.tout, n: neg(c.nout) }, "corridor"); } return best;}function classify(h, p) { const u = p[0], v = p[1], z = p[2], n = h.n; if (h.id === "wall") { if (n[2] > 0.5) return { part: "wall", mat: "stone", s: "wt", lift: 0.45, k: Math.floor((u + UW + 3) / 9) + 50 * Math.floor(v / 5.01) }; const c = Math.floor(z / CH), j = Math.floor((u + UW + 7.5 + (c & 1) * 4.5) / 9); return { part: "wall", mat: "stone", s: "wf", k: c * 64 + j }; } if (h.id === "frame" && !h.inner) { const k = z <= SPR ? (u < 0 ? 0 : 20) + Math.floor(z / (SPR / 3)) : 50 + Math.floor(Math.atan2(z - SPR, u) / (Math.PI / 7)); return { part: "arch", mat: "stone", s: n[1] < -0.5 ? "ff" : "fr", k, lift: 0.95 }; } if (h.id === "key") return { part: "arch", mat: "stone", s: "key", k: 0, lift: 0.9, carve: n[1] < -0.5 && Math.abs(Math.abs(u) + Math.abs(z - 31.3) - 1.3) < 0.45 }; if (h.id === "frame") return { part: "passage", mat: "reveal", s: n[2] > 0.5 ? "floor" : "side", k: 0, v }; if (h.id === "corridor") return { part: "passage", mat: "void", s: "void", k: 0, v }; // the leaf, in its own frame const lp = h.lp, ln = h.ln, lu = lp[0], lz = lp[2]; if (STYLE === "iron") return classifyIron(lu, lz, ln); if (ln[1] < -0.5) { const rd = Math.hypot(lu - RU, lz - RZ); if (Math.abs(rd - RR) < 0.62) return { part: "ring", mat: "ring", s: "ring", k: 0, a: Math.atan2(lz - RZ, lu - RU) }; if (Math.hypot(lu - RU, lz - (RZ + RR + 0.7)) < 0.95) return { part: "ring", mat: "boss", s: "boss", k: 0 }; for (let i = 0; i < STRAPS.length; i++) { const tip = RL - 1.2, half = lu > tip - 3 ? 1.05 * Math.max(0.35, (tip - lu) / 3) : 1.05; if (lu <= tip && Math.abs(lz - STRAPS[i]) < half) { let rivet = false; for (let r = 0; r < 4; r++) if (Math.abs(lu - (-RL + PW * (r + 0.5))) < 0.55 && Math.abs(lz - STRAPS[i]) < 0.5) rivet = true; return { part: "bands", mat: "iron", s: "strap", k: i, rivet, knuckle: lu < -RL + 0.9 }; } } const k = clamp(Math.floor((lu + RL) / PW), 0, 3), x = lu + RL - k * PW; const g0 = 3 + 9 * hash(k, 1), g1 = 13 + 8 * hash(k, 2); const grain = Math.abs(x - (1.4 + 1.6 * hash(k, 3))) < 0.5 && ((lz > g0 && lz < g0 + 3.5) || (lz > g1 && lz < g1 + 2.5)); return { part: "door", mat: "wood", s: "lf", k, grain }; } if (ln[1] > 0.5) return { part: "door", mat: "wood", s: "lb", k: clamp(Math.floor((lu + RL) / PW), 0, 3) }; return { part: "door", mat: "wood", s: "le", k: 0, top: ln[2] > 0.3 };}// the vault door: riveted plates (2 columns x 2 rows + the arch plate), strap hinges with knobs, a brass lock platefunction ironRivet(lu, lz) { // a row of rivets just inside the top and bottom edge of each plate, and a ring of them on the arch plate const along = (x, step) => { const m = ((x % step) + step) % step; return m < 0.55 || m > step - 0.55; }; if (lz < 20.5) { for (const zr of [1.4, 9.1, 11.9, 19.1]) if (Math.abs(lz - zr) < 0.55 && Math.abs(lu) < 7.9 && Math.abs(lu - 0.3) > 1.2 && along(lu + 7.3, 3.5)) return true; return false; } const r = Math.hypot(lu, lz - SPR), a = (Math.atan2(lz - SPR, lu) * 180) / Math.PI; if (Math.abs(r - 6.6) < 0.55 && along(a - 15, 30) && a > 5 && a < 175) return true; return false;}function classifyIron(lu, lz, ln) { if (ln[1] >= -0.5) return { part: "door", mat: "plate", s: ln[1] > 0.5 ? "back" : "le", k: 0, top: ln[2] > 0.3 }; if (lu >= LOCK[0] && lu <= LOCK[1] && lz >= LOCK[2] && lz <= LOCK[3]) { const kx = lu - KEYHOLE[0], kz = lz - KEYHOLE[1]; const key = Math.hypot(kx, kz) < 0.8 || (Math.abs(kx) < 0.4 && kz < 0 && kz > -2.4); const screw = (Math.abs(lu - LOCK[0] - 0.7) < 0.45 || Math.abs(lu - LOCK[1] + 0.7) < 0.45) && (Math.abs(lz - LOCK[2] - 0.7) < 0.45 || Math.abs(lz - LOCK[3] + 0.7) < 0.45); return { part: "lock", mat: "brass", s: "lock", k: 0, key, screw }; } for (let i = 0; i < STRAPS.length; i++) { const zc = STRAPS[i]; if ((lu <= IRON_STRAP_END && Math.abs(lz - zc) < 1.35) || Math.hypot(lu - IRON_STRAP_END, lz - zc) < IRON_KNOB) { let rivet = Math.hypot(lu - IRON_STRAP_END, lz - zc) < 0.55; for (const ur of [-7.3, -4.4, -1.5]) if (Math.abs(lu - ur) < 0.5 && Math.abs(lz - zc) < 0.5) rivet = true; return { part: "bands", mat: "iron", s: "strap", k: i, rivet, knuckle: lu < -RL + 0.9 }; } } const k = lz >= 20.5 ? 4 : (lz < 10.5 ? 0 : 2) + (lu < 0.3 ? 0 : 1); return { part: "door", mat: "plate", s: "plate", k, rivet: ironRivet(lu, lz), lu, lz };}function cast(th) { const lf = leafFrame(th); const buf = new Array(48 * 48).fill(null); for (let py = 0; py < 48; py++) for (let px = 0; px < 48; px++) { const { o, d } = rayAt(px + 0.5, py + 0.5); const h = scene(o, d, lf, 0, true, false); if (!h) continue; const p = [o[0] + h.t * d[0], o[1] + h.t * d[1], o[2] + h.t * d[2]]; const c = classify(h, p); c.d = h.n[0] * LV[0] + h.n[1] * LV[1] + h.n[2] * LV[2]; c.t = h.t; c.v = p[1]; if (c.d > 0 && c.mat !== "void") { const q = [p[0] + h.n[0] * 0.06, p[1] + h.n[1] * 0.06, p[2] + h.n[2] * 0.06]; c.sh = !!scene(q, LS, lf, 1e-4, false, h.id === "leaf"); } buf[py * 48 + px] = c; } return { buf, proj };}function tone(buf, fx) { const keys = new Array(48 * 48).fill(null); for (let i = 0; i < 48 * 48; i++) { const b = buf[i]; if (!b) continue; const px = i % 48, py = (i - px) / 48; const up = py > 0 ? buf[i - 48] : null, dn = py < 47 ? buf[i + 48] : null; const lf = px > 0 ? buf[i - 1] : null, rt = px < 47 ? buf[i + 1] : null; const seam = (q) => q && q.s === b.s && q.k !== b.k; let key; if (b.mat === "stone") { let val = 1.3 + 1.25 * b.d + (b.lift || 0); const h = hash(b.k * 1.7 + b.s.charCodeAt(1), 3.1); if (h < 0.22) val -= 0.45; else if (h > 0.8) val += 0.4; if (b.sh) val -= 0.9; let lv = Math.floor(val); if (seam(rt) || seam(dn)) lv = -1; else if (seam(up) || seam(lf)) lv += 1; else if (b.part === "arch" && ((up && up.part !== "arch") || (lf && lf.part !== "arch"))) lv += 1; if (!up && lv >= 0) lv += 1; if (b.carve) lv = Math.min(lv, 1); key = lv < 0 ? (val < 1.2 ? "ink" : "n0") : STONE[clamp(lv, 0, 4)]; } else if (b.mat === "reveal") { const dv = b.v + FP; key = b.sh ? (dv < 4 ? "n0" : "ink") : dv < 2.5 ? "n1" : dv < 6 ? "n0" : "ink"; } else if (b.mat === "void") { const dv = b.v - WT; key = dv < 2 ? "n0" : dv < 6 && chk(px, py) ? "n0" : "ink"; } else if (b.mat === "wood") { let val = 2.05 + 1.2 * b.d; if (b.s !== "le") { const h = hash(b.k, 7.7); val += h < 0.3 ? -0.3 : h > 0.75 ? 0.3 : 0; } if (b.sh) val -= 1; let lv = Math.floor(val); if (b.s === "lf" || b.s === "lb") { if (seam(rt)) lv = 0; else if (seam(lf)) lv += 1; else if (b.grain) lv -= 1; } else if (b.top) lv += 1; if (!up && lv > 0) lv += 1; key = WOOD[clamp(lv, 0, 4)]; } else if (b.mat === "iron") { let val = (STYLE === "iron" ? 2.15 : 1.3) + 1.0 * b.d; // on the vault door the straps stand proud of its plates and catch more light if (b.sh) val -= 0.8; let lv = Math.floor(val); const same = (q) => q && q.mat === "iron" && q.k === b.k; if (!same(up)) lv += 2; else if (!same(dn)) lv = 0; if (b.knuckle) lv = Math.max(lv, 2); if (b.rivet) lv = Math.max(lv + 2, 3); key = IRON[clamp(lv, 0, 4)]; } else if (b.mat === "plate") { // iron plates: a lit top/left lip, a dark gap on the right/bottom, domed rivets with a shadow under them let val = 1.5 + 1.2 * b.d; if (b.s === "plate") { const h = hash(b.k, 5.3); val += h < 0.35 ? -0.25 : h > 0.7 ? 0.2 : 0; } if (b.sh) val -= 0.9; let lv = Math.floor(val); const rv = (q) => q && q.mat === "plate" && q.rivet; if (b.s === "plate") { // each plate is a shallow raised panel: lit lip on its top/left border, shaded on its bottom/right border const sameP = (q) => q && q.mat === "plate" && q.s === "plate" && q.k === b.k; if (seam(rt) || seam(dn)) lv = -1; else if (!sameP(dn) || !sameP(rt)) lv -= 1; else if (!sameP(up) || !sameP(lf)) lv += 1; if (b.rivet) lv = fx.glint && fx.glint(b.lu, b.lz) ? 5 : Math.max(lv + 2, 3); else if (rv(up) || rv(lf)) lv = Math.min(lv, 0); } else if (b.top) lv += 1; key = lv >= 5 ? "n5" : lv < 0 ? "ink" : IRON[clamp(lv, 0, 4)]; } else if (b.mat === "brass") { const same = (q) => q && q.mat === "brass"; let lv = b.d > 0.4 ? 2 : 1; if (!same(up)) lv = 3; else if (!same(lf)) lv = 2; else if (!same(dn) || !same(rt)) lv = 0; if (b.sh) lv = Math.max(0, lv - 1); if (b.screw) lv = 3; key = b.key ? "ink" : BRASS[clamp(lv, 0, 3)]; } else if (b.mat === "ring") { const a = (b.a * 180) / Math.PI; // leaf frame: +90 = top, 180 = left let lv = a > 80 && a < 200 ? 3 : a < 10 && a > -110 ? 0 : 2; if (b.d < 0) lv -= 1; if (b.sh) lv -= 1; if (fx.ring) lv += fx.ring(a); key = IRON[clamp(lv, 0, 4)]; if (fx.ring && fx.ring(a) >= 2) key = "n5"; } else if (b.mat === "boss") { key = up && up.mat === "boss" ? (lf && lf.mat === "boss" ? "n1" : "n2") : "n3"; if (b.sh && key === "n3") key = "n2"; } keys[i] = key; } return keys;}const PARTS = ["wall", "arch", "passage", "door", "bands", "ring", "lock"];function drawDoor(g, th, fx) { const fr = cast(th), buf = fr.buf, keys = tone(buf, fx); let x0 = 47, x1 = 0, y0 = 47, y1 = 0; const seen = new Set(); for (let i = 0; i < 48 * 48; i++) if (buf[i]) { const x = i % 48, y = (i - x) / 48; x0 = Math.min(x0, x); x1 = Math.max(x1, x); y0 = Math.min(y0, y); y1 = Math.max(y1, y); seen.add(buf[i].part); } g.outlined("ink", () => { for (const part of PARTS) if (seen.has(part)) g.part(part, () => g.field({ x: [x0, x1], y: [y0, y1] }, (x, y) => { const b = buf[y * 48 + x]; return b && b.part === part ? keys[y * 48 + x] : null; })); }); return fr;}// a local point is drawn only where it is in front of what the ray already hit (rays never move in u)function put(g, fr, q, key) { const [sx, sy] = fr.proj(q), x = Math.floor(sx), y = Math.floor(sy); if (x < 0 || y < 0 || x > 47 || y > 47) return; const b = fr.buf[y * 48 + x]; if (b && b.v < q[1]) return; g.pixel(x, y, key);}// ring glint: a bright point sweeps over the top of the ring (leaf-frame angles), then restsfunction ringGlint(t) { const f = Math.round(t * N); if (f < 2 || f > 5) return null; const at = [165, 125, 85, 45][f - 2]; return (a) => { const d = Math.abs(a - at); return d < 22 ? 2 : d < 50 ? 1 : 0; };}// rivet glint: a bright diagonal band sweeps across the plates left to right, lighting each rivet it crossesfunction rivetGlint(t) { const f = Math.round(t * N); if (f < 1 || f > 6) return null; const gp = -12 + (f - 1) * 5; return (lu, lz) => Math.abs(lu - (lz - 14) * 0.45 - gp) < 1.6;}const Door = sprite(pal, (g, t, state, style) => { STYLE = style; if (state === "closed") { if (style === "iron") { drawDoor(g, 0, { glint: rivetGlint(t) }); return; } const fr = drawDoor(g, 0, { ring: ringGlint(t) }); const f = Math.round(t * N); if (f === 3 || f === 4) { // the glint peaks as a tiny star on the top of the ring g.detached("glint", () => { const q = [RU + (f === 3 ? -0.6 : 0.8), -1, RZ + RR + 0.1]; const [sx, sy] = fr.proj(q), x = Math.floor(sx), y = Math.floor(sy); g.pixel(x, y, "n5"); if (f === 3) for (const [dx, dy] of [[1, 0], [-1, 0], [0, -1]]) g.pixel(x + dx, y + dy, "n4"); }); } return; } if (state === "open") { const f = Math.min(N - 1, Math.round(t * N)); const fr = drawDoor(g, OPEN_TH[style][f], {}); // grit shaken loose from the lintel falls through the doorway (gone by the last frame); the iron door sheds more const grit = style === "iron" ? [[-3, 27, 1], [2, 28, 2], [5.5, 26, 3], [-0.5, 28.5, 2], [4, 27, 1]] : [[-3, 27, 0], [2, 28, 1], [5.5, 26, 2]]; if (f >= 1 && f <= 6) g.detached("grit", () => { for (const [u0, z0, ph] of grit) { const k = f - 1 - ph; if (k < 0 || k > 3) continue; const z = z0 - 2 - k * (3 + k * 2); if (z < 1) continue; put(g, fr, [u0 + (k & 1) * 0.5, -0.8, z], k < 2 ? "n4" : "n3"); } }); if (f === N - 1) motes(g, fr, 0); return; } motes(g, drawDoor(g, REST, {}), t);});// dust motes drifting in the doorway light (the opened loop, and the last frame of open == its frame 0)function motes(g, fr, t) { g.detached("dust", () => { for (const [u0, v0, z0, ph] of [[-1.5, -0.5, 22, 0], [3.5, 1.5, 15, 0.37], [0.5, 3, 8, 0.7], [6, 0.5, 25, 0.15]]) { const p = (t + ph) % 1; const q = [u0 + Math.sin(TAU * (p + ph)) * 1.2, v0, z0 - p * 4]; put(g, fr, q, p < 0.2 || p > 0.8 ? "n2" : p > 0.4 && p < 0.6 ? "n4" : "n3"); } });}const ONE_SHOT = { fps: 10, duration: N / 10, loop: false };submit(Door, { size: [48, 48], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: N / 10, loop: true }, anchor: { x: 24, y: 45 }, variants: [ { name: "closed", args: ["closed", "wood"] }, { name: "open", args: ["open", "wood"], animate: ONE_SHOT }, { name: "opened", args: ["opened", "wood"] }, { name: "closed-iron", args: ["closed", "iron"] }, { name: "open-iron", args: ["open", "iron"], animate: ONE_SHOT }, { name: "opened-iron", args: ["opened", "iron"] }, ],});掲載されたコードでは、ビルドスクリプトが自動付与する共通部分を省略しています。具体的には 1 行のランタイムヘッダー、共有パレット、および汎用ヘルパー関数(contactShadow、litIndex、flame、hash、chk、TAU)です。表示されている行番号は実際のファイルと一致しています。これらのプログラムは AIエージェントが生成し、描画されたコマの仕上がりを確認しながら修正を加えたものです。
エージェントに、こうしたプログラムを書かせる。
Tessarune は描画 API のリファレンス、25 冊の制作マニュアル、結果を確かめるための描画エンジンをエージェントに渡します。