// Terrain that stands in the room rather than lying on its floor: bushes // filling a square to half height, the killer ooze as a translucent cube // of jello, dust billowing, dimensional warps throwing light. Each is a // paintable sprite at public/terrain3d/.png (transparent PNG, any // size); the bakes below stand in until painted. export const TERRAIN3D = ["thornbush", "rosebush", "jello", "dust", "warpglow", "safe"] as const; const baked = new Map(); export function terrainFallback(name: string): HTMLCanvasElement { let t = baked.get(name); if (t) return t; t = document.createElement("canvas"); t.width = 128; t.height = 64; const c = t.getContext("2d")!; const h2 = (a: number, b: number) => { const n = Math.sin(a * 127.1 + b * 311.7) * 43758.5453; return n - Math.floor(n); }; if (name === "thornbush" || name === "rosebush") { // A hedge of tangles wall to wall; roses get their blooms. for (let i = 0; i < 40; i++) { const x = h2(i, 1) * 128, y = 16 + h2(i, 3) * 46, r = 6 + h2(i, 5) * 9; const g = 60 + (h2(i, 7) * 50 | 0); c.fillStyle = `rgba(${g * 0.35 | 0},${g},${g * 0.3 | 0},0.9)`; c.beginPath(); c.arc(x, y, r, 0, Math.PI * 2); c.fill(); } c.strokeStyle = "rgba(30,40,18,0.7)"; for (let i = 0; i < 14; i++) { c.beginPath(); c.moveTo(h2(i, 11) * 128, 18 + h2(i, 13) * 40); c.lineTo(h2(i, 11) * 128 + 10 - h2(i, 17) * 20, 8 + h2(i, 19) * 40); c.stroke(); } if (name === "rosebush") { for (let i = 0; i < 9; i++) { c.fillStyle = "rgba(210,40,70,0.95)"; c.beginPath(); c.arc(6 + h2(i, 23) * 116, 14 + h2(i, 29) * 40, 3.5, 0, Math.PI * 2); c.fill(); } } } else if (name === "jello") { // The gelatinous whole of the killer ooze, drawn as one cube face // with a paler top edge — the renderer's alpha keeps it see-through. t.height = 128; const g2 = t.getContext("2d")!; g2.fillStyle = "rgba(90,190,70,0.85)"; g2.fillRect(4, 12, 120, 112); g2.fillStyle = "rgba(170,240,140,0.9)"; g2.fillRect(4, 12, 120, 10); g2.fillStyle = "rgba(230,255,210,0.5)"; g2.fillRect(16, 30, 22, 12); for (let i = 0; i < 8; i++) { g2.fillStyle = "rgba(40,120,30,0.5)"; g2.beginPath(); g2.arc(16 + h2(i, 31) * 96, 40 + h2(i, 37) * 72, 3 + h2(i, 41) * 3, 0, Math.PI * 2); g2.fill(); } } else if (name === "dust") { t.height = 128; const g2 = t.getContext("2d")!; for (let i = 0; i < 26; i++) { const a = 0.12 + h2(i, 43) * 0.16; g2.fillStyle = `rgba(200,190,170,${a})`; g2.beginPath(); g2.arc(14 + h2(i, 47) * 100, 14 + h2(i, 53) * 100, 12 + h2(i, 59) * 16, 0, Math.PI * 2); g2.fill(); } } else if (name === "safe") { // A squat iron strongbox with its combination dial. t.width = 128; t.height = 128; const g2 = t.getContext("2d")!; g2.fillStyle = "#3c3f46"; g2.fillRect(14, 34, 100, 90); g2.fillStyle = "#54585f"; g2.fillRect(20, 40, 88, 78); g2.strokeStyle = "#23252a"; g2.lineWidth = 4; g2.strokeRect(16, 36, 96, 86); g2.fillStyle = "#8b8f96"; g2.beginPath(); g2.arc(64, 78, 16, 0, Math.PI * 2); g2.fill(); g2.fillStyle = "#2a2c31"; g2.beginPath(); g2.arc(64, 78, 10, 0, Math.PI * 2); g2.fill(); g2.fillStyle = "#8b8f96"; g2.fillRect(96, 66, 8, 24); } else { // warpglow: a pillar of violet light, brightest at its core. t.width = 64; t.height = 128; const g2 = t.getContext("2d")!; const grad = g2.createLinearGradient(0, 0, 64, 0); grad.addColorStop(0, "rgba(140,80,220,0)"); grad.addColorStop(0.5, "rgba(200,160,255,0.75)"); grad.addColorStop(1, "rgba(140,80,220,0)"); g2.fillStyle = grad; g2.fillRect(0, 0, 64, 128); } baked.set(name, t); return t; }