{"project":{"id":"AFckR3t","userId":"davidyarham@gmail.com","username":null,"userPicture":null,"name":"Touch 'n' Go","thumbnail":"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","visible":true,"contributors":"","githubRepo":null,"forkedFrom":null,"isTemplate":false,"tags":"","files":{"folder":"","files":[{"name":"index.html","content":"<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>Touch 'n' Go — Voxel Remake</title>\n  <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n  <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n  <link href=\"https://fonts.googleapis.com/css2?family=Unbounded:wght@500;700;900&family=Manrope:wght@400;600;700;800&display=swap\" rel=\"stylesheet\">\n  <link rel=\"stylesheet\" href=\"css/styles.css\">\n  <script type=\"importmap\">\n  {\n    \"imports\": {\n      \"three\": \"https://cdn.jsdelivr.net/npm/three@0.164.1/build/three.module.js\",\n      \"three/addons/\": \"https://cdn.jsdelivr.net/npm/three@0.164.1/examples/jsm/\"\n    }\n  }\n  </script>\n</head>\n<body>\n  <canvas id=\"game\"></canvas>\n\n  <header id=\"hud\">\n    <div class=\"stat\"><label>Score</label><span id=\"hud-score\">000000</span></div>\n    <div class=\"stat\"><label>Level</label><span id=\"hud-level\">1/40</span></div>\n    <div class=\"stat\"><label>Blocks</label><span id=\"hud-blocks\">0</span></div>\n    <div class=\"stat\"><label>Combo</label><span id=\"hud-combo\">&mdash;</span></div>\n    <div class=\"stat\"><label>Lives</label><span id=\"hud-lives\"></span></div>\n    <div class=\"stat grow\" id=\"hud-timewrap\"><label>Time</label>\n      <div class=\"timebar\"><div id=\"hud-time\" class=\"ok\"></div></div>\n    </div>\n  </header>\n\n  <div id=\"popups\"></div>\n  <div id=\"screen\"></div>\n\n  <script>import('./js/main.js').catch(e => console.error('Game failed to load:', e));</script>\n</body>\n</html>\n"},{"name":"README.md","content":"# Touch 'n' Go — Voxel Remake\n\nA full-screen 3D voxel remake of **Touch 'n' Go** (F1 Licenceware, Amiga, 1994),\nbuilt with three.js and set at a luminous Monument-Valley dusk. Based on the\ngame shown in this longplay: https://www.youtube.com/watch?v=muUprwejK70\n\n## The goal\n**Demolish every block, then land on the gold exit pad.** Each hop off a tile\nbreaks its top layer away. Stacks come in 1-touch (teal), 2-touch (amber) and\n3-touch (rose) variants — height shows visits remaining. The exit pad never\ncrumbles: cross it freely as a safe hub, but the level only ends when you land\non it with the board empty. Its beacon blazes gold when the last block falls.\n\n## Guaranteed-solvable levels\nAll **40 levels** are authored as *solution paths*: the board is generated by\nreplaying the path, each tile's layer count equals the number of times the path\nvisits it, and the path's final cell becomes the exit. Every level is therefore\nprovably 100% clearable — verified by an automated full-campaign playthrough.\nDifficulty climbs from a 9-block tutorial to ASCENSION's 94-block sprawl, with\nthe pace tightening from ~1.9s to ~0.7s per move.\n\n## Presentation\nVoxel mesher with baked vertex AO, edge glint, per-voxel jitter and painted\nlight temperature. ACES tone mapping, UnrealBloom, tilt-shift depth of field,\nlong dusk shadows, a calm gradient sky with a low sun and floating ruins.\nEvery fresh level opens with a **5-second flyover preview** (skippable, clock\npaused). A generative score — drone, pad swells and a kalimba improvising\nthrough a feedback echo — shifts mood across five acts of the campaign.\n\n## Endless mode\nPress **E** on the title for procedurally generated stages, forever — built by\nthe same path-authoring math as the campaign, so every generated board is\nprovably clearable. Stages grow, the pace tightens, and the sky changes every\nstage. Best stage reached is saved.\n\n## Pickups\nTall stacks sometimes carry a spinning **time crystal** (+4s on touch) and some\nblocks hide a **gem** (+150 when torn free) — placed deterministically, so\nroutes are learnable.\n\n## Progression & scoring\nProgress persists: cleared levels unlock in a **level select** (L on the title),\nand each clear is graded against the authored optimal route — **PERFECT /\nGOLD / SILVER / BRONZE** medals are saved per level (PERFECT pays +1000).\nChained moves build a **combo** that multiplies block score up to ×10, with\nfloating score popups and pops that ring higher as the chain grows. Every\nlevel opens in a free **survey orbit** — the clock starts on your first move.\n\n## Controls\n- **Arrows / WASD** — roll (swipe on touch screens); first move starts the clock\n- **Space / Enter / Tap** — start, confirm\n- **L** — level select (on the title screen)\n- **P / Esc** — pause\n- **R** — sacrifice a life and restart the level\n- **M** — toggle all sound & music\n\nDebug: `TNG.info`, `TNG.warp(n)`, `TNG.solve()`.\n"}],"folders":[{"folder":"css","files":[{"name":"styles.css","content":"/* Touch 'n' Go — luminous dusk. UI floats on the sky, nothing is boxed. */\n* { margin: 0; padding: 0; box-sizing: border-box; }\n\n:root {\n  --cream: #fff6e8;\n  --cream-dim: rgba(255, 244, 226, 0.62);\n  --teal: #2fb5a3;\n  --amber: #ffb038;\n  --coral: #ff6a7d;\n  --twilight: #2e2a55;\n  --shadow-soft: 0 2px 26px rgba(20, 14, 34, 0.7), 0 1px 2px rgba(16, 10, 28, 0.8);\n  --font-display: \"Unbounded\", system-ui, sans-serif;\n  --font-body: \"Manrope\", system-ui, sans-serif;\n}\n\nhtml, body {\n  height: 100%;\n  overflow: hidden;\n  background: linear-gradient(180deg, #16143a 0%, #453a6a 40%, #8f5548 74%, #b3805e 100%);\n  font-family: var(--font-body);\n  color: var(--cream);\n  user-select: none;\n  -webkit-user-select: none;\n}\n\n#game {\n  position: fixed;\n  inset: 0;\n  width: 100vw;\n  height: 100vh;\n  display: block;\n  touch-action: none;\n}\n\n/* a deeper vignette — the frame holds the dark */\nbody::after {\n  content: '';\n  position: fixed;\n  inset: 0;\n  pointer-events: none;\n  background: radial-gradient(ellipse at 50% 44%, transparent 58%, rgba(16, 11, 30, 0.5) 100%);\n  z-index: 1;\n}\n\n/* ---------- HUD: quiet typography floating on the twilight ---------- */\n#hud {\n  position: fixed;\n  top: 0; left: 0; right: 0;\n  display: flex;\n  align-items: center;\n  gap: clamp(18px, 3.4vw, 46px);\n  padding: 18px clamp(18px, 4.5vw, 48px) 10px;\n  pointer-events: none;\n  z-index: 5;\n}\n\n.stat label {\n  display: block;\n  font-size: 10px;\n  font-weight: 700;\n  letter-spacing: 0.3em;\n  text-transform: uppercase;\n  color: var(--cream-dim);\n  text-shadow: 0 1px 12px rgba(46, 32, 68, 0.5);\n}\n\n.stat span {\n  font-family: var(--font-body);\n  font-weight: 800;\n  font-size: clamp(16px, 2.2vw, 22px);\n  line-height: 1.3;\n  letter-spacing: 0.02em;\n  color: var(--cream);\n  text-shadow: var(--shadow-soft);\n  font-variant-numeric: tabular-nums;\n}\n\n.stat.grow { flex: 1; max-width: 400px; margin-left: auto; }\n\n.heart { color: var(--coral); font-size: 17px; margin-right: 3px; text-shadow: 0 1px 10px rgba(46,32,68,0.55); }\n.heart.off { color: rgba(255, 244, 226, 0.22); }\n\n.timebar {\n  margin-top: 6px;\n  height: 8px;\n  border-radius: 4px;\n  background: rgba(46, 42, 85, 0.35);\n  box-shadow: inset 0 1px 2px rgba(30, 20, 50, 0.4);\n  overflow: hidden;\n}\n#hud-time {\n  height: 100%;\n  width: 100%;\n  border-radius: 4px;\n  transition: width 0.15s linear;\n}\n#hud-time.ok   { background: linear-gradient(90deg, #1f9184, var(--teal)); }\n#hud-time.warn { background: linear-gradient(90deg, #d98f1c, var(--amber)); }\n#hud-time.low  { background: linear-gradient(90deg, #d33a52, var(--coral)); animation: pulse 0.5s infinite; }\n\n@keyframes pulse { 50% { filter: brightness(1.5); } }\n\n/* ---------- screens: type set straight onto the sky ---------- */\n#screen {\n  position: fixed;\n  inset: 0;\n  display: none;\n  align-items: center;\n  justify-content: center;\n  z-index: 10;\n  pointer-events: none;\n  padding: 24px;\n}\n\n.panel {\n  text-align: center;\n  max-width: min(94vw, 760px);\n  animation: rise-in 0.7s cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n@keyframes rise-in {\n  from { transform: translateY(22px); opacity: 0; }\n  to   { transform: translateY(0); opacity: 1; }\n}\n\n.panel h1 {\n  font-family: var(--font-display);\n  font-weight: 900;\n  font-size: clamp(2.2rem, 7vw, 4.6rem);\n  line-height: 1.04;\n  letter-spacing: 0.01em;\n  text-transform: uppercase;\n  color: var(--cream);\n  text-shadow: 0 3px 0 rgba(60, 30, 42, 0.5), 0 12px 48px rgba(16, 11, 30, 0.7);\n}\n.panel h1 em {\n  font-style: normal;\n  color: var(--amber);\n}\n.panel h1.over { color: var(--coral); }\n\n.panel h2 {\n  font-family: var(--font-display);\n  font-weight: 700;\n  font-size: clamp(1.3rem, 3.6vw, 2.2rem);\n  letter-spacing: 0.02em;\n  text-transform: uppercase;\n  text-shadow: var(--shadow-soft);\n}\n.panel.good h2 { color: #7dffe4; }\n.panel.bad h2 { color: #ffb9c2; }\n\n.sub {\n  margin-top: 14px;\n  font-size: clamp(10px, 1.5vw, 13px);\n  font-weight: 700;\n  letter-spacing: 0.38em;\n  text-transform: uppercase;\n  color: var(--cream-dim);\n  text-shadow: 0 1px 14px rgba(46, 32, 68, 0.6);\n}\n\n.rule {\n  margin: 26px auto 4px;\n  max-width: 34em;\n  font-size: clamp(15px, 2vw, 18px);\n  font-weight: 500;\n  line-height: 1.7;\n  color: var(--cream);\n  text-shadow: 0 1px 16px rgba(46, 32, 68, 0.65);\n}\n.rule b { color: #ffd98c; font-weight: 700; }\n\n.lvlname {\n  margin-top: 10px;\n  font-family: var(--font-display);\n  font-weight: 700;\n  font-size: clamp(1rem, 2.6vw, 1.5rem);\n  letter-spacing: 0.04em;\n  text-transform: uppercase;\n  color: var(--amber);\n  text-shadow: var(--shadow-soft);\n}\n\n.keys {\n  margin-top: 22px;\n  font-size: 11px;\n  font-weight: 600;\n  letter-spacing: 0.24em;\n  color: var(--cream-dim);\n  text-shadow: 0 1px 12px rgba(46, 32, 68, 0.55);\n}\n\n.blink {\n  margin-top: 26px;\n  font-family: var(--font-body);\n  font-weight: 800;\n  font-size: clamp(0.8rem, 1.9vw, 1.05rem);\n  letter-spacing: 0.3em;\n  text-transform: uppercase;\n  color: var(--cream);\n  text-shadow: var(--shadow-soft);\n  animation: breathe 2s ease-in-out infinite;\n}\n@keyframes breathe { 50% { opacity: 0.35; } }\n\n.hi {\n  margin-top: 16px;\n  font-size: 11px;\n  font-weight: 700;\n  letter-spacing: 0.34em;\n  color: var(--cream-dim);\n  text-shadow: 0 1px 12px rgba(46, 32, 68, 0.55);\n}\n\n/* title type rides high; the board breathes beneath it */\n.panel.title-panel {\n  align-self: flex-start;\n  margin-top: max(5vh, 26px);\n}\n\n/* level preview rides high too — the flyover needs the frame */\n.panel.preview {\n  align-self: flex-start;\n  margin-top: max(4vh, 18px);\n}\n\n.countdown {\n  margin-top: 12px;\n  font-family: var(--font-display);\n  font-weight: 900;\n  font-size: clamp(2.2rem, 5.5vw, 3.6rem);\n  line-height: 1;\n  color: var(--amber);\n  text-shadow: var(--shadow-soft);\n}\n\n/* ---------- floating score popups ---------- */\n#popups { position: fixed; inset: 0; pointer-events: none; z-index: 6; }\n.pop-score {\n  position: absolute;\n  transform: translate(-50%, -50%);\n  font-family: var(--font-body);\n  font-weight: 800;\n  font-size: 15px;\n  color: var(--cream);\n  text-shadow: 0 1px 10px rgba(16, 10, 28, 0.8);\n  animation: pop-rise 0.85s ease-out forwards;\n}\n.pop-score.hot { color: var(--amber); font-size: 18px; }\n.pop-score.blazing { color: var(--coral); font-size: 21px; }\n@keyframes pop-rise {\n  0%   { opacity: 0; transform: translate(-50%, -30%) scale(0.7); }\n  15%  { opacity: 1; transform: translate(-50%, -50%) scale(1.1); }\n  100% { opacity: 0; transform: translate(-50%, -170%) scale(1); }\n}\n\n/* ---------- combo heat in the HUD ---------- */\n#hud-combo.hot { color: var(--amber); }\n#hud-combo.blazing { color: var(--coral); animation: pulse 0.4s infinite; }\n\n/* ---------- medal chips ---------- */\n.medal {\n  display: inline-block;\n  padding: 3px 12px;\n  border-radius: 999px;\n  font-size: 11px;\n  font-weight: 800;\n  letter-spacing: 0.22em;\n  margin: 2px;\n}\n.medal.m4 { background: rgba(255, 210, 58, 0.22); color: #ffd23a; box-shadow: 0 0 18px rgba(255, 210, 58, 0.35); }\n.medal.m3 { background: rgba(255, 176, 56, 0.18); color: #ffb038; }\n.medal.m2 { background: rgba(220, 226, 240, 0.16); color: #dfe4f0; }\n.medal.m1 { background: rgba(205, 130, 90, 0.2); color: #e0a17a; }\n\n/* ---------- level select ---------- */\n.panel.select { pointer-events: auto; max-width: min(94vw, 720px); }\n.level-grid {\n  display: grid;\n  grid-template-columns: repeat(10, 1fr);\n  gap: 8px;\n  margin-top: 22px;\n}\n.lvl-btn {\n  appearance: none;\n  border: 1px solid rgba(255, 244, 226, 0.22);\n  background: rgba(20, 14, 34, 0.42);\n  color: var(--cream);\n  border-radius: 10px;\n  padding: 9px 0 7px;\n  font-family: var(--font-body);\n  font-weight: 800;\n  font-size: 13px;\n  cursor: pointer;\n  transition: transform 0.12s, background 0.12s, border-color 0.12s;\n}\n.lvl-btn:hover { transform: translateY(-2px); background: rgba(47, 181, 163, 0.25); border-color: var(--teal); }\n.lvl-btn:disabled { opacity: 0.28; cursor: default; transform: none; }\n.lvl-btn .dot { display: block; height: 5px; width: 5px; border-radius: 50%; margin: 4px auto 0; background: transparent; }\n.lvl-btn .dot.m4 { background: #ffd23a; box-shadow: 0 0 8px #ffd23a; }\n.lvl-btn .dot.m3 { background: #ffb038; }\n.lvl-btn .dot.m2 { background: #dfe4f0; }\n.lvl-btn .dot.m1 { background: #e0a17a; }\n\n/* ---------- danger: the last five seconds burn at the frame ---------- */\nbody.danger::after {\n  background: radial-gradient(ellipse at 50% 44%, transparent 52%, rgba(140, 30, 40, 0.5) 100%);\n  animation: danger-breathe 0.5s ease-in-out infinite;\n}\n@keyframes danger-breathe { 50% { opacity: 0.55; } }\n"}],"folders":[]},{"folder":"js","files":[{"name":"main.js","content":"import * as THREE from 'three';\nimport { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';\nimport { RenderPass } from 'three/addons/postprocessing/RenderPass.js';\nimport { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';\nimport { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';\nimport { OutputPass } from 'three/addons/postprocessing/OutputPass.js';\nimport { RGBELoader } from 'three/addons/loaders/RGBELoader.js';\nimport {\n  VOID_DEEP, MINT, AMBER, CORAL, BONE, GOLD, BALL_R,\n  LAYER_TOP_COLORS, LAYER_DEEP_COLORS,\n  buildLayerSlab, buildBallModel, buildExitSlab, makeDebrisVoxel, getMaterials,\n} from './voxels.js';\n\n/* ============================================================\n   TOUCH 'N' GO — voxel remake of the 1994 Amiga action-puzzler.\n   Clear EVERY block: each hop off a tile breaks one layer away.\n   Levels are authored as solution paths, so every board is\n   provably 100% clearable.\n   ============================================================ */\n\n/* ---------- Levels (start + solution path; visits = layers) ----------\n   Every board is generated by replaying its path, so every level is\n   provably 100% clearable. Difficulty rises: bigger boards, more\n   multi-touch stacks, and a pace that tightens from ~1.9s to ~0.86s\n   per move across the campaign. ---------- */\nconst LEVELS = [\n  { name: 'FIRST STEPS',    time: 15, start: [0, 0], path: 'RRDLLDRR' },\n  { name: 'THE RIVER',      time: 22, start: [0, 0], path: 'RRDRRDDRRURRDDDRR' },\n  { name: 'SIDE POCKET',    time: 20, start: [0, 0], path: 'RRDURRDDLLLLU' },\n  { name: 'CROSSROADS',     time: 20, start: [2, 0], path: 'DDLLRRRRLLDD' },\n  { name: 'THE SPIRAL',     time: 30, start: [0, 0], path: 'DDRRRRDDDDLLLLUUURRRDDLLUR' },\n  { name: 'GRAND CROSS',    time: 24, start: [3, 0], path: 'DDDLLLRRRRRRLLLDDD' },\n  { name: 'FIGURE EIGHT',   time: 20, start: [2, 0], path: 'RRDDLLUULLDDRRUU' },\n  { name: 'PINWHEEL',       time: 18, start: [2, 0], path: 'DDRRDLLUDDLUUR' },\n  { name: 'BATTLEMENTS',    time: 24, start: [0, 1], path: 'RUDRUDRUDRUDRUDDLLLLL' },\n  { name: 'SWITCHBACK',     time: 18, start: [0, 1], path: 'RUDRDURUDRDURUDR' },\n  { name: 'TWIN MESAS',     time: 36, start: [0, 0], path: 'RRDLLDRRLLURRULLUURRULLURRLLDRRDLL' },\n  { name: 'BREAKOUT',       time: 32, start: [0, 0], path: 'RRRRDDDDLLLLUUURRRDDLLURUURRDRDD' },\n  { name: 'CATHEDRAL',      time: 24, start: [3, 0], path: 'DDDLLLUDRRRRRRDULLLDDD' },\n  { name: 'THE GAUNTLET',   time: 36, start: [0, 0], path: 'RRRRRDLLLLLDRRURDRURDDLLLLLDRRRRRDRR' },\n  { name: 'ARCHIPELAGO',    time: 28, start: [0, 0], path: 'RRDLLDRRRRRRULLURRRRRRDLLDRR' },\n  { name: 'NINE TOWERS',    time: 24, start: [0, 0], path: 'RRDLLDRRLLURRULLRRDLLDRR' },\n  { name: 'LONG RIVER',     time: 34, start: [0, 0], path: 'RRDRRUDRRDDLLDDLRDRRRUURRUDRRDDRR' },\n  { name: 'RAMPARTS',       time: 36, start: [0, 2], path: 'RRUUDDRRRUUUDDDRRRUUUUDDDDRRRUUDDRR' },\n  { name: 'ZENITH',         time: 45, start: [0, 0], path: 'DDDRUUURDDDRUUULLLDRRRDLLLDRRRUUULDDDLUUULDDD' },\n  { name: 'THE PILGRIMAGE', time: 55, start: [0, 0], path: 'RRDRRDDRRUDRRDDRRDLLDRRLLURRULLULLDDLLUUDDLLUUULLDDDRRDRRRRR' },\n  // ---- act two: the far shore ----\n  { name: 'DELTA',          time: 28, start: [0, 0], path: 'RRDRRDDUDRRDRRDDLRRRDRRDDUDRR' },\n  { name: 'MONOLITH',       time: 30, start: [0, 0], path: 'RRDRRRRRRDLLLLRRRRULLLLRRRRDLLLL' },\n  { name: 'FOUR MESAS',     time: 35, start: [0, 0], path: 'RRDLLDRRRRRRDLLDRRDDRRDLLDRRRRRRDLLDRR' },\n  { name: 'LABYRINTH',      time: 35, start: [0, 0], path: 'RRRRRRDDLLLLDDRRRRUURRDDDDLLLLLLUULLUUU' },\n  { name: 'THE CRUCIBLE',   time: 32, start: [3, 0], path: 'DDDLLLUDRRRRRRDULLLDDDLLUDRRRRDULLDD' },\n  { name: 'PENINSULAS',     time: 34, start: [0, 0], path: 'RRDDDUUURRDDDDUUUURRDDDDDUUUUURRDDDUUURR' },\n  { name: 'TWIN CLAWS',     time: 34, start: [0, 0], path: 'DDLLLUDRRRRRRDULLLDDLLLUDRRRRRRDULLLDDRR' },\n  { name: 'THE CHASM',      time: 34, start: [0, 0], path: 'RRDRRDDLLLLDLLUUURRURRDRRDDLLLLDLLUUURRUU' },\n  { name: 'CAUSEWAY',       time: 35, start: [0, 0], path: 'RRUDRRRUDRRRLLLLLLLLDRRDURRRDURRRDLLLLLLLL' },\n  { name: 'ATOLL',          time: 34, start: [0, 0], path: 'RRRRRDDDDDLLLLLUUUUURRRRRDDDDDLLLLLUUUUULL' },\n  { name: 'STEPPES',        time: 36, start: [0, 0], path: 'RRRRLLLDRRRRLLLDRRRRLLLDRRRRLLLDRRRRLLLDRRRR' },\n  { name: 'GREAT WALL',     time: 38, start: [0, 0], path: 'RRUUDDRRRUUUDDDRRRUUUUDDDDRRRUUUUUDDDDDRRRUUDDRR' },\n  { name: 'THE FORGE',      time: 32, start: [0, 0], path: 'RRDLLDRRLLURRULLRRDLLDRRRDLLLLUUUURRRRDDD' },\n  { name: 'GRAND CANAL',    time: 30, start: [0, 0], path: 'RRRRRRRRRRLLLLLLLLLLRRRRRRRRRRDDLLLL' },\n  { name: 'DOUBLE HELIX',   time: 40, start: [0, 0], path: 'DDRRRRDDDDLLLLUUURRRDDLLURLDRRUULLLDDDRRRRUUUULLLLUU' },\n  { name: 'OUTLANDS',       time: 39, start: [0, 0], path: 'RRDRRDDUDRRRRDDRRDLLDRRLLURRULLULLLLDDDLLUULLUUDDLL' },\n  { name: 'THE REEF',       time: 38, start: [0, 0], path: 'RRDLLDRRRRRRDLLDRRLLURRULLDDRRDLLDRRRRUUDDRR' },\n  { name: 'THE SUMMIT',     time: 37, start: [0, 0], path: 'RRUUDDRRRUUUDDDRRRUUUUDDDDDDRRDLLDRRLLURRULLRRDLLDRR' },\n  { name: 'NIGHTFALL',      time: 45, start: [0, 0], path: 'DDDRUUURDDDRUUULLLDRRRDLLLDRRRUUULDDDLUUULDDDDDRRDLLDRRLLURRULL' },\n  { name: 'ASCENSION',      time: 69, start: [0, 0], path: 'RRDRRDDRRUDRRRRRRRRLLLLLLLLDDRRDLLDRRLLURRULLLLLDDRRDLLDRRLLURRULLRRDLLDRRRDLLLLUUUURRRRDDDDRR' },\n];\nconst DIRS = { R: [1, 0], L: [-1, 0], U: [0, -1], D: [0, 1] };\n\nconst LAYER_H = 0.268;   // 2 voxels of 0.134\nconst HOP_T = 0.17;\n\n/* ---------- DOM ---------- */\nconst $ = (id) => document.getElementById(id);\nconst el = {\n  score: $('hud-score'), level: $('hud-level'), blocks: $('hud-blocks'),\n  combo: $('hud-combo'), lives: $('hud-lives'), time: $('hud-time'), hud: $('hud'),\n  screen: $('screen'), popups: $('popups'),\n};\n\n/* ---------- renderer / composer ---------- */\nconst renderer = new THREE.WebGLRenderer({ canvas: $('game'), antialias: true });\nrenderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));\nrenderer.setSize(window.innerWidth, window.innerHeight);\nrenderer.shadowMap.enabled = true;\nrenderer.shadowMap.type = THREE.VSMShadowMap; // blurrable, painterly-soft shadows\nrenderer.toneMapping = THREE.ACESFilmicToneMapping; // richer golds, deeper voids\nrenderer.toneMappingExposure = 0.94; // moodier — let the darks be dark\n\nconst scene = new THREE.Scene();\nscene.fog = new THREE.Fog(0xa06552, 13, 42); // themed haze — geometry melts into it\nconst SUN_DIR = new THREE.Vector3(0.55, 0.26, 0.79).normalize();\n\nconst camera = new THREE.PerspectiveCamera(44, window.innerWidth / window.innerHeight, 0.1, 200);\n\n// MSAA render target — the post chain keeps real anti-aliasing (WebGL2)\nconst composer = new EffectComposer(renderer, new THREE.WebGLRenderTarget(\n  window.innerWidth, window.innerHeight, { samples: 4, type: THREE.HalfFloatType }\n));\ncomposer.addPass(new RenderPass(scene, camera));\nconst bloomPass = new UnrealBloomPass(\n  new THREE.Vector2(window.innerWidth, window.innerHeight),\n  0.38,  // strength — a halo, not a smear\n  0.35,  // radius — tight, voxel edges stay crisp\n  0.85   // threshold — only genuinely bright things bloom\n);\ncomposer.addPass(bloomPass);\n\n// tilt-shift depth of field — the board reads like a hand-built miniature.\n// Strong on the title diorama, a whisper during play.\nfunction makeTiltShiftPass(dx, dy) {\n  return new ShaderPass({\n    uniforms: {\n      tDiffuse: { value: null },\n      uTexel: { value: new THREE.Vector2(1 / window.innerWidth, 1 / window.innerHeight) },\n      uDir: { value: new THREE.Vector2(dx, dy) },\n      uAmount: { value: 0 },\n      uFocus: { value: 0.46 },\n      uBand: { value: 0.17 },\n    },\n    vertexShader: /* glsl */`\n      varying vec2 vUv;\n      void main() {\n        vUv = uv;\n        gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n      }`,\n    fragmentShader: /* glsl */`\n      uniform sampler2D tDiffuse;\n      uniform vec2 uTexel, uDir;\n      uniform float uAmount, uFocus, uBand;\n      varying vec2 vUv;\n      void main() {\n        float d = abs(vUv.y - uFocus);\n        float amt = uAmount * smoothstep(uBand * 0.4, uBand * 2.4, d);\n        vec2 s = uDir * uTexel * amt;\n        vec4 c = texture2D(tDiffuse, vUv) * 0.227;\n        c += (texture2D(tDiffuse, vUv + s * 1.384) + texture2D(tDiffuse, vUv - s * 1.384)) * 0.316;\n        c += (texture2D(tDiffuse, vUv + s * 3.230) + texture2D(tDiffuse, vUv - s * 3.230)) * 0.0705;\n        gl_FragColor = c;\n      }`,\n  });\n}\nconst tiltH = makeTiltShiftPass(1, 0);\nconst tiltV = makeTiltShiftPass(0, 1);\ncomposer.addPass(tiltH);\ncomposer.addPass(tiltV);\ncomposer.addPass(new OutputPass());\nlet tiltAmount = 0, tiltTarget = 3.2;\nlet tiltFocus = 0.46, tiltFocusTarget = 0.46;\nlet skyTilt = -0.08, skyTiltTarget = -0.08; // panorama pitch, follows camera angle\n\n/* ---------- the sky dome: a themed 360 gradient sky, one per act ---------- */\nfunction makeVoidDome() {\n  const geo = new THREE.SphereGeometry(90, 48, 32);\n  const mat = new THREE.ShaderMaterial({\n    side: THREE.BackSide, depthWrite: false, fog: false,\n    uniforms: {\n      uSunDir: { value: SUN_DIR.clone() },\n      uTop:   { value: new THREE.Color(0.010, 0.008, 0.048) },\n      uVio:   { value: new THREE.Color(0.070, 0.042, 0.150) },\n      uRose:  { value: new THREE.Color(0.340, 0.100, 0.082) },\n      uCream: { value: new THREE.Color(0.640, 0.310, 0.120) },\n      uUnder: { value: new THREE.Color(0.110, 0.045, 0.042) },\n      uSunCol:{ value: new THREE.Color(0.28, 0.14, 0.04) },\n    },\n    vertexShader: /* glsl */`\n      varying vec3 vDir;\n      void main() {\n        vDir = normalize(position);\n        gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n      }`,\n    fragmentShader: /* glsl */`\n      uniform vec3 uSunDir, uTop, uVio, uRose, uCream, uUnder;\n      uniform vec3 uSunCol;\n      varying vec3 vDir;\n      void main() {\n        float h = vDir.y;\n        vec3 col;\n        if (h > 0.42)       col = mix(uVio, uTop, smoothstep(0.42, 1.0, h));\n        else if (h > 0.10)  col = mix(uRose, uVio, smoothstep(0.10, 0.42, h));\n        else if (h > -0.06) col = mix(uCream, uRose, smoothstep(-0.06, 0.10, h));\n        else                col = mix(uUnder, uCream, smoothstep(-0.7, -0.06, h));\n        // bloom around the low sun\n        float d = max(0.0, dot(normalize(vDir), uSunDir));\n        col += uSunCol * pow(d, 7.0);\n        col += uSunCol * 1.7 * pow(d, 28.0);\n        gl_FragColor = vec4(col, 1.0);\n      }`,\n  });\n  const mesh = new THREE.Mesh(geo, mat);\n  mesh.renderOrder = -10;\n  return mesh;\n}\nconst dome = makeVoidDome();\nscene.add(dome);\n\n/* ---------- first stars, high in the twilight only ---------- */\n{\n  const N = 170, pos = new Float32Array(N * 3);\n  for (let i = 0; i < N; i++) {\n    const r = 60 + Math.random() * 20;\n    const a = Math.random() * Math.PI * 2;\n    const b = 0.35 + Math.random() * 1.1; // elevation: upper sky only\n    pos[i * 3] = Math.cos(a) * Math.cos(b) * r;\n    pos[i * 3 + 1] = Math.sin(b) * r;\n    pos[i * 3 + 2] = Math.sin(a) * Math.cos(b) * r;\n  }\n  const g = new THREE.BufferGeometry();\n  g.setAttribute('position', new THREE.BufferAttribute(pos, 3));\n  var starsMat = new THREE.PointsMaterial({\n    color: 0xfff2dd, size: 1.4, sizeAttenuation: false,\n    transparent: true, opacity: 0.55, fog: false, depthWrite: false,\n  });\n  var starsObj = new THREE.Points(g, starsMat);\n  starsObj.renderOrder = -9;\n  scene.add(starsObj);\n}\n\n/* ---------- real 360 skybox photos: a fresh sky every five levels ----------\n   Equirectangular HDRs streamed from Poly Haven. The sky is a real ball —\n   a big textured sphere the camera moves INSIDE, so orbiting produces\n   gentle parallax. The gradient dome stands in while each photo streams. */\nconst skyMat = new THREE.MeshBasicMaterial({ side: THREE.BackSide, fog: false, depthWrite: false });\nconst skySphere = new THREE.Mesh(new THREE.SphereGeometry(55, 64, 40), skyMat);\nskySphere.renderOrder = -10;\nskySphere.visible = false;\nscene.add(skySphere);\n\nconst skyCache = new Map(); // url -> texture\nlet skyWantedUrl = null;\nconst rgbeLoader = new RGBELoader();\nfunction useSky(url, intensity) {\n  skyMat.map = skyCache.get(url);\n  skyMat.color.setScalar(intensity);\n  skyMat.needsUpdate = true;\n  skySphere.visible = true;\n  dome.visible = false;      // photo replaces the gradient (sun + clouds ride the dome)\n  starsObj.visible = false;  // the photos bring their own stars\n}\nfunction requestSky(url, intensity) {\n  skyWantedUrl = url;\n  if (skyCache.has(url)) { useSky(url, intensity); return; }\n  skySphere.visible = false; // gradient dome stands in while the photo streams\n  dome.visible = true;\n  starsObj.visible = true;\n  rgbeLoader.load(url, (tex) => {\n    skyCache.set(url, tex);\n    if (skyWantedUrl === url) useSky(url, intensity);\n  }, undefined, () => { /* stay on the procedural sky if the stream fails */ });\n}\n\n/* ---------- lights: one low key, themed sky fill ---------- */\nconst keyLight = new THREE.DirectionalLight(0xffd9a0, 1.55);\nkeyLight.position.copy(SUN_DIR).multiplyScalar(16); // low sun — long raking shadows\nkeyLight.castShadow = true;\nkeyLight.shadow.mapSize.set(2048, 2048); // VSM blur hides the resolution anyway\nkeyLight.shadow.bias = -0.0002;\nkeyLight.shadow.normalBias = 0.02; // kills voxel-face shadow acne\nkeyLight.shadow.radius = 5;        // soft, slightly dreamy edges\nkeyLight.shadow.blurSamples = 16;\nkeyLight.shadow.camera.left = -14; keyLight.shadow.camera.right = 14;\nkeyLight.shadow.camera.top = 14; keyLight.shadow.camera.bottom = -14;\nscene.add(keyLight);\nconst hemiLight = new THREE.HemisphereLight(0xffd9ad, 0x8f6a86, 0.42);\nscene.add(hemiLight);\nconst ambLight = new THREE.AmbientLight(0x6a5a7a, 0.26); // lean — shadows keep their depth\nscene.add(ambLight);\nconst rimLight = new THREE.DirectionalLight(0x9a8fd8, 0.55); // cool rim off the twilight\nrimLight.position.set(-10, 7, -11);\nscene.add(rimLight);\n\n/* ---------- soft round sprite + glow halos ---------- */\nfunction makeSoftDot() {\n  const c = document.createElement('canvas');\n  c.width = c.height = 64;\n  const g = c.getContext('2d');\n  const grad = g.createRadialGradient(32, 32, 0, 32, 32, 32);\n  grad.addColorStop(0, 'rgba(255,255,255,1)');\n  grad.addColorStop(0.4, 'rgba(255,255,255,0.65)');\n  grad.addColorStop(1, 'rgba(255,255,255,0)');\n  g.fillStyle = grad;\n  g.fillRect(0, 0, 64, 64);\n  return new THREE.CanvasTexture(c);\n}\nconst softDot = makeSoftDot();\nfunction makeGlowSprite(color, scale, opacity) {\n  const s = new THREE.Sprite(new THREE.SpriteMaterial({\n    map: softDot, color, transparent: true, opacity,\n    blending: THREE.AdditiveBlending, depthWrite: false,\n  }));\n  s.scale.setScalar(scale);\n  return s;\n}\n\n/* ---------- gold spark bursts (additive) ---------- */\nconst SPARK_N = 220;\nconst sparkGeo = new THREE.BufferGeometry();\nconst sparkPos = new Float32Array(SPARK_N * 3);\nconst sparkVel = new Float32Array(SPARK_N * 3);\nconst sparkLife = new Float32Array(SPARK_N);\nsparkLife.fill(0);\nfor (let i = 0; i < SPARK_N; i++) sparkPos[i * 3 + 1] = -999;\nsparkGeo.setAttribute('position', new THREE.BufferAttribute(sparkPos, 3));\nscene.add(new THREE.Points(sparkGeo, new THREE.PointsMaterial({\n  color: GOLD, size: 0.14, transparent: true, opacity: 0.9,\n  map: softDot, blending: THREE.AdditiveBlending, depthWrite: false, sizeAttenuation: true,\n})));\nlet sparkCursor = 0;\nfunction burstSparks(x, y, z, n, speed) {\n  for (let k = 0; k < n; k++) {\n    const i = sparkCursor; sparkCursor = (sparkCursor + 1) % SPARK_N;\n    sparkPos[i * 3] = x; sparkPos[i * 3 + 1] = y; sparkPos[i * 3 + 2] = z;\n    const a = Math.random() * Math.PI * 2, up = Math.random();\n    sparkVel[i * 3] = Math.cos(a) * speed * (0.4 + Math.random() * 0.6);\n    sparkVel[i * 3 + 1] = speed * (0.5 + up);\n    sparkVel[i * 3 + 2] = Math.sin(a) * speed * (0.4 + Math.random() * 0.6);\n    sparkLife[i] = 0.5 + Math.random() * 0.45;\n  }\n}\nfunction updateSparks(dt) {\n  for (let i = 0; i < SPARK_N; i++) {\n    if (sparkLife[i] <= 0) continue;\n    sparkLife[i] -= dt;\n    sparkVel[i * 3 + 1] -= 7 * dt;\n    sparkPos[i * 3] += sparkVel[i * 3] * dt;\n    sparkPos[i * 3 + 1] += sparkVel[i * 3 + 1] * dt;\n    sparkPos[i * 3 + 2] += sparkVel[i * 3 + 2] * dt;\n    if (sparkLife[i] <= 0) sparkPos[i * 3 + 1] = -999;\n  }\n  sparkGeo.attributes.position.needsUpdate = true;\n}\n\n/* ---------- landing shockwave rings ---------- */\nconst RING_N = 6;\nconst ringGeo = new THREE.RingGeometry(0.5, 0.64, 26);\nconst rings = [];\nfor (let i = 0; i < RING_N; i++) {\n  const mesh = new THREE.Mesh(ringGeo, new THREE.MeshBasicMaterial({\n    color: 0xfff2dd, transparent: true, opacity: 0,\n    side: THREE.DoubleSide, depthWrite: false,\n  }));\n  mesh.rotation.x = -Math.PI / 2;\n  mesh.visible = false;\n  scene.add(mesh);\n  rings.push({ mesh, t: 1, str: 1 });\n}\nlet ringCursor = 0;\nfunction burstRing(x, y, z, strength) {\n  const r = rings[ringCursor];\n  ringCursor = (ringCursor + 1) % RING_N;\n  r.t = 0; r.str = strength;\n  r.mesh.position.set(x, y + 0.04, z);\n  r.mesh.visible = true;\n}\nfunction updateRings(dt) {\n  for (const r of rings) {\n    if (r.t >= 1) continue;\n    r.t = Math.min(1, r.t + dt * 3);\n    const s = (0.3 + r.t * 2) * r.str;\n    r.mesh.scale.set(s, s, s);\n    r.mesh.material.opacity = (1 - r.t) * 0.38;\n    if (r.t >= 1) r.mesh.visible = false;\n  }\n}\n\n/* ---------- the sun: a soft disc riding the sky (themed per act) ---------- */\nconst sunHalo = new THREE.Sprite(new THREE.SpriteMaterial({\n  map: softDot, color: 0xffdca0, transparent: true, opacity: 0.45,\n  blending: THREE.AdditiveBlending, depthWrite: false, fog: false,\n}));\nsunHalo.scale.setScalar(26);\nsunHalo.position.copy(SUN_DIR).multiplyScalar(72);\ndome.add(sunHalo); // rides the dome so it follows the camera like the sky\nconst sunCore = new THREE.Sprite(new THREE.SpriteMaterial({\n  map: softDot, color: 0xfff4dc, transparent: true, opacity: 0.9,\n  blending: THREE.AdditiveBlending, depthWrite: false, fog: false,\n}));\nsunCore.scale.setScalar(8);\nsunCore.position.copy(SUN_DIR).multiplyScalar(71);\ndome.add(sunCore);\n\n/* ---------- slow wisps of cloud near the horizon ---------- */\nconst clouds = [];\nfor (let i = 0; i < 6; i++) {\n  const c = new THREE.Sprite(new THREE.SpriteMaterial({\n    map: softDot, color: 0xfff0d8, transparent: true,\n    opacity: 0.09 + (i % 3) * 0.03, depthWrite: false, fog: false,\n  }));\n  c.scale.set(16 + (i % 3) * 6, 1.6 + (i % 2), 1);\n  dome.add(c);\n  clouds.push({ c, ang: (i / 6) * Math.PI * 2, r: 55, y: 3 + (i % 4) * 2.4, speed: 0.004 + (i % 3) * 0.002 });\n}\n\n/* ---------- haze glow far below the board (themed) ---------- */\nconst hazeGlow = new THREE.Sprite(new THREE.SpriteMaterial({\n  map: softDot, color: 0xffd9ad, transparent: true, opacity: 0.09,\n  blending: THREE.AdditiveBlending, depthWrite: false, fog: false,\n}));\nhazeGlow.scale.setScalar(34);\nhazeGlow.position.set(0, -15, 0);\nscene.add(hazeGlow);\n\n/* ---------- themed atmosphere: motes that belong to each sky ----------\n   embers rise in the fire act, snow falls through the grey veil,\n   fireflies drift at dusk. One system, retinted per theme. */\nconst THEME_MOTES = [\n  { color: 0xffd9a8, v: 0.14 },  // dawn — drifting pollen\n  { color: 0xffc98a, v: 0.11 },  // dusk — fireflies\n  { color: 0xff9a50, v: 0.33 },  // amber — rising embers\n  { color: 0xcfe0ff, v: -0.1 },  // moonlit — settling glitter\n  { color: 0xdfe8ff, v: 0.05 },  // starfield — still dust\n  { color: 0xff7a30, v: 0.5 },   // fire — embers\n  { color: 0xf0f4f8, v: -0.5 },  // grey veil — snow\n  { color: 0xa8ffd8, v: 0.18 },  // tempest — spores\n];\nconst MOTE_N = 90;\nconst moteGeo = new THREE.BufferGeometry();\nconst motePos = new Float32Array(MOTE_N * 3);\nfor (let i = 0; i < MOTE_N; i++) {\n  motePos[i * 3] = (Math.random() - 0.5) * 20;\n  motePos[i * 3 + 1] = -2.5 + Math.random() * 7;\n  motePos[i * 3 + 2] = (Math.random() - 0.5) * 20;\n}\nmoteGeo.setAttribute('position', new THREE.BufferAttribute(motePos, 3));\nconst moteMat = new THREE.PointsMaterial({\n  color: 0xffc98a, size: 0.055, transparent: true, opacity: 0.4,\n  map: softDot, blending: THREE.AdditiveBlending, depthWrite: false, sizeAttenuation: true,\n});\nscene.add(new THREE.Points(moteGeo, moteMat));\nlet moteV = 0.11;\nfunction updateMotes(dt) {\n  const p = moteGeo.attributes.position.array;\n  for (let i = 0; i < MOTE_N; i++) {\n    p[i * 3] += Math.sin(tick * 0.8 + i * 1.3) * 0.08 * dt;\n    p[i * 3 + 1] += moteV * (0.6 + (i % 5) * 0.2) * dt;\n    p[i * 3 + 2] += Math.cos(tick * 0.7 + i * 2.1) * 0.08 * dt;\n    if (p[i * 3 + 1] > 4.8) p[i * 3 + 1] = -2.5;\n    if (p[i * 3 + 1] < -2.6) p[i * 3 + 1] = 4.7;\n  }\n  moteGeo.attributes.position.needsUpdate = true;\n}\n\n/* ---------- soft clouds drifting far below the board ---------- */\nconst underClouds = [];\nfor (let i = 0; i < 4; i++) {\n  const c = new THREE.Sprite(new THREE.SpriteMaterial({\n    map: softDot, color: 0xe8a878, transparent: true, opacity: 0.07,\n    depthWrite: false, fog: false,\n  }));\n  c.scale.set(13 + (i % 2) * 5, 4 + (i % 3), 1);\n  scene.add(c);\n  underClouds.push({ c, ang: (i / 4) * Math.PI * 2, r: 5 + i * 2.4, speed: 0.01 + (i % 3) * 0.006 });\n}\n\n/* ---------- grounding: the maze casts real shadow into the sky below ----------\n   A transparent shadow-catcher plane floats beneath the board; only the\n   cast shadow renders, so the maze's silhouette drifts on the haze. */\nconst shadowCatcher = new THREE.Mesh(\n  new THREE.PlaneGeometry(44, 44),\n  new THREE.ShadowMaterial({ opacity: 0.32, transparent: true })\n);\nshadowCatcher.rotation.x = -Math.PI / 2;\nshadowCatcher.position.y = -2.2;\nshadowCatcher.receiveShadow = true;\nscene.add(shadowCatcher);\n\n// soft contact shade directly beneath the board — anchors it in the sky\nconst groundShadow = new THREE.Sprite(new THREE.SpriteMaterial({\n  map: softDot, color: 0x000000, transparent: true, opacity: 0.22,\n  depthWrite: false, fog: false,\n}));\ngroundShadow.position.set(0, -1.1, 0);\nscene.add(groundShadow);\n\nfunction updateFarBlocks(dt) {\n  for (const cl of clouds) {\n    cl.ang += cl.speed * dt;\n    cl.c.position.set(Math.cos(cl.ang) * cl.r, cl.y, Math.sin(cl.ang) * cl.r);\n  }\n  for (const uc of underClouds) {\n    uc.ang += uc.speed * dt;\n    uc.c.position.set(Math.cos(uc.ang) * uc.r, -5.2 - uc.r * 0.2, Math.sin(uc.ang) * uc.r);\n  }\n  updateMotes(dt);\n}\n\n/* ---------- sky themes: a different 360 sky every five levels ----------\n   Dome colours are linear-space; light/fog colours are sRGB hex.\n   Each theme carries its own HDRI url + brightness. */\nconst PH = 'https://dl.polyhaven.org/file/ph-assets/HDRIs/hdr/2k/';\nconst THEMES = [\n  { // L1-5 — FIRST LIGHT: a soft rose dawn\n    sky: PH + 'kiara_1_dawn_2k.hdr', bgI: 0.6,\n    top: [0.020, 0.014, 0.050], vio: [0.100, 0.060, 0.140], rose: [0.380, 0.150, 0.140],\n    cream: [0.700, 0.400, 0.280], under: [0.140, 0.070, 0.070], sunCol: [0.30, 0.18, 0.10],\n    fog: 0xa87878, key: 0xffc9a8, ki: 1.45, hemiS: 0xf2c9c0, hemiG: 0x7a5a6a, hi: 0.42,\n    amb: 0x5a4c60, ai: 0.26, rim: 0xb09ad8, ri: 0.5,\n    sun: 0xffd9c0, sunOp: 0.4, core: 0xfff4ea, star: 0.25, haze: 0xe8b09a,\n  },\n  { // L6-10 — DUSK: embers of the day\n    sky: PH + 'evening_road_01_puresky_2k.hdr', bgI: 0.55,\n    top: [0.010, 0.008, 0.048], vio: [0.070, 0.042, 0.150], rose: [0.340, 0.100, 0.082],\n    cream: [0.640, 0.310, 0.120], under: [0.110, 0.045, 0.042], sunCol: [0.28, 0.14, 0.04],\n    fog: 0xa06552, key: 0xffd9a0, ki: 1.55, hemiS: 0xffd9ad, hemiG: 0x7a5a72, hi: 0.42,\n    amb: 0x5a4c68, ai: 0.26, rim: 0x9a8fd8, ri: 0.55,\n    sun: 0xffc98a, sunOp: 0.45, core: 0xfff4dc, star: 0.5, haze: 0xe8a878,\n  },\n  { // L11-15 — AMBER HAZE: the sun sinks into smoke\n    sky: PH + 'industrial_sunset_puresky_2k.hdr', bgI: 0.55,\n    top: [0.014, 0.008, 0.020], vio: [0.110, 0.045, 0.060], rose: [0.380, 0.110, 0.045],\n    cream: [0.660, 0.260, 0.080], under: [0.120, 0.050, 0.025], sunCol: [0.36, 0.14, 0.03],\n    fog: 0x9c6a48, key: 0xffc084, ki: 1.5, hemiS: 0xe8b088, hemiG: 0x6a4a48, hi: 0.4,\n    amb: 0x503c40, ai: 0.28, rim: 0xc98aa0, ri: 0.45,\n    sun: 0xffab60, sunOp: 0.5, core: 0xffe8c0, star: 0.35, haze: 0xe09060,\n  },\n  { // L16-20 — MOONRISE: a cold moon over silvered cloud\n    sky: PH + 'moonlit_golf_2k.hdr', bgI: 1.05,\n    top: [0.001, 0.001, 0.006], vio: [0.006, 0.009, 0.040], rose: [0.020, 0.038, 0.120],\n    cream: [0.055, 0.095, 0.240], under: [0.008, 0.014, 0.045], sunCol: [0.10, 0.14, 0.28],\n    fog: 0x232c4d, key: 0xbfd4ff, ki: 1.3, hemiS: 0x6a7fc9, hemiG: 0x1c2440, hi: 0.4,\n    amb: 0x2a3050, ai: 0.34, rim: 0x8fa8ff, ri: 0.6,\n    sun: 0xcfe0ff, sunOp: 0.35, core: 0xf0f6ff, star: 0.9, haze: 0x5a7ac9,\n  },\n  { // L21-25 — THE STARFIELD: deepest night, the galaxy overhead\n    sky: PH + 'satara_night_2k.hdr', bgI: 1.4,\n    top: [0.001, 0.001, 0.004], vio: [0.004, 0.006, 0.024], rose: [0.012, 0.022, 0.070],\n    cream: [0.030, 0.055, 0.140], under: [0.005, 0.008, 0.028], sunCol: [0.06, 0.09, 0.20],\n    fog: 0x1a2138, key: 0x9fb8f0, ki: 1.15, hemiS: 0x50639f, hemiG: 0x141a30, hi: 0.38,\n    amb: 0x222a45, ai: 0.34, rim: 0x7f98e8, ri: 0.6,\n    sun: 0xbccfff, sunOp: 0.25, core: 0xe8f0ff, star: 1.0, haze: 0x46609f,\n  },\n  { // L26-30 — THE SKY IS ON FIRE\n    sky: PH + 'the_sky_is_on_fire_2k.hdr', bgI: 0.5,\n    top: [0.012, 0.004, 0.008], vio: [0.080, 0.014, 0.024], rose: [0.300, 0.045, 0.020],\n    cream: [0.620, 0.150, 0.040], under: [0.085, 0.020, 0.010], sunCol: [0.42, 0.10, 0.02],\n    fog: 0x8a4534, key: 0xffb070, ki: 1.5, hemiS: 0xd98a5a, hemiG: 0x58281c, hi: 0.4,\n    amb: 0x402828, ai: 0.28, rim: 0xd96a8f, ri: 0.42,\n    sun: 0xff8a3a, sunOp: 0.55, core: 0xffd9a0, star: 0.3, haze: 0xd96a38,\n  },\n  { // L31-35 — THE GREY VEIL: a steel storm rolls in\n    sky: PH + 'wasteland_clouds_puresky_2k.hdr', bgI: 0.4,\n    top: [0.008, 0.010, 0.014], vio: [0.030, 0.040, 0.055], rose: [0.090, 0.110, 0.130],\n    cream: [0.200, 0.230, 0.250], under: [0.035, 0.045, 0.055], sunCol: [0.14, 0.16, 0.18],\n    fog: 0x5f6a72, key: 0xd9e4e8, ki: 1.3, hemiS: 0x9fb0b8, hemiG: 0x2c3438, hi: 0.4,\n    amb: 0x353d42, ai: 0.3, rim: 0xa8c0d0, ri: 0.5,\n    sun: 0xd9e4e8, sunOp: 0.3, core: 0xf0f6f8, star: 0.2, haze: 0x8a9aa5,\n  },\n  { // L36-40 — THE TEMPEST: an eerie green-black storm for the summit\n    sky: PH + 'kloppenheim_06_puresky_2k.hdr', bgI: 0.45,\n    top: [0.002, 0.006, 0.009], vio: [0.007, 0.028, 0.038], rose: [0.020, 0.095, 0.070],\n    cream: [0.060, 0.240, 0.150], under: [0.008, 0.036, 0.026], sunCol: [0.08, 0.26, 0.16],\n    fog: 0x2f5548, key: 0xd9ffe8, ki: 1.25, hemiS: 0x7ab89f, hemiG: 0x14201c, hi: 0.4,\n    amb: 0x203830, ai: 0.3, rim: 0x9fe8c9, ri: 0.55,\n    sun: 0xbfffdd, sunOp: 0.4, core: 0xf0fff6, star: 0.7, haze: 0x4fae8a,\n  },\n];\nfunction applyTheme(i) {\n  const th = THEMES[Math.max(0, Math.min(THEMES.length - 1, i))];\n  const u = dome.material.uniforms;\n  u.uTop.value.setRGB(...th.top);\n  u.uVio.value.setRGB(...th.vio);\n  u.uRose.value.setRGB(...th.rose);\n  u.uCream.value.setRGB(...th.cream);\n  u.uUnder.value.setRGB(...th.under);\n  u.uSunCol.value.setRGB(...th.sunCol);\n  scene.fog.color.setHex(th.fog);\n  keyLight.color.setHex(th.key); keyLight.intensity = th.ki;\n  hemiLight.color.setHex(th.hemiS); hemiLight.groundColor.setHex(th.hemiG); hemiLight.intensity = th.hi;\n  ambLight.color.setHex(th.amb); ambLight.intensity = th.ai;\n  rimLight.color.setHex(th.rim); rimLight.intensity = th.ri;\n  sunHalo.material.color.setHex(th.sun); sunHalo.material.opacity = th.sunOp;\n  sunCore.material.color.setHex(th.core);\n  starsMat.opacity = th.star;\n  hazeGlow.material.color.setHex(th.haze);\n  const tm = THEME_MOTES[Math.max(0, Math.min(THEME_MOTES.length - 1, i))];\n  moteMat.color.setHex(tm.color);\n  moteV = tm.v;\n  for (const uc of underClouds) uc.c.material.color.setHex(th.haze);\n  requestSky(th.sky, th.bgI); // swap in this act's 360 photo sky\n}\n\n/* ---------- world groups ---------- */\nconst world = new THREE.Group();       // tile slabs\nscene.add(world);\nconst fxGroup = new THREE.Group();     // falling slabs + debris + confetti\nscene.add(fxGroup);\n\n/* ---------- the player: a rolling voxel ball ---------- */\nconst player = new THREE.Group();            // ground-contact origin: position + squash\nconst ballSpin = new THREE.Group();          // free-rolling rotation about the centre\nballSpin.position.y = BALL_R;\nconst ballModel = buildBallModel();\nballModel.position.y = -BALL_R;              // model origin is bottom-centre\nballSpin.add(ballModel);\nplayer.add(ballSpin);\nconst glowLight = new THREE.PointLight(GOLD, 0.55, 4);\nglowLight.position.set(0, BALL_R, 0);        // lantern-heart inside the ball\nplayer.add(glowLight);\nscene.add(player);\nlet squash = 0; // landing squash timer\nconst ROLL_ANGLE = 1 / BALL_R;               // radians per tile — true contact rolling\n\n/* ---------- the exit beacon: a shaft of gold reaching into the sky ---------- */\nfunction makeBeamTexture() {\n  const c = document.createElement('canvas');\n  c.width = 4; c.height = 256;\n  const g = c.getContext('2d');\n  const grad = g.createLinearGradient(0, 256, 0, 0); // bright at base, gone by the top\n  grad.addColorStop(0, 'rgba(255,255,255,0.95)');\n  grad.addColorStop(0.25, 'rgba(255,255,255,0.45)');\n  grad.addColorStop(0.7, 'rgba(255,255,255,0.10)');\n  grad.addColorStop(1, 'rgba(255,255,255,0)');\n  g.fillStyle = grad;\n  g.fillRect(0, 0, 4, 256);\n  return new THREE.CanvasTexture(c);\n}\nconst beacon = new THREE.Group();\nconst beamMat = new THREE.MeshBasicMaterial({\n  map: makeBeamTexture(), color: GOLD, transparent: true, opacity: 0.1,\n  blending: THREE.AdditiveBlending, depthWrite: false, side: THREE.DoubleSide, fog: false,\n});\nconst beam = new THREE.Mesh(new THREE.CylinderGeometry(0.09, 0.32, 34, 12, 1, true), beamMat);\nbeam.position.y = 17 + LAYER_H; // vanishes into the sky, never shows a top\nbeacon.add(beam);\nconst exitLight = new THREE.PointLight(GOLD, 0.3, 3.5);\nexitLight.position.y = LAYER_H + 0.5;\nbeacon.add(exitLight);\n// a slow gold halo ring floats over the pad — the goal reads from anywhere\nconst haloRing = new THREE.Mesh(\n  new THREE.TorusGeometry(0.4, 0.022, 10, 40),\n  new THREE.MeshBasicMaterial({ color: GOLD, transparent: true, opacity: 0.5, blending: THREE.AdditiveBlending, depthWrite: false, fog: false })\n);\nhaloRing.rotation.x = Math.PI / 2;\nhaloRing.position.y = LAYER_H + 0.55;\nbeacon.add(haloRing);\nscene.add(beacon);\nlet exitArmed = false;\n\n/* ---------- Game state ---------- */\nlet state = 'boot', stateT = 0, tick = 0;\nlet levelIx = 0, score = 0, lives = 3;\nlet hiscore = 0;\ntry { hiscore = +(localStorage.getItem('tng_hi3d') || 0); } catch (e) {}\nlet newRecord = false;\n\n/* ---------- persistence: unlocked progress + per-level medals ---------- */\nconst saveData = (() => {\n  try { return JSON.parse(localStorage.getItem('tng_save') || '{}'); } catch (e) { return {}; }\n})();\nsaveData.unlocked = Math.max(1, saveData.unlocked || 1);\nsaveData.medals = saveData.medals || {}; // levelIx -> 1..4 (bronze..perfect)\nfunction persistSave() {\n  try { localStorage.setItem('tng_save', JSON.stringify(saveData)); } catch (e) {}\n}\nconst MEDAL_NAMES = { 4: 'PERFECT', 3: 'GOLD', 2: 'SILVER', 1: 'BRONZE' };\nlet lastMedal = 0, lastMoves = 0, lastPar = 0;\nlet lastTime = 0, newBestTime = false;\n\n/* ---------- ENDLESS: procedurally generated stages, forever ----------\n   Same math as the campaign: a stage IS its solution path, so every\n   generated board is provably clearable. Stages grow and the pace\n   tightens without end. */\nlet endless = false, endlessStage = 1, currentEndless = null;\nfunction genEndless(stage) {\n  const target = Math.min(72, 12 + stage * 4);\n  const D = { R: [1, 0], L: [-1, 0], U: [0, -1], D: [0, 1] };\n  const dirs = ['R', 'L', 'U', 'D'];\n  const visits = new Map();\n  const kk = (a, b) => a + ',' + b;\n  let x = 0, y = 0, path = '';\n  visits.set(kk(0, 0), 1);\n  let guard = 0;\n  while (path.length < target && guard++ < 500) {\n    const dir = dirs[(Math.random() * 4) | 0];\n    const len = 1 + ((Math.random() * 3) | 0);\n    let cx = x, cy = y, ok = true;\n    const cells = [];\n    for (let i = 0; i < len; i++) {\n      cx += D[dir][0]; cy += D[dir][1];\n      if (Math.abs(cx) > 7 || Math.abs(cy) > 7) { ok = false; break; } // keep it compact\n      const seen = (visits.get(kk(cx, cy)) || 0) + cells.filter(c => c === kk(cx, cy)).length;\n      if (seen >= 3) { ok = false; break; } // never demand a 4th touch\n      cells.push(kk(cx, cy));\n    }\n    if (!ok) continue;\n    for (const c of cells) visits.set(c, (visits.get(c) || 0) + 1);\n    x = cx; y = cy;\n    path += dir.repeat(len);\n  }\n  const pace = Math.max(0.72, 1.4 - stage * 0.045);\n  return { name: 'STAGE ' + stage, time: Math.max(10, Math.round(path.length * pace + 4)), start: [0, 0], path };\n}\nfunction activeLevel() {\n  return endless ? currentEndless : LEVELS[Math.min(levelIx, LEVELS.length - 1)];\n}\n\n/* ---------- combo: chained moves multiply the demolition ---------- */\nlet combo = 0, comboT = 0, movesUsed = 0;\nconst COMBO_WINDOW = 1.05; // seconds between moves to keep the chain alive\nlet fovPulse = 0;          // camera punch on combo milestones\n\n/* ---------- pickups: time crystals + hidden gems, placed by hash ---------- */\nfunction pickHash(a, b, c) {\n  let h = (a * 73856093) ^ (b * 19349663) ^ ((c + 1) * 83492791);\n  h = (h ^ (h >> 13)) * 1274126177;\n  return (h ^ (h >> 16)) >>> 0;\n}\nlet crystals = []; // {mesh, tile} — bobbing +4s pickups\nconst crystalGeo = new THREE.OctahedronGeometry(0.1);\nconst crystalMat = new THREE.MeshBasicMaterial({\n  color: 0x7dffe4, transparent: true, opacity: 0.9,\n  blending: THREE.AdditiveBlending, depthWrite: false, fog: false,\n});\nlet tiles = new Map();                 // \"x,y\" -> {x,y,req,left,meshes:[]}\nlet gw = 0, gh = 0;\nlet px = 0, py = 0;\nlet hop = null, queued = null;\nlet timeLeft = 0, totalTime = 1;\nlet deathReason = '';\nlet falling = [];                      // {obj, vel, rot}\nlet clearBonus = 0;\nlet lastTickSec = -1;\nlet camDist = 10;\nlet shakeAmt = 0;\nlet blocksLeft = 0;\nlet slabAnims = [];      // staggered board build-in\nlet entranceT = 1;       // player drop-in\nconst key = (x, y) => x + ',' + y;\nfunction easeOutBack(t) {\n  const c = 1.70158;\n  return 1 + (c + 1) * Math.pow(t - 1, 3) + c * Math.pow(t - 1, 2);\n}\n\n/* ---------- Audio (soft chip synth) ---------- */\nlet AC = null, masterGain = null, muted = false;\nfunction audio() {\n  if (muted) return null;\n  if (!AC) {\n    try {\n      AC = new (window.AudioContext || window.webkitAudioContext)();\n      masterGain = AC.createGain();\n      masterGain.gain.value = 0.17;\n      masterGain.connect(AC.destination);\n    } catch (e) { return null; }\n  }\n  if (AC.state === 'suspended') AC.resume();\n  initMusic(); // score starts on the first user gesture (autoplay policy)\n  return AC;\n}\nfunction blip(freq, dur, type, when, vol, slide) {\n  const ac = audio(); if (!ac) return;\n  const t0 = ac.currentTime + (when || 0);\n  const o = ac.createOscillator(), g = ac.createGain();\n  o.type = type || 'triangle';\n  o.frequency.setValueAtTime(freq, t0);\n  if (slide) o.frequency.exponentialRampToValueAtTime(Math.max(20, slide), t0 + dur);\n  g.gain.setValueAtTime(vol || 0.5, t0);\n  g.gain.exponentialRampToValueAtTime(0.001, t0 + dur);\n  o.connect(g); g.connect(masterGain);\n  o.start(t0); o.stop(t0 + dur + 0.02);\n}\nfunction noiseBurst(dur, vol, freq) {\n  const ac = audio(); if (!ac) return;\n  const n = Math.floor(ac.sampleRate * dur);\n  const buf = ac.createBuffer(1, n, ac.sampleRate);\n  const d = buf.getChannelData(0);\n  for (let i = 0; i < n; i++) d[i] = (Math.random() * 2 - 1) * (1 - i / n);\n  const src = ac.createBufferSource(); src.buffer = buf;\n  const f = ac.createBiquadFilter(); f.type = 'lowpass'; f.frequency.value = freq || 800;\n  const g = ac.createGain(); g.gain.value = vol || 0.5;\n  src.connect(f); f.connect(g); g.connect(masterGain);\n  src.start();\n}\nconst sfx = {\n  hop()     { blip(300, 0.07, 'triangle', 0, 0.5, 430); },\n  pop(mult) { // the demolition sings higher as the combo builds\n    const m = Math.min(mult || 1, 10);\n    noiseBurst(0.14, 0.4, 850 + m * 90);\n    blip(150 + m * 26, 0.1, 'square', 0, 0.18, 80 + m * 10);\n  },\n  fall()    { blip(420, 0.6, 'sawtooth', 0, 0.4, 40); },\n  stuck()   { blip(180, 0.3, 'square', 0, 0.35, 80); },\n  crystal() { [880, 1175, 1568].forEach((f, i) => blip(f, 0.09, 'sine', i * 0.05, 0.5)); },\n  gem()     { [990, 1320].forEach((f, i) => blip(f, 0.08, 'triangle', i * 0.06, 0.55)); },\n  milestone(c) { [660, 880, 1100, 1320].forEach((f, i) => blip(f * (c >= 10 ? 1.25 : 1), 0.08, 'triangle', i * 0.05, 0.5)); },\n  tickTock(){ blip(950, 0.045, 'square', 0, 0.3); },\n  start()   { [392, 523, 659, 784].forEach((f, i) => blip(f, 0.1, 'triangle', i * 0.07, 0.5)); },\n  clear()   { [523, 659, 784, 1047, 784, 1047].forEach((f, i) => blip(f, 0.11, 'triangle', i * 0.09, 0.55)); },\n  dead()    { [330, 262, 196, 131].forEach((f, i) => blip(f, 0.15, 'triangle', i * 0.12, 0.5)); },\n  over()    { [262, 233, 208, 196, 131].forEach((f, i) => blip(f, 0.24, 'triangle', i * 0.19, 0.5)); },\n  win()     { [523, 659, 784, 1047, 1319, 1568].forEach((f, i) => blip(f, 0.13, 'triangle', i * 0.1, 0.5)); },\n};\n\n/* ---------- generative score: drone + pad swells + twilight kalimba ----------\n   Technique lineage: KILN's per-scene generative music. Each act of the\n   campaign has its own root note and scale colour; a sparse kalimba\n   improvises phrases through a feedback echo over slow pad swells. */\nconst MUSIC_SCENES = {\n  title:   { root: 220.00, scale: [0, 3, 5, 7, 10], mel: 2, density: 0.30, drone: 0.026 },\n  victory: { root: 261.63, scale: [0, 2, 4, 7, 9],  mel: 2, density: 0.55, drone: 0.022 },\n  bands: [\n    { root: 220.00, scale: [0, 3, 5, 7, 10], mel: 2, density: 0.38, drone: 0.026 }, // L1-8   dusk calm\n    { root: 261.63, scale: [0, 2, 4, 7, 9],  mel: 2, density: 0.44, drone: 0.024 }, // L9-16  bright\n    { root: 196.00, scale: [0, 2, 5, 7, 10], mel: 2, density: 0.40, drone: 0.028 }, // L17-24 open, airy\n    { root: 293.66, scale: [0, 2, 5, 7, 10], mel: 3, density: 0.46, drone: 0.026 }, // L25-32 climbing\n    { root: 164.81, scale: [0, 1, 5, 7, 10], mel: 2, density: 0.34, drone: 0.034 }, // L33-40 phrygian dread\n  ],\n};\nlet musicGain = null, echoBus = null, droneNodes = null, melDeg = 2;\nfunction currentScene() {\n  if (state === 'victory') return MUSIC_SCENES.victory;\n  if (state === 'title' || state === 'boot' || state === 'gameover') return MUSIC_SCENES.title;\n  if (endless) return MUSIC_SCENES.bands[(endlessStage - 1) % 5];\n  return MUSIC_SCENES.bands[Math.min(4, Math.floor(levelIx / 8))];\n}\nfunction degHz(sc, deg, octMul) {\n  const n = sc.scale.length;\n  const oct = Math.floor(deg / n);\n  const semi = sc.scale[((deg % n) + n) % n];\n  return sc.root * Math.pow(2, semi / 12 + oct) * (octMul || 1);\n}\nfunction initMusic() {\n  if (musicGain || !AC) return;\n  musicGain = AC.createGain();\n  musicGain.gain.value = 0.85;\n  musicGain.connect(masterGain);\n  // echo bus — the open dusk rings back\n  const dl = AC.createDelay(1.5);\n  dl.delayTime.value = 0.46;\n  const fb = AC.createGain(); fb.gain.value = 0.32;\n  const damp = AC.createBiquadFilter(); damp.type = 'lowpass'; damp.frequency.value = 1600;\n  dl.connect(damp); damp.connect(fb); fb.connect(dl);\n  dl.connect(musicGain);\n  echoBus = dl;\n  // drone: the world's standing tone\n  const sc = currentScene();\n  const o1 = AC.createOscillator(), o2 = AC.createOscillator();\n  o1.type = 'sine'; o2.type = 'triangle';\n  o1.frequency.value = sc.root / 4;\n  o2.frequency.value = (sc.root / 4) * 1.004;\n  const lp = AC.createBiquadFilter(); lp.type = 'lowpass'; lp.frequency.value = 240;\n  const g = AC.createGain();\n  g.gain.value = 0.0001;\n  g.gain.linearRampToValueAtTime(sc.drone, AC.currentTime + 3);\n  o1.connect(lp); o2.connect(lp); lp.connect(g); g.connect(musicGain);\n  o1.start(); o2.start();\n  droneNodes = { o1, o2, g };\n  setInterval(playPad, 7800);\n  setInterval(() => { const s = currentScene(); if (Math.random() < s.density) playPhrase(s); }, 2100);\n  playPad();\n}\nfunction retuneDrone() {\n  if (!droneNodes || !AC) return;\n  const sc = currentScene(), t = AC.currentTime;\n  droneNodes.o1.frequency.linearRampToValueAtTime(sc.root / 4, t + 2.5);\n  droneNodes.o2.frequency.linearRampToValueAtTime((sc.root / 4) * 1.004, t + 2.5);\n  droneNodes.g.gain.linearRampToValueAtTime(sc.drone, t + 2.5);\n}\nfunction playPad() {\n  if (!AC || muted || document.hidden) return;\n  const sc = currentScene();\n  const t = AC.currentTime + 0.05;\n  const degs = [0, 2, Math.random() < 0.5 ? 4 : 5];\n  for (const dg of degs) {\n    const o = AC.createOscillator();\n    o.type = dg === 0 ? 'triangle' : 'sine';\n    o.frequency.value = degHz(sc, dg, 1);\n    const g = AC.createGain();\n    g.gain.setValueAtTime(0.0001, t);\n    g.gain.linearRampToValueAtTime(0.028, t + 2.8);\n    g.gain.linearRampToValueAtTime(0.0001, t + 7.6);\n    o.connect(g); g.connect(musicGain);\n    o.start(t); o.stop(t + 8);\n  }\n}\n// kalimba: warm pluck — sine fundamental + soft 3rd partial into the echo\nfunction pluck(freq, when, gain, dur) {\n  const t = when;\n  const g = AC.createGain();\n  g.gain.setValueAtTime(0.0001, t);\n  g.gain.exponentialRampToValueAtTime(gain, t + 0.012);\n  g.gain.exponentialRampToValueAtTime(0.001, t + dur);\n  const o = AC.createOscillator();\n  o.type = 'sine'; o.frequency.value = freq;\n  const o2 = AC.createOscillator();\n  o2.type = 'sine'; o2.frequency.value = freq * 3.01;\n  const g2 = AC.createGain();\n  g2.gain.setValueAtTime(0.0001, t);\n  g2.gain.exponentialRampToValueAtTime(gain * 0.18, t + 0.008);\n  g2.gain.exponentialRampToValueAtTime(0.001, t + dur * 0.35);\n  o.connect(g); o2.connect(g2); g2.connect(g);\n  g.connect(musicGain);\n  const send = AC.createGain(); send.gain.value = 0.55;\n  g.connect(send); send.connect(echoBus);\n  o.start(t); o2.start(t);\n  o.stop(t + dur + 0.1); o2.stop(t + dur + 0.1);\n}\n// improvise a short phrase: a random walk that settles low\nfunction playPhrase(sc) {\n  if (!AC || muted || document.hidden || AC.state !== 'running') return;\n  let t = AC.currentTime + 0.05;\n  const n = 2 + Math.floor(Math.random() * 4);\n  for (let i = 0; i < n; i++) {\n    melDeg += [-2, -1, -1, 1, 1, 2, 3][Math.floor(Math.random() * 7)];\n    melDeg = Math.max(0, Math.min(9, melDeg));\n    const last = i === n - 1;\n    if (last && Math.random() < 0.5) melDeg = Math.max(0, melDeg - 3);\n    pluck(degHz(sc, melDeg, sc.mel), t, 0.05 + Math.random() * 0.03, last ? 2.4 : 1.4);\n    t += 0.3 + Math.random() * 0.26;\n  }\n}\n\n/* ---------- Level build ---------- */\nfunction tileWorldPos(x, y) {\n  return new THREE.Vector3(x - (gw - 1) / 2, 0, y - (gh - 1) / 2);\n}\nfunction stackTopY(t) { return t ? t.left * LAYER_H : 0; }\n\nfunction disposeGroup(g) {\n  g.traverse((o) => {\n    if (o.geometry && !o.userData.sharedGeo) o.geometry.dispose();\n    if (o.material && o.userData.ownMaterial) o.material.dispose();\n  });\n}\n\nfunction buildLevel(ix) {\n  const L = endless ? currentEndless : LEVELS[ix];\n  // simulate path → visit counts; the path's final cell becomes the exit\n  const visits = new Map();\n  let x = L.start[0], y = L.start[1];\n  const cells = [[x, y]];\n  visits.set(key(x, y), 1);\n  for (const ch of L.path) {\n    const d = DIRS[ch];\n    x += d[0]; y += d[1];\n    const k = key(x, y);\n    visits.set(k, (visits.get(k) || 0) + 1);\n    cells.push([x, y]);\n  }\n  const exitOrigKey = key(x, y);\n  // normalize bounds\n  let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;\n  for (const [cx, cy] of cells) {\n    minX = Math.min(minX, cx); maxX = Math.max(maxX, cx);\n    minY = Math.min(minY, cy); maxY = Math.max(maxY, cy);\n  }\n  gw = maxX - minX + 1; gh = maxY - minY + 1;\n\n  // clear old\n  while (world.children.length) { const c = world.children[0]; disposeGroup(c); world.remove(c); }\n  while (fxGroup.children.length) { const c = fxGroup.children[0]; disposeGroup(c); fxGroup.remove(c); }\n  falling = [];\n  slabAnims = [];\n  tiles = new Map();\n  blocksLeft = 0;\n\n  const sx0 = L.start[0] - minX, sy0 = L.start[1] - minY;\n  for (const [k, req] of visits) {\n    const [vx, vy] = k.split(',').map(Number);\n    const tx = vx - minX, ty = vy - minY;\n    const base = tileWorldPos(tx, ty);\n    const delay0 = (Math.abs(tx - sx0) + Math.abs(ty - sy0)) * 0.055;\n    if (k === exitOrigKey) {\n      // the exit: indestructible, walk on it freely, land here last to win\n      const slab = buildExitSlab();\n      slab.position.set(base.x, -2.6, base.z);\n      slabAnims.push({ slab, targetY: 0, t: -delay0 });\n      world.add(slab);\n      tiles.set(key(tx, ty), { x: tx, y: ty, req: 1, left: 1, exit: true, meshes: [slab] });\n      beacon.position.set(base.x, 0, base.z);\n      continue;\n    }\n    const t = { x: tx, y: ty, req, left: req, meshes: [] };\n    const seed = endless ? endlessStage * 131 : ix;\n    for (let i = 0; i < req; i++) {\n      const slab = buildLayerSlab(i, tx, ty);\n      // build-in: slabs rise from the void, rippling out from the start tile\n      const delay = delay0 + i * 0.07;\n      slab.position.set(base.x, i * LAYER_H - 2.6, base.z);\n      slabAnims.push({ slab, targetY: i * LAYER_H, t: -delay });\n      // some blocks hide a gem — +150 when torn free\n      if (pickHash(tx * 3 + i, ty * 5 + i, seed * 7) % 6 === 0) slab.userData.gem = true;\n      world.add(slab);\n      t.meshes.push(slab);\n    }\n    // tall stacks sometimes carry a time crystal — +4s on touch\n    if (req >= 2 && pickHash(tx, ty, seed) % 5 === 0) {\n      const cm = new THREE.Mesh(crystalGeo, crystalMat);\n      cm.userData.sharedGeo = true;\n      cm.position.set(base.x, req * LAYER_H + 0.34, base.z);\n      world.add(cm);\n      t.crystal = cm;\n      crystals.push({ mesh: cm, tile: t });\n    }\n    tiles.set(key(tx, ty), t);\n    blocksLeft += req;\n  }\n  crystals = crystals.filter(c => c.mesh.parent === world);\n  exitArmed = false;\n\n  px = L.start[0] - minX; py = L.start[1] - minY;\n  const st = tiles.get(key(px, py));\n  const p0 = tileWorldPos(px, py);\n  player.position.set(p0.x, stackTopY(st) + 3, p0.z);\n  player.rotation.set(0, 0, 0);\n  ballSpin.rotation.set(0, 0, 0);\n  player.scale.set(1, 1, 1);\n  player.visible = true;\n  squash = 0;\n  entranceT = -0.5; // drop in once the board has risen\n\n  hop = null; queued = null;\n  timeLeft = L.time; totalTime = L.time;\n  lastTickSec = -1;\n  combo = 0; comboT = 0; movesUsed = 0;\n\n  camDist = Math.max(gw, gh * 1.25) * 1.0 + 4.0;\n  keyLight.position.copy(SUN_DIR).multiplyScalar(camDist * 1.6); // keep the low sun angle\n  // haze scales with the board so big late-game boards stay readable\n  scene.fog.near = camDist * 1.7;\n  scene.fog.far = camDist * 5.4;\n  applyTheme(endless ? (endlessStage - 1) % THEMES.length : Math.floor(ix / 5)); // fresh sky per act / per stage\n}\n\nfunction tileAt(x, y) { return tiles.get(key(x, y)) || null; }\nfunction enterable(x, y) { const t = tileAt(x, y); return !!t && (t.exit || t.left > 0); }\n\n/* ---------- Flow ---------- */\nfunction setState(s) { state = s; stateT = 0; syncScreen(); retuneDrone(); }\n\nfunction startGame(fromLevel) {\n  endless = false;\n  levelIx = fromLevel || 0; score = 0; lives = 3; newRecord = false;\n  sfx.start();\n  buildLevel(levelIx);\n  setState('intro');\n}\nfunction startEndless() {\n  endless = true; endlessStage = 1;\n  currentEndless = genEndless(1);\n  levelIx = 0; score = 0; lives = 3; newRecord = false;\n  sfx.start();\n  buildLevel(0);\n  setState('intro');\n}\nfunction nextLife() {\n  lives--;\n  if (lives < 0) {\n    saveHiscore();\n    if (endless) {\n      saveData.endlessBest = Math.max(saveData.endlessBest || 0, endlessStage);\n      persistSave();\n    }\n    sfx.over();\n    setState('gameover');\n  } else {\n    buildLevel(levelIx);\n    setState('intro');\n  }\n}\nfunction saveHiscore() {\n  if (score > hiscore) {\n    hiscore = score; newRecord = true;\n    try { localStorage.setItem('tng_hi3d', String(hiscore)); } catch (e) {}\n  }\n}\nfunction levelClear() {\n  clearBonus = Math.ceil(timeLeft) * 10;\n  score += 500 + clearBonus;\n  // medals: judged against the authored optimal route\n  lastPar = activeLevel().path.length;\n  lastMoves = movesUsed;\n  lastMedal = movesUsed <= lastPar ? 4 : movesUsed <= lastPar + 3 ? 3 : movesUsed <= Math.ceil(lastPar * 1.3) ? 2 : 1;\n  if (lastMedal === 4) score += 1000; // perfection pays\n  lastTime = Math.round((totalTime - Math.max(0, timeLeft)) * 10) / 10;\n  newBestTime = false;\n  if (!endless) {\n    saveData.medals[levelIx] = Math.max(saveData.medals[levelIx] || 0, lastMedal);\n    saveData.unlocked = Math.min(LEVELS.length, Math.max(saveData.unlocked, levelIx + 2));\n    saveData.times = saveData.times || {};\n    if (!saveData.times[levelIx] || lastTime < saveData.times[levelIx]) {\n      saveData.times[levelIx] = lastTime;\n      newBestTime = true;\n    }\n    persistSave();\n  }\n  sfx.clear();\n  confetti();\n  burstRing(player.position.x, player.position.y, player.position.z, 2.4);\n  shakeAmt = Math.min(0.5, shakeAmt + 0.25);\n  setState('clear');\n}\nfunction die(reason) {\n  deathReason = reason;\n  shakeAmt = Math.min(0.6, shakeAmt + 0.35);\n  if (reason === 'TIME UP!') { sfx.dead(); }\n  else {\n    sfx.fall();\n    burstSparks(player.position.x, player.position.y + 0.4, player.position.z, 14, 2);\n    falling.push({\n      obj: player,\n      vel: new THREE.Vector3((Math.random() - 0.5) * 1.5, -2, (Math.random() - 0.5) * 1.5),\n      rot: new THREE.Vector3(Math.random() * 4 - 2, 0, Math.random() * 4 - 2),\n    });\n  }\n  setState('death');\n}\n\n/* ---------- Movement ---------- */\nfunction tryMove(dx, dz) {\n  if (state !== 'play') return;\n  if (hop) { queued = { dx, dz }; return; }\n  entranceT = 1; // moving early cancels the drop-in\n  const tx = px + dx, ty = py + dz;\n  const from = tileAt(px, py);\n  const fromH = stackTopY(from);\n  const target = tileAt(tx, ty);\n  const doom = !enterable(tx, ty);\n  hop = {\n    fx: px, fy: py, tx, ty, t: 0, fromH, toH: doom ? 0 : stackTopY(target), doom,\n    axis: new THREE.Vector3(dz, 0, -dx).normalize(), // rolling axis = up x dir\n  };\n  movesUsed++;\n  combo = comboT > 0 ? combo + 1 : 1; // keep rolling to keep the chain alive\n  comboT = COMBO_WINDOW;\n  if (combo === 5 || combo === 10) { fovPulse = 1; sfx.milestone(combo); } // the camera feels it\n  // the block you leap OFF breaks away immediately — no way back\n  popLayer(from);\n  // a whisper of gold dust kicked up at the push-off\n  burstSparks(player.position.x, fromH + 0.05, player.position.z, 2, 0.7);\n  sfx.hop();\n}\nfunction popLayer(t) {\n  if (!t || t.exit || t.left <= 0) return;\n  t.left--;\n  blocksLeft--;\n  const mult = Math.max(1, Math.min(combo, 10));\n  const points = 10 * mult;\n  score += points;\n  const slab = t.meshes.pop();\n  const base = tileWorldPos(t.x, t.y);\n  const topY = (t.left + 1) * LAYER_H;\n  if (slab && slab.userData.gem) { // a gem was hiding inside this block\n    score += 150;\n    spawnPopup(base.x, topY + 0.35, base.z, '+150 ◆', 6);\n    burstSparks(base.x, topY + 0.2, base.z, 14, 2.4);\n    sfx.gem();\n  }\n  if (slab) {\n    world.remove(slab);\n    fxGroup.add(slab);\n    falling.push({\n      obj: slab,\n      // a little upward kick before the drop — the tear reads as force\n      vel: new THREE.Vector3((Math.random() - 0.5) * 1.6, 0.9 + Math.random() * 0.4, (Math.random() - 0.5) * 1.6),\n      rot: new THREE.Vector3((Math.random() - 0.5) * 5, (Math.random() - 0.5) * 5, (Math.random() - 0.5) * 5),\n    });\n  }\n  // shards + sparks + a flash ring where the block tore loose\n  const ci = Math.min(t.left, 2);\n  for (let i = 0; i < 4; i++) {\n    const d = makeDebrisVoxel(i % 2 ? LAYER_TOP_COLORS[ci] : LAYER_DEEP_COLORS[ci], 0.09 + Math.random() * 0.06);\n    d.position.set(base.x + (Math.random() - 0.5) * 0.7, topY, base.z + (Math.random() - 0.5) * 0.7);\n    fxGroup.add(d);\n    falling.push({\n      obj: d,\n      vel: new THREE.Vector3((Math.random() - 0.5) * 3, 1 + Math.random() * 2, (Math.random() - 0.5) * 3),\n      rot: new THREE.Vector3((Math.random() - 0.5) * 12, (Math.random() - 0.5) * 12, (Math.random() - 0.5) * 12),\n    });\n  }\n  burstSparks(base.x, topY, base.z, 5 + Math.min(mult, 6), 1.6);\n  burstRing(base.x, topY, base.z, 0.4);\n  spawnPopup(base.x, topY, base.z, '+' + points, mult);\n  shakeAmt = Math.min(0.35, shakeAmt + 0.05);\n  sfx.pop(mult);\n}\n\n/* floating score text, projected from world space to the screen */\nfunction spawnPopup(x, y, z, text, mult) {\n  const v = new THREE.Vector3(x, y + 0.3, z).project(camera);\n  if (v.z > 1) return; // behind the camera\n  const div = document.createElement('div');\n  div.className = 'pop-score' + (mult >= 6 ? ' blazing' : mult >= 3 ? ' hot' : '');\n  div.textContent = mult >= 3 ? text + ' ×' + mult : text;\n  div.style.left = ((v.x * 0.5 + 0.5) * window.innerWidth) + 'px';\n  div.style.top = ((-v.y * 0.5 + 0.5) * window.innerHeight) + 'px';\n  el.popups.appendChild(div);\n  setTimeout(() => div.remove(), 900);\n}\nfunction arrive() {\n  const t = tileAt(px, py);\n  squash = 1;\n  burstRing(player.position.x, stackTopY(t), player.position.z, 0.5 + (t ? t.req * 0.1 : 0));\n  // time crystal: +4 seconds, never past the level's full clock\n  if (t && t.crystal) {\n    timeLeft = Math.min(totalTime, timeLeft + 4);\n    world.remove(t.crystal);\n    spawnPopup(player.position.x, stackTopY(t) + 0.4, player.position.z, '+4s', 3);\n    burstSparks(player.position.x, stackTopY(t) + 0.3, player.position.z, 10, 1.8);\n    sfx.crystal();\n    t.crystal = null;\n  }\n  // WIN: land on the exit once every block is demolished\n  if (t && t.exit && blocksLeft === 0) {\n    levelClear();\n    return;\n  }\n  // stuck? (resting on the exit doesn't save you if blocks remain unreachable)\n  if (!enterable(px + 1, py) && !enterable(px - 1, py) && !enterable(px, py + 1) && !enterable(px, py - 1)) {\n    sfx.stuck();\n    die('NOWHERE TO GO!');\n  }\n}\n\n/* ---------- FX ---------- */\nfunction confetti() {\n  const p0 = player.position.clone();\n  for (let i = 0; i < 40; i++) {\n    const m = makeDebrisVoxel([MINT, AMBER, CORAL, BONE, GOLD][i % 5], 0.07 + Math.random() * 0.07);\n    m.position.copy(p0).add(new THREE.Vector3(0, 0.5, 0));\n    fxGroup.add(m);\n    const a = Math.random() * Math.PI * 2;\n    falling.push({\n      obj: m,\n      vel: new THREE.Vector3(Math.cos(a) * (1 + Math.random() * 2), 3 + Math.random() * 3, Math.sin(a) * (1 + Math.random() * 2)),\n      rot: new THREE.Vector3(Math.random() * 8 - 4, Math.random() * 8 - 4, Math.random() * 8 - 4),\n    });\n  }\n  burstSparks(p0.x, p0.y + 0.6, p0.z, 30, 3);\n}\n\n/* ---------- Update ---------- */\nfunction update(dt) {\n  tick += dt; stateT += dt;\n  dome.position.copy(camera.position);\n  // pitch the sky ball so its horizon imagery sits behind the board when the\n  // camera looks down at play angle; drift it slowly, like weather\n  skyTilt += (skyTiltTarget - skyTilt) * Math.min(1, dt * 1.5);\n  skySphere.rotation.set(skyTilt, tick * 0.008, 0);\n  getMaterials().gold.emissiveIntensity = 1.0 + Math.sin(tick * 3.2) * 0.22;\n\n  // board build-in: slabs surge up out of the void with a little overshoot\n  slabAnims = slabAnims.filter((a) => {\n    if (a.slab.parent !== world) return false; // popped mid-animation\n    a.t += dt;\n    if (a.t <= 0) { a.slab.position.y = a.targetY - 2.6; return true; }\n    const t = Math.min(1, a.t / 0.5);\n    a.slab.position.y = a.targetY - 2.6 * (1 - easeOutBack(t));\n    return t < 1;\n  });\n\n  // physics for falling things\n  for (const f of falling) {\n    f.vel.y -= 9 * dt;\n    f.obj.position.addScaledVector(f.vel, dt);\n    f.obj.rotation.x += f.rot.x * dt;\n    f.obj.rotation.y += f.rot.y * dt;\n    f.obj.rotation.z += f.rot.z * dt;\n  }\n  falling = falling.filter(f => {\n    if (f.obj.position.y < -28) {\n      if (f.obj !== player) { disposeGroup(f.obj); fxGroup.remove(f.obj); }\n      return false;\n    }\n    return true;\n  });\n\n  // survey: the camera orbits the board until the first move — no clock, no rush\n\n  // exit beacon: quiet pulse while blocks remain, blazing once armed\n  if (!exitArmed && blocksLeft === 0 && (state === 'play' || state === 'intro')) {\n    exitArmed = true;\n    burstSparks(beacon.position.x, LAYER_H + 0.2, beacon.position.z, 16, 2.2);\n    burstRing(beacon.position.x, LAYER_H, beacon.position.z, 1.6);\n    blip(1047, 0.3, 'triangle', 0, 0.5); blip(1319, 0.35, 'triangle', 0.12, 0.5);\n  }\n  beamMat.opacity = exitArmed ? 0.3 + Math.sin(tick * 5) * 0.1 : 0.07 + Math.sin(tick * 2.2) * 0.03;\n  exitLight.intensity = exitArmed ? 0.7 + Math.sin(tick * 5) * 0.25 : 0.25;\n  beam.rotation.y += dt * 0.6;\n  haloRing.rotation.z += dt * (exitArmed ? 2.2 : 0.5);\n  haloRing.position.y = LAYER_H + 0.55 + Math.sin(tick * 1.6) * 0.06;\n  haloRing.material.opacity = exitArmed ? 0.75 + Math.sin(tick * 5) * 0.2 : 0.4;\n\n  // combo chain cools off between moves\n  if (state === 'play' && comboT > 0) { comboT -= dt; if (comboT <= 0) combo = 0; }\n\n  // crystals bob and spin, waiting to be touched\n  for (const c of crystals) {\n    if (!c.mesh.parent) continue;\n    c.mesh.rotation.y += dt * 2;\n    c.mesh.rotation.x = Math.sin(tick * 1.4) * 0.3;\n    c.mesh.position.y = c.tile.req * LAYER_H + 0.34 + Math.sin(tick * 2.4 + c.tile.x) * 0.05;\n  }\n\n  // a gold ribbon streams off the ball while the chain burns\n  if (hop && combo >= 3 && state === 'play') {\n    burstSparks(player.position.x, player.position.y + BALL_R, player.position.z, 1, 0.25);\n  }\n\n  // combo milestones punch the lens\n  if (fovPulse > 0.01) {\n    fovPulse *= Math.exp(-5 * dt);\n    camera.fov = 44 + fovPulse * 4;\n    camera.updateProjectionMatrix();\n  }\n\n  // the frame burns when the clock nearly dies\n  document.body.classList.toggle('danger', state === 'play' && timeLeft < 5 && timeLeft > 0);\n\n  if (state === 'play') {\n    timeLeft -= dt;\n    const s = Math.ceil(timeLeft);\n    if (timeLeft <= 5.2 && timeLeft > 0 && s !== lastTickSec) { lastTickSec = s; sfx.tickTock(); }\n    if (timeLeft <= 0) { timeLeft = 0; die('TIME UP!'); }\n  }\n\n  // panic glow: antenna light runs coral when the clock is nearly out\n  if (state === 'play' && timeLeft < 5) {\n    glowLight.color.setHex(CORAL);\n    glowLight.intensity = 0.5 + Math.sin(tick * 12) * 0.3;\n  } else {\n    glowLight.color.setHex(GOLD);\n    glowLight.intensity = 0.3;\n  }\n\n  // roll animation — the ball turns exactly as far as it travels\n  if (hop && (state === 'play' || state === 'death')) {\n    const prevT = Math.min(1, hop.t);\n    hop.t += dt / HOP_T;\n    const t = Math.min(1, hop.t);\n    const a = tileWorldPos(hop.fx, hop.fy), b = tileWorldPos(hop.tx, hop.ty);\n    const h = hop.fromH + (hop.toH - hop.fromH) * t + Math.sin(t * Math.PI) * 0.2;\n    player.position.set(a.x + (b.x - a.x) * t, h, a.z + (b.z - a.z) * t);\n    player.scale.y = 1 + Math.sin(t * Math.PI) * 0.06;\n    ballSpin.rotateOnWorldAxis(hop.axis, (t - prevT) * ROLL_ANGLE);\n    if (t >= 1) {\n      const wasDoom = hop.doom;\n      px = hop.tx; py = hop.ty;\n      hop = null;\n      if (wasDoom) {\n        if (state === 'play') die('YOU FELL!');\n      } else if (state === 'play') {\n        arrive();\n        if (queued && state === 'play') { const q = queued; queued = null; tryMove(q.dx, q.dz); }\n      }\n    }\n  }\n\n  // landing squash, drop-in entrance & idle bob\n  if (squash > 0) squash = Math.max(0, squash - dt * 6);\n  if (!hop && (state === 'play' || state === 'intro' || state === 'title')) {\n    const t = tileAt(px, py);\n    if (entranceT < 1) {\n      entranceT += dt * 2;\n      const e = Math.max(0, Math.min(1, entranceT));\n      player.position.y = stackTopY(t) + (1 - e * e) * 3; // accelerating fall\n      if (entranceT >= 1) {\n        squash = 1;\n        burstRing(player.position.x, stackTopY(t), player.position.z, 0.8);\n        noiseBurst(0.1, 0.3, 500);\n        shakeAmt = Math.min(0.3, shakeAmt + 0.12);\n      }\n    } else {\n      player.position.y = stackTopY(t) + Math.abs(Math.sin(tick * 3.2)) * 0.04;\n    }\n    const sq = squash * 0.22;\n    player.scale.set(1 + sq * 0.6, 1 - sq, 1 + sq * 0.6);\n  }\n  if (state === 'clear') {\n    player.position.y += dt * (1.2 + stateT); // beam up\n    ballSpin.rotateOnWorldAxis(new THREE.Vector3(1, 0, 0), -dt * 7); // victory backspin\n  }\n\n  if (state === 'death' && stateT > (deathReason === 'RESTART!' ? 0.5 : 1.6)) nextLife(); // restarts are snappy\n  if (state === 'clear' && stateT > 2.6) {\n    if (endless) {\n      endlessStage++;\n      currentEndless = genEndless(endlessStage);\n      buildLevel(0);\n      setState('intro');\n    } else {\n      levelIx++;\n      if (levelIx >= LEVELS.length) {\n        saveHiscore();\n        sfx.win();\n        setState('victory');\n      } else {\n        buildLevel(levelIx);\n        setState('intro');\n      }\n    }\n  }\n\n  // camera\n  let targetPos, lookAt;\n  if (state === 'title' || state === 'victory') {\n    const a = tick * 0.22;\n    targetPos = new THREE.Vector3(Math.sin(a) * camDist * 0.9, camDist * 0.3, Math.cos(a) * camDist * 0.9);\n    lookAt = new THREE.Vector3(0, 1.5, 0); // low camera looks up — horizon and sun in frame\n    tiltTarget = 1.3; // gentle miniature feel\n    tiltFocusTarget = 0.27; // focus band follows the board to the lower third\n    skyTiltTarget = -0.08;\n  } else if (state === 'intro') {\n    // survey: orbit the whole board until the first move\n    const a = Math.PI * 0.35 + stateT * 0.3;\n    targetPos = new THREE.Vector3(Math.sin(a) * camDist * 0.8, camDist * 0.62, Math.cos(a) * camDist * 0.8);\n    lookAt = new THREE.Vector3(0, 0.1, 0);\n    tiltTarget = 0.8;\n    tiltFocusTarget = 0.46;\n    skyTiltTarget = -0.65; // bring the cloud band down behind the board\n  } else {\n    const leanX = player.position.x * 0.2, leanZ = player.position.z * 0.2;\n    targetPos = new THREE.Vector3(leanX, camDist * 0.68 + Math.sin(tick * 0.5) * 0.12, camDist * 0.74 + leanZ);\n    lookAt = new THREE.Vector3(leanX * 0.6, 0.25, leanZ * 0.6);\n    tiltTarget = 0.5; // a whisper — play stays razor sharp\n    tiltFocusTarget = 0.46;\n    skyTiltTarget = -0.65; // horizon imagery fills the backdrop at play angle\n  }\n  camera.position.lerp(targetPos, Math.min(1, dt * 3));\n  // shake\n  shakeAmt *= Math.exp(-6 * dt);\n  if (shakeAmt > 0.002) {\n    camera.position.x += (Math.random() - 0.5) * shakeAmt;\n    camera.position.y += (Math.random() - 0.5) * shakeAmt * 0.6;\n    camera.position.z += (Math.random() - 0.5) * shakeAmt;\n  }\n  camera.lookAt(lookAt);\n\n  tiltAmount += (tiltTarget - tiltAmount) * Math.min(1, dt * 2.5);\n  tiltFocus += (tiltFocusTarget - tiltFocus) * Math.min(1, dt * 2.5);\n  tiltH.uniforms.uAmount.value = tiltAmount;\n  tiltV.uniforms.uAmount.value = tiltAmount;\n  tiltH.uniforms.uFocus.value = tiltFocus;\n  tiltV.uniforms.uFocus.value = tiltFocus;\n\n  updateSparks(dt);\n  updateRings(dt);\n  updateFarBlocks(dt);\n}\n\n/* ---------- HUD / screens ---------- */\nfunction hearts() {\n  let s = '';\n  for (let i = 0; i < 3; i++) s += `<span class=\"heart ${i < lives ? '' : 'off'}\">&#9829;</span>`;\n  return s;\n}\nfunction syncHUD() {\n  el.score.textContent = String(score).padStart(6, '0');\n  el.level.textContent = endless ? '∞ ' + endlessStage : (Math.min(levelIx, LEVELS.length - 1) + 1) + '/' + LEVELS.length;\n  el.blocks.textContent = blocksLeft;\n  el.combo.textContent = combo > 1 ? '×' + Math.min(combo, 10) : '—';\n  el.combo.className = combo >= 6 ? 'blazing' : combo >= 3 ? 'hot' : '';\n  el.lives.innerHTML = hearts();\n  const frac = Math.max(0, timeLeft / totalTime);\n  el.time.style.width = (frac * 100) + '%';\n  el.time.className = frac > 0.5 ? 'ok' : frac > 0.25 ? 'warn' : 'low';\n  el.hud.style.display = (state === 'title' || state === 'boot' || state === 'victory' || state === 'select') ? 'none' : 'flex';\n}\nfunction syncScreen() {\n  const L = activeLevel();\n  let html = '';\n  if (state === 'title') {\n    html = `\n      <div class=\"panel title-panel\">\n        <h1>TOUCH <em>'N'</em> GO</h1>\n        <p class=\"sub\">A VOXEL REMAKE OF THE 1994 AMIGA CLASSIC</p>\n        <p class=\"rule\">Every block you hop off <b>breaks away</b>.<br>Demolish <b>every block</b> before the clock runs out.<br>Taller stacks take more visits.</p>\n        <p class=\"keys\">ARROWS / WASD &middot; SWIPE ON TOUCH &middot; R RESTART &middot; P PAUSE &middot; M SOUND</p>\n        <p class=\"blink\">PRESS SPACE OR TAP TO START</p>\n        <p class=\"keys\">E &mdash; ENDLESS MODE${saveData.endlessBest ? ` (BEST STAGE ${saveData.endlessBest})` : ''}${saveData.unlocked > 1 ? ` &nbsp;&middot;&nbsp; L &mdash; CHOOSE LEVEL (${saveData.unlocked} UNLOCKED)` : ''}</p>\n        ${hiscore > 0 ? `<p class=\"hi\">HI-SCORE ${String(hiscore).padStart(6, '0')}</p>` : ''}\n      </div>`;\n  } else if (state === 'select') {\n    let grid = '';\n    for (let i = 0; i < LEVELS.length; i++) {\n      const locked = i >= saveData.unlocked;\n      const m = saveData.medals[i] || 0;\n      grid += `<button class=\"lvl-btn\" data-level=\"${i}\" ${locked ? 'disabled' : ''} title=\"${LEVELS[i].name}\">\n        ${i + 1}<span class=\"dot ${m ? 'm' + m : ''}\"></span></button>`;\n    }\n    html = `\n      <div class=\"panel select\">\n        <h2>CHOOSE LEVEL</h2>\n        <p class=\"sub\">${saveData.unlocked} OF ${LEVELS.length} UNLOCKED &middot; DOTS SHOW YOUR BEST MEDAL</p>\n        <div class=\"level-grid\">${grid}</div>\n        <p class=\"keys\" style=\"margin-top:18px\">SPACE / ESC &mdash; BACK TO TITLE</p>\n      </div>`;\n  } else if (state === 'paused') {\n    html = `\n      <div class=\"panel small\">\n        <h2>PAUSED</h2>\n        <p class=\"sub\">${endless ? L.name : 'LEVEL ' + (levelIx + 1) + ' &middot; ' + L.name}</p>\n        <p class=\"blink\">P / SPACE &mdash; RESUME</p>\n      </div>`;\n  } else if (state === 'intro') {\n    const best = !endless && saveData.times && saveData.times[levelIx];\n    html = `\n      <div class=\"panel small preview\">\n        <h2>${endless ? L.name : 'LEVEL ' + (levelIx + 1) + ' &mdash; ' + L.name}</h2>\n        <p class=\"sub\">${blocksLeft} BLOCKS &middot; ${L.time}s${best ? ' &middot; BEST ' + best + 's' : ''} &middot; REACH THE GOLD PAD LAST</p>\n        <p class=\"blink\">ROLL TO BEGIN &mdash; THE CLOCK WAITS FOR YOUR FIRST MOVE</p>\n      </div>`;\n  } else if (state === 'death') {\n    html = `\n      <div class=\"panel small bad\">\n        <h2>${deathReason}</h2>\n        <p class=\"sub\">${lives > 0 ? lives + (lives === 1 ? ' LIFE' : ' LIVES') + ' LEFT' : 'THAT WAS YOUR LAST LIFE...'}</p>\n      </div>`;\n  } else if (state === 'clear') {\n    html = `\n      <div class=\"panel small good\">\n        <h2>${endless ? 'STAGE ' + endlessStage + ' CLEAR!' : 'LEVEL CLEAR!'}</h2>\n        <p style=\"margin-top:10px\"><span class=\"medal m${lastMedal}\">${MEDAL_NAMES[lastMedal]}</span>${newBestTime ? '<span class=\"medal m4\">NEW BEST TIME</span>' : ''}</p>\n        <p class=\"sub\">MOVES ${lastMoves} &middot; PAR ${lastPar} &middot; TIME ${lastTime}s${lastMedal === 4 ? ' &middot; +1000' : ''}</p>\n        <p class=\"sub\">CLEAR 500 &nbsp;&middot;&nbsp; TIME BONUS ${clearBonus}</p>\n        <p class=\"lvlname\">SCORE ${String(score).padStart(6, '0')}</p>\n      </div>`;\n  } else if (state === 'gameover') {\n    html = `\n      <div class=\"panel bad\">\n        <h1 class=\"over\">GAME OVER</h1>\n        ${endless ? `<p class=\"sub\">THE VOID TAKES YOU AT STAGE ${endlessStage}${saveData.endlessBest ? ' &middot; BEST ' + saveData.endlessBest : ''}</p>` : ''}\n        <p class=\"lvlname\">FINAL SCORE ${String(score).padStart(6, '0')}</p>\n        ${newRecord ? '<p class=\"blink\">NEW HI-SCORE!</p>' : ''}\n        <p class=\"keys\">SPACE / TAP FOR TITLE</p>\n      </div>`;\n  } else if (state === 'victory') {\n    html = `\n      <div class=\"panel good\">\n        <h1>CHAMPION!</h1>\n        <p class=\"sub\">ALL ${LEVELS.length} LEVELS DEMOLISHED</p>\n        <p class=\"lvlname\">FINAL SCORE ${String(score).padStart(6, '0')}</p>\n        ${newRecord ? '<p class=\"blink\">NEW HI-SCORE!</p>' : ''}\n        <p class=\"keys\">SPACE / TAP FOR TITLE</p>\n      </div>`;\n  }\n  el.screen.innerHTML = html;\n  el.screen.style.display = html ? 'flex' : 'none';\n}\n\n/* ---------- Input ---------- */\nfunction confirmAction() {\n  audio();\n  if (state === 'title') startGame();\n  else if (state === 'select') setState('title');\n  else if (state === 'paused') setState('play');\n  else if (state === 'gameover' || state === 'victory') { levelIx = 0; buildLevel(0); setState('title'); }\n  else if (state === 'intro' && stateT > 0.4) setState('play');\n}\nwindow.addEventListener('keydown', (e) => {\n  const k = e.key;\n  if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', ' '].includes(k)) e.preventDefault();\n  if (k === 'm' || k === 'M') {\n    muted = !muted;\n    if (masterGain) masterGain.gain.value = muted ? 0 : 0.17; // silences drone + score too\n    return;\n  }\n  if (k === 'p' || k === 'P') {\n    if (state === 'play') { setState('paused'); return; }\n    if (state === 'paused') { setState('play'); return; }\n  }\n  if (k === 'Escape') {\n    if (state === 'play') { setState('paused'); return; }\n    if (state === 'paused') { setState('play'); return; }\n    if (state === 'select') { setState('title'); return; }\n  }\n  if ((k === 'l' || k === 'L') && state === 'title' && saveData.unlocked > 1) { audio(); setState('select'); return; }\n  if ((k === 'e' || k === 'E') && state === 'title') { audio(); startEndless(); return; }\n  if (k === ' ' || k === 'Enter') { confirmAction(); return; }\n  if (state === 'intro') {\n    // the first move ends the survey and starts the clock\n    const dir = { ArrowUp: [0, -1], w: [0, -1], W: [0, -1], ArrowDown: [0, 1], s: [0, 1], S: [0, 1],\n                  ArrowLeft: [-1, 0], a: [-1, 0], A: [-1, 0], ArrowRight: [1, 0], d: [1, 0], D: [1, 0] }[k];\n    if (dir) { setState('play'); tryMove(dir[0], dir[1]); }\n    return;\n  }\n  if (state === 'play') {\n    if (k === 'ArrowUp' || k === 'w' || k === 'W') tryMove(0, -1);\n    else if (k === 'ArrowDown' || k === 's' || k === 'S') tryMove(0, 1);\n    else if (k === 'ArrowLeft' || k === 'a' || k === 'A') tryMove(-1, 0);\n    else if (k === 'ArrowRight' || k === 'd' || k === 'D') tryMove(1, 0);\n    else if (k === 'r' || k === 'R') die('RESTART!');\n  } else if (state === 'title') {\n    confirmAction();\n  }\n});\n// level-select buttons\nel.screen.addEventListener('click', (e) => {\n  const btn = e.target.closest('.lvl-btn');\n  if (btn && state === 'select' && !btn.disabled) {\n    audio();\n    startGame(+btn.dataset.level);\n  }\n});\nlet touchStart = null;\nwindow.addEventListener('pointerdown', (e) => { touchStart = { x: e.clientX, y: e.clientY }; });\nwindow.addEventListener('pointerup', (e) => {\n  if (!touchStart) return;\n  const dx = e.clientX - touchStart.x, dy = e.clientY - touchStart.y;\n  touchStart = null;\n  if (state === 'select') return; // the level grid handles its own clicks\n  if (Math.abs(dx) < 20 && Math.abs(dy) < 20) { confirmAction(); return; }\n  if (state === 'intro') setState('play'); // a swipe ends the survey\n  if (state !== 'play') return;\n  if (Math.abs(dx) > Math.abs(dy)) tryMove(dx > 0 ? 1 : -1, 0);\n  else tryMove(0, dy > 0 ? 1 : -1);\n});\nwindow.addEventListener('resize', () => {\n  const w = window.innerWidth, h = window.innerHeight;\n  camera.aspect = w / h;\n  camera.updateProjectionMatrix();\n  renderer.setSize(w, h);\n  composer.setSize(w, h);\n  bloomPass.resolution.set(w, h);\n  tiltH.uniforms.uTexel.value.set(1 / w, 1 / h);\n  tiltV.uniforms.uTexel.value.set(1 / w, 1 / h);\n});\n\n/* ---------- Debug handle (window.TNG) ---------- */\nwindow.TNG = {\n  get info() { return { state, levelIx, score, lives, blocksLeft, px, py, timeLeft: Math.round(timeLeft * 10) / 10 }; },\n  move(d) { const m = DIRS[d]; if (m) tryMove(m[0], m[1]); },\n  go() { confirmAction(); },\n  warp(n) { // jump to a campaign level (1-based) for testing\n    endless = false;\n    levelIx = Math.max(0, Math.min(LEVELS.length - 1, n - 1));\n    buildLevel(levelIx);\n    setState('intro');\n    return LEVELS[levelIx].name;\n  },\n  solve() { // auto-play the authored solution path for the current level\n    const L = activeLevel();\n    let i = 0;\n    const iv = setInterval(() => {\n      if (state !== 'play' || i >= L.path.length) { clearInterval(iv); return; }\n      if (!hop) { window.TNG.move(L.path[i]); i++; }\n    }, 60);\n  },\n};\n\n/* ---------- Main loop ---------- */\nlet last = 0;\nfunction frame(ts) {\n  const dt = Math.min(0.05, (ts - last) / 1000 || 0.016);\n  last = ts;\n  update(dt);\n  syncHUD();\n  composer.render();\n  requestAnimationFrame(frame);\n}\nbuildLevel(0);\ncamera.position.set(camDist * 0.8, camDist * 0.6, camDist * 0.8);\ncamera.lookAt(0, 0, 0);\nsetState('title');\nrequestAnimationFrame(frame);\n"},{"name":"voxels.js","content":"// TOUCH 'N' GO — voxel engine: field storage, face-culled merged meshes,\n// baked vertex AO, edge glint, per-voxel jitter, painted light temperature.\n// Technique lineage: the KILN voxel mesher, retuned for a cosmic void.\n\nimport * as THREE from 'three';\n\n/* ---------- palette: the whole game uses exactly these colours ---------- */\nexport const VOID_DEEP = 0x0b0a20;  // background, fog, soot-dark\nexport const INK       = 0x161430;  // shadowed undersides\nexport const MINT      = 0x1fbf92;  // 1-touch blocks — deep enough to sing on a light sky\nexport const MINT_DEEP = 0x117a5e;\nexport const AMBER     = 0xff9d2e;  // 2-touch blocks\nexport const AMBER_DEEP= 0xa15f0e;\nexport const CORAL     = 0xf25874;  // 3-touch blocks\nexport const CORAL_DEEP= 0x9c2f4d;\nexport const BONE      = 0xf2f2ff;  // player shell, UI text\nexport const BLUE      = 0x4ab6ff;  // visor / belt\nexport const RUST      = 0xd8452f;  // feet\nexport const GOLD      = 0xffd23a;  // antenna bulb, sparks — emissive\n\nexport const MAT = {\n  MINT: 'mint', MINT_DEEP: 'mintDeep',\n  AMBER: 'amber', AMBER_DEEP: 'amberDeep',\n  CORAL: 'coral', CORAL_DEEP: 'coralDeep',\n  BONE: 'bone', BLUE: 'blue', RUST: 'rust', INK: 'ink',\n  GOLD: 'gold',\n};\n\nconst MAT_DEFS = {\n  [MAT.MINT]:       { color: MINT, shiny: 0.13 },       // glazed hero tops\n  [MAT.MINT_DEEP]:  { color: MINT_DEEP },\n  [MAT.AMBER]:      { color: AMBER, shiny: 0.13 },\n  [MAT.AMBER_DEEP]: { color: AMBER_DEEP },\n  [MAT.CORAL]:      { color: CORAL, shiny: 0.13 },\n  [MAT.CORAL_DEEP]: { color: CORAL_DEEP },\n  [MAT.BONE]:       { color: BONE, shiny: 0.22 },       // porcelain marble\n  [MAT.BLUE]:       { color: BLUE, shiny: 0.2 },\n  [MAT.RUST]:       { color: RUST },\n  [MAT.INK]:        { color: INK },\n  [MAT.GOLD]:       { color: GOLD, emissive: true },\n};\n\n// tiny procedural cubemap — indigo void walls, black overhead, a faint\n// mint smoulder below. Gives the glazed surfaces something real to reflect.\nlet envMapCache = null;\nfunction makeVoidEnvMap() {\n  if (envMapCache) return envMapCache;\n  const makeFace = (stops) => {\n    const c = document.createElement('canvas');\n    c.width = c.height = 32;\n    const g = c.getContext('2d');\n    const grad = g.createLinearGradient(0, 0, 0, 32);\n    for (const [t, col] of stops) grad.addColorStop(t, col);\n    g.fillStyle = grad;\n    g.fillRect(0, 0, 32, 32);\n    return c;\n  };\n  const side = [[0, '#3a3468'], [0.55, '#a06a88'], [1, '#f2c49a']]; // dusk walls\n  const up = [[0, '#2b2650'], [1, '#4a3f70']];                      // twilight overhead\n  const down = [[0, '#f5c9a0'], [1, '#d99a7e']];                    // warm haze below\n  const tex = new THREE.CubeTexture([\n    makeFace(side), makeFace(side), // +x  -x\n    makeFace(up), makeFace(down),   // +y  -y\n    makeFace(side), makeFace(side), // +z  -z\n  ]);\n  tex.needsUpdate = true;\n  envMapCache = tex;\n  return tex;\n}\n\nlet materialCache = null;\nexport function getMaterials() {\n  if (materialCache) return materialCache;\n  materialCache = {};\n  for (const key of Object.keys(MAT_DEFS)) {\n    const def = MAT_DEFS[key];\n    if (def.emissive) {\n      materialCache[key] = new THREE.MeshLambertMaterial({\n        vertexColors: true,\n        emissive: def.color,\n        emissiveIntensity: 1.6, // hot enough to bloom\n      });\n    } else if (def.shiny) {\n      // glazed ceramic: tight specular + a whisper of void reflection\n      materialCache[key] = new THREE.MeshPhongMaterial({\n        vertexColors: true,\n        specular: new THREE.Color(0xffe0c0), // low-sun glint\n        shininess: 64,\n        envMap: makeVoidEnvMap(),\n        combine: THREE.MixOperation,\n        reflectivity: def.shiny,\n      });\n    } else {\n      // unglazed matte\n      materialCache[key] = new THREE.MeshLambertMaterial({ vertexColors: true });\n    }\n  }\n  return materialCache;\n}\n\n/* ---------- voxel field ---------- */\nexport function createField() {\n  const cells = new Map(); // \"x,y,z\" -> mat key\n  return {\n    cells,\n    set(x, y, z, mat) { cells.set(x + ',' + y + ',' + z, mat); },\n    unset(x, y, z) { cells.delete(x + ',' + y + ',' + z); },\n    has(x, y, z) { return cells.has(x + ',' + y + ',' + z); },\n    box(x, y, z, w, h, d, mat) {\n      for (let i = 0; i < w; i++)\n        for (let j = 0; j < h; j++)\n          for (let k = 0; k < d; k++)\n            cells.set((x + i) + ',' + (y + j) + ',' + (z + k), mat);\n    },\n  };\n}\n\n/* ---------- face tables ---------- */\nconst FACES = [\n  { dir: [-1, 0, 0], corners: [[0, 1, 0], [0, 0, 0], [0, 1, 1], [0, 0, 1]], shade: 0.80 },\n  { dir: [1, 0, 0], corners: [[1, 1, 1], [1, 0, 1], [1, 1, 0], [1, 0, 0]], shade: 0.80 },\n  { dir: [0, -1, 0], corners: [[1, 0, 1], [0, 0, 1], [1, 0, 0], [0, 0, 0]], shade: 0.55 },\n  { dir: [0, 1, 0], corners: [[0, 1, 1], [1, 1, 1], [0, 1, 0], [1, 1, 0]], shade: 1.0 },\n  { dir: [0, 0, -1], corners: [[1, 0, 0], [0, 0, 0], [1, 1, 0], [0, 1, 0]], shade: 0.72 },\n  { dir: [0, 0, 1], corners: [[0, 0, 1], [1, 0, 1], [0, 1, 1], [1, 1, 1]], shade: 0.88 },\n];\n\n// deterministic per-voxel brightness jitter — handmade feel, no two blocks identical\nfunction jitter(x, y, z) {\n  let h = (x * 374761393 + y * 668265263 + z * 2147483647) | 0;\n  h = (h ^ (h >> 13)) * 1274126177;\n  h = h ^ (h >> 16);\n  return 0.94 + ((h & 255) / 255) * 0.1; // 0.94 .. 1.04\n}\n\n// vertex AO: darkens concave corners so light catches edges — fakes a bevel\nfunction vertexAO(solidAt, vx, vy, vz, corner, dir) {\n  const na = dir[0] !== 0 ? 0 : dir[1] !== 0 ? 1 : 2;\n  const axes = [0, 1, 2].filter((a) => a !== na);\n  const b = [vx + dir[0], vy + dir[1], vz + dir[2]];\n  const t1 = [0, 0, 0]; t1[axes[0]] = corner[axes[0]] === 1 ? 1 : -1;\n  const t2 = [0, 0, 0]; t2[axes[1]] = corner[axes[1]] === 1 ? 1 : -1;\n  const s1 = solidAt(b[0] + t1[0], b[1] + t1[1], b[2] + t1[2]) ? 1 : 0;\n  const s2 = solidAt(b[0] + t2[0], b[1] + t2[1], b[2] + t2[2]) ? 1 : 0;\n  const c = solidAt(b[0] + t1[0] + t2[0], b[1] + t1[1] + t2[1], b[2] + t1[2] + t2[2]) ? 1 : 0;\n  const occ = s1 && s2 ? 3 : s1 + s2 + c;\n  return 1 - occ * 0.18;\n}\n\n/* ---------- merged mesh building ---------- */\n// One merged Mesh per material. seed offsets the jitter hash so identical\n// slabs still shimmer differently tile to tile.\nexport function buildFieldMesh(field, { voxelSize = 0.13, offset = [0, 0, 0], seed = 0 } = {}) {\n  const mats = getMaterials();\n  const buckets = new Map();\n  const solidAt = (x, y, z) => field.has(x, y, z);\n  const tmpColor = new THREE.Color();\n\n  for (const [key, mat] of field.cells) {\n    const [x, y, z] = key.split(',').map(Number);\n    let bucket = buckets.get(mat);\n    if (!bucket) {\n      bucket = { pos: [], nrm: [], col: [], idx: [] };\n      buckets.set(mat, bucket);\n    }\n    const jit = jitter(x + seed, y + seed * 3, z - seed);\n    tmpColor.setHex(MAT_DEFS[mat] ? MAT_DEFS[mat].color : BONE);\n\n    for (const face of FACES) {\n      const nx = x + face.dir[0], ny = y + face.dir[1], nz = z + face.dir[2];\n      if (solidAt(nx, ny, nz)) continue;\n      const start = bucket.pos.length / 3;\n      for (const corner of face.corners) {\n        bucket.pos.push(\n          (x + corner[0]) * voxelSize + offset[0],\n          (y + corner[1]) * voxelSize + offset[1],\n          (z + corner[2]) * voxelSize + offset[2]\n        );\n        bucket.nrm.push(face.dir[0], face.dir[1], face.dir[2]);\n        const ao = vertexAO(solidAt, x, y, z, corner, face.dir);\n        // edge glint: convex edges catch the light — fakes a chamfered bevel\n        let glint = 1;\n        if (face.dir[1] === 1) {\n          const ex = corner[0] === 1 ? 1 : -1, ez = corner[2] === 1 ? 1 : -1;\n          if (!solidAt(x + ex, y, z) || !solidAt(x, y, z + ez)) glint = 1.1;\n        } else if (face.dir[1] === 0 && corner[1] === 1 && !solidAt(x, y + 1, z)) {\n          glint = 1.12;\n        }\n        const v = face.shade * ao * jit * glint;\n        // painted light temperature: undersides catch the warm haze glowing\n        // below, tops take the golden key, side bases warm toward the light\n        let tr = 1, tg = 1, tb = 1;\n        if (face.dir[1] === -1) { tr = 1.28; tg = 0.92; tb = 0.72; }\n        else if (face.dir[1] === 1) { tr = 1.05; tg = 1.0; tb = 0.94; }\n        else if (corner[1] === 0) { tr = 1.12; tg = 0.98; tb = 0.86; }\n        bucket.col.push(tmpColor.r * v * tr, tmpColor.g * v * tg, tmpColor.b * v * tb);\n      }\n      bucket.idx.push(start, start + 1, start + 2, start + 2, start + 1, start + 3);\n    }\n  }\n\n  const group = new THREE.Group();\n  for (const [mat, b] of buckets) {\n    if (b.idx.length === 0) continue;\n    const geo = new THREE.BufferGeometry();\n    geo.setAttribute('position', new THREE.Float32BufferAttribute(b.pos, 3));\n    geo.setAttribute('normal', new THREE.Float32BufferAttribute(b.nrm, 3));\n    geo.setAttribute('color', new THREE.Float32BufferAttribute(b.col, 3));\n    geo.setIndex(b.idx);\n    const mesh = new THREE.Mesh(geo, mats[mat]);\n    mesh.castShadow = true;\n    mesh.receiveShadow = true;\n    group.add(mesh);\n  }\n  return group;\n}\n\n/* ---------- model helper: origin at bottom-centre ---------- */\nexport function buildModelMesh(voxels, { voxelSize = 0.09, seed = 0 } = {}) {\n  const field = createField();\n  let minX = Infinity, maxX = -Infinity, minY = Infinity, minZ = Infinity, maxZ = -Infinity;\n  for (const [x, y, z, m] of voxels) {\n    field.set(x, y, z, m);\n    if (x < minX) minX = x; if (x + 1 > maxX) maxX = x + 1;\n    if (y < minY) minY = y;\n    if (z < minZ) minZ = z; if (z + 1 > maxZ) maxZ = z + 1;\n  }\n  const cx = (minX + maxX) / 2, cz = (minZ + maxZ) / 2;\n  return buildFieldMesh(field, {\n    voxelSize, seed,\n    offset: [-cx * voxelSize, -minY * voxelSize, -cz * voxelSize],\n  });\n}\n\n/* ---------- tile layer slab: 7x2x7 voxels, chamfered, nicked ---------- */\n// hash for deterministic per-tile nicks\nfunction tileHash(a, b, c) {\n  let h = (a * 73856093) ^ (b * 19349663) ^ (c * 83492791);\n  h = (h ^ (h >> 13)) * 1274126177;\n  return (h ^ (h >> 16)) >>> 0;\n}\n\nconst SLAB_MATS = [\n  [MAT.MINT, MAT.MINT_DEEP],\n  [MAT.AMBER, MAT.AMBER_DEEP],\n  [MAT.CORAL, MAT.CORAL_DEEP],\n];\n\nexport function buildLayerSlab(layerIx, tileX, tileY, voxelSize = 0.134) {\n  const [topMat, botMat] = SLAB_MATS[Math.min(layerIx, SLAB_MATS.length - 1)];\n  const field = createField();\n  field.box(0, 0, 0, 7, 1, 7, botMat);   // lower row — deep shade colour\n  field.box(0, 1, 0, 7, 1, 7, topMat);   // upper row — hero colour\n  // clean square slabs — variation comes from jitter + AO shading, not cutouts\n  const half = 3.5 * voxelSize;\n  return buildFieldMesh(field, {\n    voxelSize,\n    offset: [-half, 0, -half],\n    seed: tileHash(tileX, tileY, layerIx) % 1000,\n  });\n}\n\n/* ---------- the player: a rolling voxel ball ---------- */\n// 11x11x11 voxel sphere — fine enough to read round, chunky enough to\n// read voxel. Porcelain shell, a thin terracotta equator band, and\n// gold emissive plus-shaped studs at the six poles so rolls read clearly.\nexport const BALL_R = 0.4275;                 // world-space radius\nexport const BALL_VOX = (BALL_R * 2) / 11;\n\nexport function buildBallModel(voxelSize = BALL_VOX) {\n  const cells = new Map();\n  const k = (x, y, z) => x + ',' + y + ',' + z;\n  const C = 5, R2 = 5.3 * 5.3;\n  for (let x = 0; x <= 10; x++)\n    for (let y = 0; y <= 10; y++)\n      for (let z = 0; z <= 10; z++) {\n        const dx = x - C, dy = y - C, dz = z - C;\n        const d2 = dx * dx + dy * dy + dz * dz;\n        if (d2 > R2) continue;\n        let m = MAT.BONE;\n        // one terracotta quarter-wedge — the roll is always readable\n        const wedge = Math.floor(((Math.atan2(dz, dx) + Math.PI) / (Math.PI * 2)) * 4) % 4;\n        if (wedge === 1) m = MAT.RUST;\n        // small gold polar caps — they tumble and catch the bloom\n        if (Math.abs(dy) >= 4 && dx * dx + dz * dz <= 5) m = MAT.GOLD;\n        cells.set(k(x, y, z), m);\n      }\n  const v = [];\n  for (const [kk, m] of cells) {\n    const [x, y, z] = kk.split(',').map(Number);\n    v.push([x, y, z, m]);\n  }\n  return buildModelMesh(v, { voxelSize, seed: 77 });\n}\n\n/* ---------- the exit pad: indestructible, gold-hearted ---------- */\nexport function buildExitSlab(voxelSize = 0.134) {\n  const field = createField();\n  field.box(0, 0, 0, 7, 1, 7, MAT.INK);    // dark base\n  field.box(0, 1, 0, 7, 1, 7, MAT.BONE);   // porcelain top\n  field.box(2, 1, 2, 3, 1, 3, MAT.GOLD);   // emissive gold heart\n  const half = 3.5 * voxelSize;\n  return buildFieldMesh(field, { voxelSize, offset: [-half, 0, -half], seed: 4242 });\n}\n\n/* ---------- loose debris voxel (pops, shatters) ---------- */\nlet debrisGeo = null;\nexport function makeDebrisVoxel(colorHex, size = 0.13) {\n  if (!debrisGeo) debrisGeo = new THREE.BoxGeometry(1, 1, 1);\n  const m = new THREE.Mesh(\n    debrisGeo,\n    new THREE.MeshLambertMaterial({ color: colorHex })\n  );\n  m.scale.setScalar(size);\n  m.castShadow = true;\n  m.userData.sharedGeo = true;    // never dispose the shared box\n  m.userData.ownMaterial = true;  // but do dispose this mesh's material\n  return m;\n}\n\nexport const LAYER_TOP_COLORS = [MINT, AMBER, CORAL];\nexport const LAYER_DEEP_COLORS = [MINT_DEEP, AMBER_DEEP, CORAL_DEEP];\n"}],"folders":[]},{"folder":"other","files":[{"name":"main.sketch","content":""}],"folders":[],"collapsed":true}]},"variants":null,"createdAt":"2026-07-11T08:27:09.774Z","updatedAt":"2026-07-12T15:34:42.484Z"}}