{"project":{"id":"53YkLsv","userId":"davidyarham@gmail.com","username":null,"userPicture":null,"name":"Stack","thumbnail":"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","visible":true,"contributors":"","githubRepo":null,"forkedFrom":null,"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>Untitled</title>\n  <link rel=\"stylesheet\" href=\"style.css\">\n<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n<link href=\"https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@500;700&family=Syne:wght@600;700;800&display=swap\" rel=\"stylesheet\">\n<script src=\"https://unpkg.com/three@0.160.0/build/three.min.js\"></script>\n<script src=\"https://unpkg.com/lucide@latest\"></script>\n</head>\n<body>\n<div id=\"game-container\">\n\t<canvas id=\"game-canvas\"></canvas>\n\t<canvas id=\"particle-canvas\"></canvas>\n\t<div id=\"ui-overlay\">\n\t\t<div id=\"score-panel\">\n\t\t\t<div class=\"score-block\">\n\t\t\t\t<span class=\"score-label\">SCORE</span>\n\t\t\t\t<span id=\"score\" class=\"score-value\">0</span>\n\t\t\t</div>\n\t\t\t<div class=\"score-block score-block--hi\">\n\t\t\t\t<span class=\"score-label\">BEST</span>\n\t\t\t\t<span id=\"hiscore\" class=\"score-value\">0</span>\n\t\t\t</div>\n\t\t</div>\n\t\t<div id=\"start-screen\">\n\t\t\t<h1 class=\"game-title\">STACK</h1>\n\t\t\t<p class=\"game-subtitle\">Tap to place blocks</p>\n\t\t\t<button id=\"start-btn\" class=\"btn-play\">\n\t\t\t\t<i data-lucide=\"play\" class=\"btn-play__icon\"></i>\n\t\t\t\t<span>PLAY</span>\n\t\t\t</button>\n\t\t</div>\n\t\t<div id=\"game-over-screen\" class=\"hidden\">\n\t\t\t<div class=\"game-over-card\">\n\t\t\t\t<h2 class=\"game-over-title\">GAME OVER</h2>\n\t\t\t\t<div class=\"final-score-display\">\n\t\t\t\t\t<span class=\"final-score-label\">SCORE</span>\n\t\t\t\t\t<span id=\"final-score\" class=\"final-score-num\">0</span>\n\t\t\t\t</div>\n\t\t\t\t<div id=\"new-best\" class=\"new-best hidden\">★ NEW BEST ★</div>\n\t\t\t\t<button id=\"restart-btn\" class=\"btn-play\">\n\t\t\t\t\t<i data-lucide=\"rotate-ccw\" class=\"btn-play__icon\"></i>\n\t\t\t\t\t<span>RETRY</span>\n\t\t\t\t</button>\n\t\t\t</div>\n\t\t</div>\n\t\t<div id=\"tap-hint\" class=\"hidden\">\n\t\t\t<span>TAP ANYWHERE</span>\n\t\t</div>\n\t</div>\n\t<div id=\"combo-display\" class=\"hidden\"></div>\n</div>\n  <script type=\"module\" src=\"main.js\"></script>\n</body>\n</html>"},{"name":"main.js","content":"// ===== THREE.JS STACK GAME =====\nconst {\n\tScene,\n\tPerspectiveCamera,\n\tWebGLRenderer,\n\tDirectionalLight,\n\tAmbientLight,\n\tBoxGeometry,\n\tMeshStandardMaterial,\n\tMesh,\n\tColor,\n\tFog,\n\tVector3,\n\tGroup,\n\tHemisphereLight,\n\tPointLight\n} = THREE;\n\n// ===== SOUND ENGINE =====\nclass SoundEngine {\n\tconstructor() {\n\t\tthis.ctx = null;\n\t\tthis.initialized = false;\n\t}\n\tinit() {\n\t\tif (this.initialized) return;\n\t\tthis.ctx = new(window.AudioContext || window.webkitAudioContext)();\n\t\tthis.initialized = true;\n\t}\n\tplay(type, noteOffset = 0) {\n\t\tif (!this.ctx) return;\n\t\tif (this.ctx.state === 'suspended') this.ctx.resume();\n\t\tconst t = this.ctx.currentTime;\n\t\tif (type === 'place') {\n\t\t\tconst osc = this.ctx.createOscillator();\n\t\t\tconst gain = this.ctx.createGain();\n\t\t\tosc.type = 'sine';\n\t\t\tconst freq = 260 + noteOffset * 18;\n\t\t\tosc.frequency.setValueAtTime(Math.min(freq, 980), t);\n\t\t\tosc.frequency.exponentialRampToValueAtTime(Math.min(freq * 1.4, 1400), t + 0.06);\n\t\t\tgain.gain.setValueAtTime(0.13, t);\n\t\t\tgain.gain.exponentialRampToValueAtTime(0.001, t + 0.25);\n\t\t\tosc.connect(gain).connect(this.ctx.destination);\n\t\t\tosc.start(t);\n\t\t\tosc.stop(t + 0.25);\n\t\t}\n\t\tif (type === 'perfect') {\n\t\t\tconst notes = [523, 659, 784, 1047];\n\t\t\tnotes.forEach((f, i) => {\n\t\t\t\tconst osc = this.ctx.createOscillator();\n\t\t\t\tconst gain = this.ctx.createGain();\n\t\t\t\tosc.type = 'sine';\n\t\t\t\tconst delay = i * 0.07;\n\t\t\t\tosc.frequency.setValueAtTime(f, t + delay);\n\t\t\t\tgain.gain.setValueAtTime(0.1, t + delay);\n\t\t\t\tgain.gain.exponentialRampToValueAtTime(0.001, t + delay + 0.35);\n\t\t\t\tosc.connect(gain).connect(this.ctx.destination);\n\t\t\t\tosc.start(t + delay);\n\t\t\t\tosc.stop(t + delay + 0.4);\n\t\t\t});\n\t\t}\n\t\tif (type === 'fall') {\n\t\t\tconst osc = this.ctx.createOscillator();\n\t\t\tconst gain = this.ctx.createGain();\n\t\t\tosc.type = 'sawtooth';\n\t\t\tosc.frequency.setValueAtTime(350, t);\n\t\t\tosc.frequency.exponentialRampToValueAtTime(40, t + 0.7);\n\t\t\tgain.gain.setValueAtTime(0.08, t);\n\t\t\tgain.gain.exponentialRampToValueAtTime(0.001, t + 0.7);\n\t\t\tosc.connect(gain).connect(this.ctx.destination);\n\t\t\tosc.start(t);\n\t\t\tosc.stop(t + 0.75);\n\t\t}\n\t\tif (type === 'combo') {\n\t\t\tconst base = 520 + noteOffset * 40;\n\t\t\tconst osc = this.ctx.createOscillator();\n\t\t\tconst gain = this.ctx.createGain();\n\t\t\tosc.type = 'triangle';\n\t\t\tosc.frequency.setValueAtTime(Math.min(base, 1200), t);\n\t\t\tgain.gain.setValueAtTime(0.1, t);\n\t\t\tgain.gain.exponentialRampToValueAtTime(0.001, t + 0.3);\n\t\t\tosc.connect(gain).connect(this.ctx.destination);\n\t\t\tosc.start(t);\n\t\t\tosc.stop(t + 0.35);\n\t\t}\n\t}\n}\n\n// ===== HSL COLOR SYSTEM =====\nfunction getBlockColor(index) {\n\tconst hue = (index * 14 + 340) % 360;\n\tconst sat = 65 + Math.sin(index * 0.3) * 15;\n\tconst light = 52 + Math.sin(index * 0.5) * 8;\n\treturn `hsl(${hue}, ${sat}%, ${light}%)`;\n}\n\nfunction getBlockEmissive(index) {\n\tconst hue = (index * 14 + 340) % 360;\n\treturn `hsl(${hue}, 40%, 15%)`;\n}\n\n// ===== PARTICLE SYSTEM (2D CANVAS) =====\nclass ParticleSystem {\n\tconstructor(canvas) {\n\t\tthis.canvas = canvas;\n\t\tthis.ctx = canvas.getContext('2d');\n\t\tthis.particles = [];\n\t\tthis.resize();\n\t\twindow.addEventListener('resize', () => this.resize());\n\t}\n\tresize() {\n\t\tthis.canvas.width = window.innerWidth * Math.min(devicePixelRatio, 2);\n\t\tthis.canvas.height = window.innerHeight * Math.min(devicePixelRatio, 2);\n\t}\n\temit(screenX, screenY, color, count = 12) {\n\t\tconst scale = Math.min(devicePixelRatio, 2);\n\t\tfor (let i = 0; i < count; i++) {\n\t\t\tconst angle = (Math.PI * 2 * i / count) + (Math.random() - 0.5) * 0.5;\n\t\t\tconst speed = 2 + Math.random() * 4;\n\t\t\tthis.particles.push({\n\t\t\t\tx: screenX * scale,\n\t\t\t\ty: screenY * scale,\n\t\t\t\tvx: Math.cos(angle) * speed * scale,\n\t\t\t\tvy: Math.sin(angle) * speed * scale - 2 * scale,\n\t\t\t\tlife: 1,\n\t\t\t\tdecay: 0.015 + Math.random() * 0.02,\n\t\t\t\tsize: (2 + Math.random() * 3) * scale,\n\t\t\t\tcolor: color\n\t\t\t});\n\t\t}\n\t}\n\tupdate() {\n\t\tthis.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);\n\t\tfor (let i = this.particles.length - 1; i >= 0; i--) {\n\t\t\tconst p = this.particles[i];\n\t\t\tp.x += p.vx;\n\t\t\tp.y += p.vy;\n\t\t\tp.vy += 0.12;\n\t\t\tp.vx *= 0.98;\n\t\t\tp.life -= p.decay;\n\t\t\tif (p.life <= 0) {\n\t\t\t\tthis.particles.splice(i, 1);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tthis.ctx.globalAlpha = p.life * p.life;\n\t\t\tthis.ctx.fillStyle = p.color;\n\t\t\tthis.ctx.beginPath();\n\t\t\tthis.ctx.arc(p.x, p.y, p.size * p.life, 0, Math.PI * 2);\n\t\t\tthis.ctx.fill();\n\t\t}\n\t\tthis.ctx.globalAlpha = 1;\n\t}\n}\n\n// ===== GAME STATE =====\nlet scene, camera, renderer, gameGroup;\nlet stack = [];\nlet fallingPieces = [];\nlet gameState = 'idle';\nlet score = 0;\nlet hiScore = parseInt(localStorage.getItem('stackHiScore') || '0');\nlet combo = 0;\nlet baseSpeed = 0.04;\nlet animFrameId;\nlet groupTargetY = 0;\nlet moveTime = 0;\nlet particles;\nconst BLOCK_HEIGHT = 0.35;\nconst PERFECT_THRESHOLD = 0.15;\nconst SWING_AMPLITUDE = 5.5;\n\nconst sound = new SoundEngine();\n\n// DOM refs\nconst canvas = document.getElementById('game-canvas');\nconst scoreEl = document.getElementById('score');\nconst hiScoreEl = document.getElementById('hiscore');\nconst startScreen = document.getElementById('start-screen');\nconst gameOverScreen = document.getElementById('game-over-screen');\nconst finalScoreEl = document.getElementById('final-score');\nconst newBestEl = document.getElementById('new-best');\nconst tapHint = document.getElementById('tap-hint');\nconst comboDisplay = document.getElementById('combo-display');\n\nhiScoreEl.textContent = hiScore;\n\n// ===== INIT THREE.JS =====\nfunction initScene() {\n\tscene = new Scene();\n\tscene.background = new Color('#1a1a2e');\n\tscene.fog = new Fog('#1a1a2e', 14, 30);\n\n\tcamera = new PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 100);\n\tcamera.position.set(6, 8, 6);\n\tcamera.lookAt(0, 0, 0);\n\n\trenderer = new WebGLRenderer({\n\t\tcanvas,\n\t\tantialias: true,\n\t\talpha: false\n\t});\n\trenderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));\n\trenderer.setSize(window.innerWidth, window.innerHeight);\n\trenderer.shadowMap.enabled = true;\n\trenderer.shadowMap.type = THREE.PCFSoftShadowMap;\n\trenderer.toneMapping = THREE.ACESFilmicToneMapping;\n\trenderer.toneMappingExposure = 1.1;\n\n\tconst hemiLight = new HemisphereLight(0x6677aa, 0x223344, 0.6);\n\tscene.add(hemiLight);\n\n\tconst dirLight = new DirectionalLight(0xffeedd, 2.0);\n\tdirLight.position.set(5, 15, 8);\n\tdirLight.castShadow = true;\n\tdirLight.shadow.camera.near = 0.1;\n\tdirLight.shadow.camera.far = 50;\n\tdirLight.shadow.camera.left = -10;\n\tdirLight.shadow.camera.right = 10;\n\tdirLight.shadow.camera.top = 10;\n\tdirLight.shadow.camera.bottom = -10;\n\tdirLight.shadow.mapSize.set(2048, 2048);\n\tdirLight.shadow.bias = -0.001;\n\tscene.add(dirLight);\n\n\tconst ambLight = new AmbientLight(0x8899cc, 0.3);\n\tscene.add(ambLight);\n\n\tconst fillLight = new DirectionalLight(0xaabbff, 0.5);\n\tfillLight.position.set(-5, 8, -5);\n\tscene.add(fillLight);\n\n\tgameGroup = new Group();\n\tscene.add(gameGroup);\n\n\tparticles = new ParticleSystem(document.getElementById('particle-canvas'));\n}\n\nfunction createBlock(x, y, z, width, depth, colorIndex) {\n\tconst geo = new BoxGeometry(width, BLOCK_HEIGHT, depth);\n\tconst mat = new MeshStandardMaterial({\n\t\tcolor: new Color(getBlockColor(colorIndex)),\n\t\temissive: new Color(getBlockEmissive(colorIndex)),\n\t\temissiveIntensity: 0.15,\n\t\troughness: 0.45,\n\t\tmetalness: 0.15,\n\t});\n\tconst mesh = new Mesh(geo, mat);\n\tmesh.position.set(x, y, z);\n\tmesh.castShadow = true;\n\tmesh.receiveShadow = true;\n\tmesh.userData.colorIndex = colorIndex;\n\treturn mesh;\n}\n\nfunction resetGame() {\n\twhile (gameGroup.children.length > 0) {\n\t\tconst child = gameGroup.children[0];\n\t\tgameGroup.remove(child);\n\t\tif (child.geometry) child.geometry.dispose();\n\t\tif (child.material) child.material.dispose();\n\t}\n\tfallingPieces = [];\n\tstack = [];\n\tscore = 0;\n\tcombo = 0;\n\tbaseSpeed = 0.04;\n\tgroupTargetY = 0;\n\tmoveTime = 0;\n\n\tscoreEl.textContent = '0';\n\n\tconst base = createBlock(0, 0, 0, 3, 3, 0);\n\tgameGroup.add(base);\n\tgameGroup.position.y = 0;\n\tstack.push({\n\t\tmesh: base,\n\t\twidth: 3,\n\t\tdepth: 3,\n\t\tx: 0,\n\t\tz: 0,\n\t\taxis: 'x'\n\t});\n\n\taddNewBlock();\n}\n\nfunction addNewBlock() {\n\tconst prev = stack[stack.length - 1];\n\tconst y = stack.length * BLOCK_HEIGHT;\n\tconst axis = stack.length % 2 === 1 ? 'x' : 'z';\n\n\tconst x = axis === 'x' ? -SWING_AMPLITUDE : prev.x;\n\tconst z = axis === 'z' ? -SWING_AMPLITUDE : prev.z;\n\n\tconst block = createBlock(x, y, z, prev.width, prev.depth, stack.length);\n\tgameGroup.add(block);\n\n\tmoveTime = -Math.PI / 2; // Start from the left edge, moving right via sin\n\n\tstack.push({\n\t\tmesh: block,\n\t\twidth: prev.width,\n\t\tdepth: prev.depth,\n\t\tx: x,\n\t\tz: z,\n\t\taxis: axis,\n\t\tmoving: true\n\t});\n}\n\nfunction projectToScreen(worldPos) {\n\tconst v = worldPos.clone().project(camera);\n\treturn {\n\t\tx: (v.x * 0.5 + 0.5) * window.innerWidth,\n\t\ty: (-v.y * 0.5 + 0.5) * window.innerHeight\n\t};\n}\n\nfunction placeBlock() {\n\tconst current = stack[stack.length - 1];\n\tif (!current.moving) return;\n\tcurrent.moving = false;\n\n\tconst prev = stack[stack.length - 2];\n\tlet currentPos, prevPos, currentSize;\n\n\tif (current.axis === 'x') {\n\t\tcurrentPos = current.mesh.position.x;\n\t\tprevPos = prev.x;\n\t\tcurrentSize = current.width;\n\t} else {\n\t\tcurrentPos = current.mesh.position.z;\n\t\tprevPos = prev.z;\n\t\tcurrentSize = current.depth;\n\t}\n\n\tconst overshoot = currentPos - prevPos;\n\tconst overlap = currentSize - Math.abs(overshoot);\n\n\tif (overlap <= 0) {\n\t\tgameOver();\n\t\treturn;\n\t}\n\n\t// Perfect placement\n\tif (Math.abs(overshoot) < PERFECT_THRESHOLD) {\n\t\tcombo++;\n\t\tsound.play('perfect');\n\n\t\t// Snap to previous position\n\t\tif (current.axis === 'x') {\n\t\t\tcurrent.mesh.position.x = prevPos;\n\t\t\tcurrent.x = prevPos;\n\t\t} else {\n\t\t\tcurrent.mesh.position.z = prevPos;\n\t\t\tcurrent.z = prevPos;\n\t\t}\n\n\t\tshowCombo();\n\n\t\t// Emit particles for perfect\n\t\tconst sPos = projectToScreen(current.mesh.position);\n\t\tconst clr = getBlockColor(stack.length - 1);\n\t\tparticles.emit(sPos.x, sPos.y, clr, 20 + combo * 4);\n\t\tparticles.emit(sPos.x, sPos.y, '#ffc947', 8);\n\n\t\t// Grow block on combo streak\n\t\tif (combo >= 3) {\n\t\t\tconst growAmt = 0.15;\n\t\t\tif (current.axis === 'x') {\n\t\t\t\tcurrent.depth = Math.min(current.depth + growAmt, 3);\n\t\t\t} else {\n\t\t\t\tcurrent.width = Math.min(current.width + growAmt, 3);\n\t\t\t}\n\t\t\tcurrent.mesh.geometry.dispose();\n\t\t\tcurrent.mesh.geometry = new BoxGeometry(current.width, BLOCK_HEIGHT, current.depth);\n\t\t}\n\n\t\t// Pulse effect on the block\n\t\tconst origY = current.mesh.position.y;\n\t\tconst startT = performance.now();\n\n\t\tfunction pulseAnim(now) {\n\t\t\tconst elapsed = (now - startT) / 300;\n\t\t\tif (elapsed >= 1) {\n\t\t\t\tcurrent.mesh.position.y = origY;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcurrent.mesh.position.y = origY + Math.sin(elapsed * Math.PI) * 0.06;\n\t\t\tcurrent.mesh.scale.setScalar(1 + Math.sin(elapsed * Math.PI) * 0.03);\n\t\t\trequestAnimationFrame(pulseAnim);\n\t\t}\n\t\trequestAnimationFrame(pulseAnim);\n\n\t} else {\n\t\tcombo = 0;\n\n\t\t// Calculate trimmed and cut pieces properly\n\t\tconst newSize = overlap;\n\t\t// The center of the overlapping region\n\t\tconst newPos = prevPos + overshoot / 2;\n\n\t\t// The cut-off piece\n\t\tconst cutSize = Math.abs(overshoot);\n\t\tconst cutPos = overshoot > 0 ?\n\t\t\t(newPos + newSize / 2 + cutSize / 2) :\n\t\t\t(newPos - newSize / 2 - cutSize / 2);\n\n\t\tlet fallGeo, fallX, fallZ;\n\t\tif (current.axis === 'x') {\n\t\t\tfallGeo = new BoxGeometry(cutSize, BLOCK_HEIGHT, current.depth);\n\t\t\tfallX = cutPos;\n\t\t\tfallZ = current.mesh.position.z;\n\t\t} else {\n\t\t\tfallGeo = new BoxGeometry(current.width, BLOCK_HEIGHT, cutSize);\n\t\t\tfallX = current.mesh.position.x;\n\t\t\tfallZ = cutPos;\n\t\t}\n\n\t\tconst fallMat = new MeshStandardMaterial({\n\t\t\tcolor: current.mesh.material.color.clone(),\n\t\t\temissive: current.mesh.material.emissive.clone(),\n\t\t\temissiveIntensity: 0.1,\n\t\t\troughness: 0.5,\n\t\t\tmetalness: 0.1,\n\t\t\ttransparent: true,\n\t\t\topacity: 1\n\t\t});\n\t\tconst fallMesh = new Mesh(fallGeo, fallMat);\n\t\tfallMesh.position.set(fallX, current.mesh.position.y, fallZ);\n\t\tfallMesh.castShadow = true;\n\t\tgameGroup.add(fallMesh);\n\t\tfallingPieces.push({\n\t\t\tmesh: fallMesh,\n\t\t\tvelocity: 0,\n\t\t\trotSpeed: (Math.random() - 0.5) * 0.12,\n\t\t\taxis: current.axis,\n\t\t\topacity: 1\n\t\t});\n\n\t\t// Update the placed block\n\t\tif (current.axis === 'x') {\n\t\t\tcurrent.width = newSize;\n\t\t\tcurrent.mesh.position.x = newPos;\n\t\t\tcurrent.x = newPos;\n\t\t} else {\n\t\t\tcurrent.depth = newSize;\n\t\t\tcurrent.mesh.position.z = newPos;\n\t\t\tcurrent.z = newPos;\n\t\t}\n\n\t\tcurrent.mesh.geometry.dispose();\n\t\tcurrent.mesh.geometry = new BoxGeometry(current.width, BLOCK_HEIGHT, current.depth);\n\n\t\tsound.play('place', score);\n\n\t\t// Small particle burst on slice\n\t\tconst sPos = projectToScreen(new Vector3(fallX, current.mesh.position.y, fallZ));\n\t\tparticles.emit(sPos.x, sPos.y, getBlockColor(stack.length - 1), 6);\n\t}\n\n\tscore++;\n\tscoreEl.textContent = score;\n\n\t// Animate score bump\n\tconst scoreBlock = scoreEl.closest('.score-block');\n\tscoreBlock.classList.remove('score-bump');\n\tvoid scoreBlock.offsetWidth;\n\tscoreBlock.classList.add('score-bump');\n\n\tif (score > hiScore) {\n\t\thiScore = score;\n\t\thiScoreEl.textContent = hiScore;\n\t\tlocalStorage.setItem('stackHiScore', hiScore.toString());\n\t}\n\n\tbaseSpeed = 0.04 + Math.min(score * 0.002, 0.06);\n\tgroupTargetY = -(stack.length - 1) * BLOCK_HEIGHT;\n\n\taddNewBlock();\n}\n\nfunction showCombo() {\n\tif (combo >= 2) {\n\t\tconst texts = ['PERFECT', 'PERFECT', 'AMAZING', 'INCREDIBLE', 'GODLIKE', 'UNSTOPPABLE'];\n\t\tconst tier = Math.min(combo - 2, texts.length - 1);\n\t\tcomboDisplay.textContent = `${texts[tier]} ×${combo}`;\n\n\t\t// Color based on combo level\n\t\tconst hue = combo * 30 % 360;\n\t\tcomboDisplay.style.color = combo >= 5 ? '#ff3366' : `hsl(${40 + hue}, 100%, 65%)`;\n\t\tcomboDisplay.style.textShadow = `0 2px 30px hsla(${40 + hue}, 100%, 50%, 0.6)`;\n\n\t\tcomboDisplay.classList.remove('hidden');\n\t\tcomboDisplay.classList.remove('combo-animate');\n\t\tvoid comboDisplay.offsetWidth;\n\t\tcomboDisplay.classList.add('combo-animate');\n\t\tsound.play('combo', combo);\n\t\tsetTimeout(() => comboDisplay.classList.add('hidden'), 800);\n\t}\n}\n\nfunction gameOver() {\n\tgameState = 'over';\n\tconst current = stack[stack.length - 1];\n\n\tfallingPieces.push({\n\t\tmesh: current.mesh,\n\t\tvelocity: 0.02,\n\t\trotSpeed: (Math.random() - 0.5) * 0.15,\n\t\taxis: current.axis,\n\t\topacity: 1\n\t});\n\tstack.pop();\n\n\tsound.play('fall');\n\ttapHint.classList.add('hidden');\n\n\tsetTimeout(() => {\n\t\tfinalScoreEl.textContent = score;\n\t\tif (score >= hiScore && score > 0) {\n\t\t\tnewBestEl.classList.remove('hidden');\n\t\t} else {\n\t\t\tnewBestEl.classList.add('hidden');\n\t\t}\n\t\tgameOverScreen.classList.remove('hidden');\n\t}, 800);\n}\n\n// ===== GAME LOOP =====\nlet lastTime = 0;\n\nfunction animate(now = 0) {\n\tanimFrameId = requestAnimationFrame(animate);\n\tconst dt = Math.min((now - lastTime) / 16.667, 3); // normalize to ~60fps\n\tlastTime = now;\n\n\t// Sinusoidal movement for current block (smooth easing at edges)\n\tif (gameState === 'playing') {\n\t\tconst current = stack[stack.length - 1];\n\t\tif (current && current.moving) {\n\t\t\tconst speed = baseSpeed + baseSpeed * 0.3 * Math.sin(score * 0.1); // slight variation\n\t\t\tmoveTime += speed * dt;\n\t\t\tconst pos = Math.sin(moveTime) * SWING_AMPLITUDE;\n\t\t\tif (current.axis === 'x') {\n\t\t\t\tcurrent.mesh.position.x = pos;\n\t\t\t} else {\n\t\t\t\tcurrent.mesh.position.z = pos;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Animate falling pieces\n\tfor (let i = fallingPieces.length - 1; i >= 0; i--) {\n\t\tconst fp = fallingPieces[i];\n\t\tfp.velocity -= 0.01 * dt;\n\t\tfp.mesh.position.y += fp.velocity * dt;\n\n\t\tif (fp.axis === 'x') {\n\t\t\tfp.mesh.rotation.z += fp.rotSpeed * dt;\n\t\t} else {\n\t\t\tfp.mesh.rotation.x += fp.rotSpeed * dt;\n\t\t}\n\n\t\t// Fade out\n\t\tif (fp.mesh.material.transparent && fp.opacity !== undefined) {\n\t\t\tfp.opacity -= 0.008 * dt;\n\t\t\tfp.mesh.material.opacity = Math.max(0, fp.opacity);\n\t\t}\n\n\t\tif (fp.mesh.position.y < -20) {\n\t\t\tgameGroup.remove(fp.mesh);\n\t\t\tfp.mesh.geometry.dispose();\n\t\t\tfp.mesh.material.dispose();\n\t\t\tfallingPieces.splice(i, 1);\n\t\t}\n\t}\n\n\t// Smooth group descent - keep active block at consistent screen height\n\tgameGroup.position.y += (groupTargetY - gameGroup.position.y) * 0.07 * dt;\n\n\t// Gentle camera orbit at fixed height\n\tconst orbitAngle = now * 0.00008;\n\tconst orbitRadius = 8.5;\n\tconst fixedCamY = 8;\n\tcamera.position.y += (fixedCamY - camera.position.y) * 0.04 * dt;\n\tcamera.position.x += (Math.cos(orbitAngle) * orbitRadius - camera.position.x) * 0.02 * dt;\n\tcamera.position.z += (Math.sin(orbitAngle) * orbitRadius - camera.position.z) * 0.02 * dt;\n\n\t// Look at the top of the visible stack\n\tconst topWorldY = gameGroup.position.y + stack.length * BLOCK_HEIGHT;\n\tcamera.lookAt(0, topWorldY * 0.5 + 0.5, 0);\n\n\t// Update particles\n\tparticles.update();\n\n\trenderer.render(scene, camera);\n}\n\n// ===== EVENT HANDLING =====\nfunction onTap() {\n\tif (gameState === 'playing') {\n\t\tplaceBlock();\n\t}\n}\n\nfunction startGame() {\n\tsound.init();\n\tgameState = 'playing';\n\tstartScreen.classList.add('hidden');\n\tgameOverScreen.classList.add('hidden');\n\ttapHint.classList.remove('hidden');\n\tresetGame();\n}\n\ndocument.getElementById('start-btn').addEventListener('click', startGame);\ndocument.getElementById('restart-btn').addEventListener('click', startGame);\n\ncanvas.addEventListener('pointerdown', (e) => {\n\te.preventDefault();\n\tonTap();\n});\n\ndocument.addEventListener('keydown', (e) => {\n\tif (e.code === 'Space' || e.key === ' ') {\n\t\te.preventDefault();\n\t\tif (gameState === 'playing') onTap();\n\t\telse if (gameState === 'idle') startGame();\n\t\telse if (gameState === 'over' && !gameOverScreen.classList.contains('hidden')) startGame();\n\t}\n});\n\nwindow.addEventListener('resize', () => {\n\tif (!camera || !renderer) return;\n\tcamera.aspect = window.innerWidth / window.innerHeight;\n\tcamera.updateProjectionMatrix();\n\trenderer.setSize(window.innerWidth, window.innerHeight);\n});\n\n// ===== INIT =====\ninitScene();\n\nconst initialBase = createBlock(0, 0, 0, 3, 3, 0);\ngameGroup.add(initialBase);\n\nanimate();\nlucide.createIcons();"},{"name":"style.css","content":":root {\n\tcolor-scheme: dark;\n\t--bg: #1a1a2e;\n\t--surface: #16213e;\n\t--surface-2: #1a1a40;\n\t--text: #eef0ff;\n\t--text-muted: #8888aa;\n\t--accent: #e94560;\n\t--accent-glow: #ff6b81;\n\t--gold: #ffc947;\n\tfont-family: 'Syne', sans-serif;\n}\n\n* {\n\tbox-sizing: border-box;\n\tmargin: 0;\n\tpadding: 0;\n}\n\nhtml, body {\n\twidth: 100%;\n\theight: 100%;\n\toverflow: hidden;\n\tbackground: var(--bg);\n\tcolor: var(--text);\n\ttouch-action: manipulation;\n\t-webkit-tap-highlight-color: transparent;\n\tuser-select: none;\n}\n\n#game-container {\n\tposition: relative;\n\twidth: 100%;\n\theight: 100dvh;\n\toverflow: hidden;\n}\n\n#game-canvas {\n\tdisplay: block;\n\twidth: 100%;\n\theight: 100%;\n}\n\n#particle-canvas {\n\tposition: absolute;\n\tinset: 0;\n\twidth: 100%;\n\theight: 100%;\n\tpointer-events: none;\n\tz-index: 5;\n}\n\n#ui-overlay {\n\tposition: absolute;\n\tinset: 0;\n\tpointer-events: none;\n\tdisplay: flex;\n\tflex-direction: column;\n\talign-items: center;\n\tz-index: 10;\n}\n\n#score-panel {\n\tdisplay: flex;\n\tgap: 1.5rem;\n\tpadding: 1.25rem 1.5rem;\n\tmargin-top: env(safe-area-inset-top, 12px);\n}\n\n.score-block {\n\tdisplay: flex;\n\tflex-direction: column;\n\talign-items: center;\n\tbackground: rgba(22, 33, 62, 0.85);\n\tbackdrop-filter: blur(16px);\n\t-webkit-backdrop-filter: blur(16px);\n\tborder: 1px solid rgba(255, 255, 255, 0.08);\n\tborder-radius: 14px;\n\tpadding: 0.5rem 1.25rem;\n\tmin-width: 80px;\n\ttransition: transform 0.2s ease;\n}\n\n.score-block.score-bump {\n\tanimation: scoreBump 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);\n}\n\n@keyframes scoreBump {\n\t0% {\n\t\ttransform: scale(1);\n\t}\n\n\t50% {\n\t\ttransform: scale(1.15);\n\t}\n\n\t100% {\n\t\ttransform: scale(1);\n\t}\n}\n\n.score-label {\n\tfont-family: 'IBM Plex Mono', monospace;\n\tfont-size: 0.6rem;\n\tfont-weight: 600;\n\tletter-spacing: 0.15em;\n\tcolor: var(--text-muted);\n\ttext-transform: uppercase;\n}\n\n.score-value {\n\tfont-family: 'IBM Plex Mono', monospace;\n\tfont-size: 1.5rem;\n\tfont-weight: 700;\n\tcolor: var(--text);\n\tline-height: 1.2;\n}\n\n#start-screen {\n\tflex: 1;\n\tdisplay: flex;\n\tflex-direction: column;\n\talign-items: center;\n\tjustify-content: center;\n\tgap: 0.75rem;\n\tpointer-events: auto;\n}\n\n.game-title {\n\tfont-size: clamp(3.5rem, 12vw, 6rem);\n\tfont-weight: 800;\n\tletter-spacing: 0.08em;\n\tbackground: linear-gradient(135deg, var(--accent) 0%, var(--gold) 50%, #a78bfa 100%);\n\t-webkit-background-clip: text;\n\t-webkit-text-fill-color: transparent;\n\tbackground-clip: text;\n\tbackground-size: 200% 200%;\n\tline-height: 1;\n\ttext-shadow: none;\n\tanimation: titleShimmer 4s ease-in-out infinite;\n}\n\n@keyframes titleShimmer {\n\t0%, 100% {\n\t\tbackground-position: 0% 50%;\n\t\tfilter: brightness(1);\n\t}\n\n\t50% {\n\t\tbackground-position: 100% 50%;\n\t\tfilter: brightness(1.2);\n\t}\n}\n\n.game-subtitle {\n\tfont-family: 'IBM Plex Mono', monospace;\n\tfont-size: 0.85rem;\n\tcolor: var(--text-muted);\n\tletter-spacing: 0.1em;\n\tmargin-bottom: 1.5rem;\n}\n\n.btn-play {\n\tpointer-events: auto;\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 0.5rem;\n\tbackground: linear-gradient(135deg, var(--accent), #c23152);\n\tcolor: white;\n\tborder: none;\n\tborder-radius: 50px;\n\tpadding: 0.85rem 2rem;\n\tfont-family: 'Syne', sans-serif;\n\tfont-size: 0.95rem;\n\tfont-weight: 700;\n\tletter-spacing: 0.12em;\n\tcursor: pointer;\n\ttransition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);\n\tbox-shadow: 0 4px 24px rgba(233, 69, 96, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.15);\n}\n\n.btn-play:hover {\n\ttransform: translateY(-3px) scale(1.03);\n\tbox-shadow: 0 8px 40px rgba(233, 69, 96, 0.55), inset 0 1px 0 rgba(255, 255, 255, 0.15);\n}\n\n.btn-play:active {\n\ttransform: translateY(0) scale(0.98);\n}\n\n.btn-play__icon {\n\twidth: 18px;\n\theight: 18px;\n}\n\n#game-over-screen {\n\tposition: absolute;\n\tinset: 0;\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\tpointer-events: auto;\n\tbackground: rgba(26, 26, 46, 0.7);\n\tbackdrop-filter: blur(12px);\n\t-webkit-backdrop-filter: blur(12px);\n\tanimation: fadeIn 0.5s ease;\n}\n\n.game-over-card {\n\tdisplay: flex;\n\tflex-direction: column;\n\talign-items: center;\n\tgap: 1rem;\n\tbackground: linear-gradient(145deg, var(--surface), var(--surface-2));\n\tborder: 1px solid rgba(255, 255, 255, 0.1);\n\tborder-radius: 24px;\n\tpadding: 2.5rem 3rem;\n\tbox-shadow: 0 24px 80px rgba(0, 0, 0, 0.6), 0 0 60px rgba(233, 69, 96, 0.08);\n\tanimation: cardSlideUp 0.5s cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n@keyframes cardSlideUp {\n\tfrom {\n\t\topacity: 0;\n\t\ttransform: translateY(40px) scale(0.92);\n\t}\n\n\tto {\n\t\topacity: 1;\n\t\ttransform: translateY(0) scale(1);\n\t}\n}\n\n@keyframes fadeIn {\n\tfrom {\n\t\topacity: 0;\n\t}\n\n\tto {\n\t\topacity: 1;\n\t}\n}\n\n.game-over-title {\n\tfont-size: 1.5rem;\n\tfont-weight: 800;\n\tletter-spacing: 0.1em;\n\tcolor: var(--text-muted);\n}\n\n.final-score-display {\n\tdisplay: flex;\n\tflex-direction: column;\n\talign-items: center;\n}\n\n.final-score-label {\n\tfont-family: 'IBM Plex Mono', monospace;\n\tfont-size: 0.6rem;\n\tletter-spacing: 0.15em;\n\tcolor: var(--text-muted);\n}\n\n.final-score-num {\n\tfont-family: 'IBM Plex Mono', monospace;\n\tfont-size: 3.5rem;\n\tfont-weight: 700;\n\tline-height: 1.1;\n\tbackground: linear-gradient(135deg, var(--accent), var(--gold));\n\t-webkit-background-clip: text;\n\t-webkit-text-fill-color: transparent;\n\tbackground-clip: text;\n}\n\n.new-best {\n\tfont-size: 0.85rem;\n\tfont-weight: 700;\n\tletter-spacing: 0.12em;\n\tcolor: var(--gold);\n\tanimation: bestPulse 1s ease-in-out infinite;\n}\n\n@keyframes bestPulse {\n\t0%, 100% {\n\t\topacity: 1;\n\t}\n\n\t50% {\n\t\topacity: 0.5;\n\t}\n}\n\n#tap-hint {\n\tposition: absolute;\n\tbottom: max(2rem, env(safe-area-inset-bottom, 1rem));\n\tleft: 50%;\n\ttransform: translateX(-50%);\n\tfont-family: 'IBM Plex Mono', monospace;\n\tfont-size: 0.7rem;\n\tletter-spacing: 0.2em;\n\tcolor: var(--text-muted);\n\tanimation: hintBlink 2s ease-in-out infinite;\n}\n\n@keyframes hintBlink {\n\t0%, 100% {\n\t\topacity: 0.3;\n\t}\n\n\t50% {\n\t\topacity: 0.9;\n\t}\n}\n\n#combo-display {\n\tposition: absolute;\n\ttop: 50%;\n\tleft: 50%;\n\ttransform: translate(-50%, -50%);\n\tfont-size: 2.2rem;\n\tfont-weight: 800;\n\tletter-spacing: 0.08em;\n\tpointer-events: none;\n\tz-index: 20;\n}\n\n.combo-animate {\n\tanimation: comboFlash 0.8s cubic-bezier(0.16, 1, 0.3, 1) forwards;\n}\n\n@keyframes comboFlash {\n\t0% {\n\t\topacity: 0;\n\t\ttransform: translate(-50%, -50%) scale(0.3);\n\t}\n\n\t20% {\n\t\topacity: 1;\n\t\ttransform: translate(-50%, -50%) scale(1.3);\n\t}\n\n\t100% {\n\t\topacity: 0;\n\t\ttransform: translate(-50%, -65%) scale(1);\n\t}\n}\n\n.hidden {\n\tdisplay: none !important;\n}\n\n@media (max-width: 480px) {\n\t.game-over-card {\n\t\tpadding: 2rem 2rem;\n\t\tmargin: 0 1rem;\n\t}\n}"}],"folders":[]},"variants":null,"createdAt":"2026-03-01T01:17:35.439Z","updatedAt":"2026-03-01T03:35:12.525Z"}}