{"project":{"id":"dnzLHrg","userId":"davidyarham@gmail.com","username":null,"userPicture":null,"name":"BlockCraft","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>Untitled</title>\n  <link rel=\"stylesheet\" href=\"style.css\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover\" />\n<script src=\"https://unpkg.com/three@0.160.0/build/three.min.js\"></script>\n</head>\n<body>\n<div id=\"game\">\n\t<div id=\"overlay\">\n\t\t<div class=\"overlay__panel\">\n\t\t\t<div class=\"overlay__eyebrow\">Voxel sandbox experience</div>\n\t\t\t<h1>BlockCraft</h1>\n\t\t\t<p class=\"overlay__subtitle\">Sharper visuals, smoother atmosphere, desktop and mobile friendly</p>\n\t\t\t<div class=\"overlay__stats\" aria-hidden=\"true\">\n\t\t\t\t<div class=\"overlay__stat\">\n\t\t\t\t\t<strong>Fast</strong>\n\t\t\t\t\t<span>Performance-first rendering</span>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"overlay__stat\">\n\t\t\t\t\t<strong>Build</strong>\n\t\t\t\t\t<span>Place and break block worlds</span>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"overlay__stat\">\n\t\t\t\t\t<strong>Save</strong>\n\t\t\t\t\t<span>Local world persistence</span>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<ul>\n\t\t\t\t<li>Move: WASD or left joystick</li>\n\t\t\t\t<li>Look: mouse or drag right side</li>\n\t\t\t\t<li>Jump: Space or jump button</li>\n\t\t\t\t<li>Break block: Left click or Break button</li>\n\t\t\t\t<li>Place block: Right click or Place button</li>\n\t\t\t\t<li>Hotbar: 1-6 or tap slots</li>\n\t\t\t\t<li>Save: K</li>\n\t\t\t\t<li>Load: L</li>\n\t\t\t</ul>\n\t\t\t<div class=\"overlay__actions\">\n\t\t\t\t<button id=\"startBtn\">Start Game</button>\n\t\t\t\t<p class=\"overlay__hint\">Best on desktop with mouse lock · mobile uses touch zones</p>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n\n\t<div id=\"hud\" aria-hidden=\"true\">\n\t\t<div id=\"crosshair\"></div>\n\t\t<div id=\"hudTop\">\n\n\t\t\t<div id=\"saveState\">World: unsaved</div>\n\t\t</div>\n\n\t\t<div id=\"mobileTopActions\">\n\t\t\t<button class=\"mobile-btn\" id=\"pauseBtn\" aria-label=\"Pause game\">Pause</button>\n\t\t\t<button class=\"mobile-btn\" id=\"saveBtn\" aria-label=\"Save world\">Save</button>\n\t\t\t<button class=\"mobile-btn\" id=\"loadBtn\" aria-label=\"Load world\">Load</button>\n\t\t</div>\n\n\t\t<div id=\"hotbarWrap\">\n\t\t\t<div id=\"hotbar\">\n\t\t\t\t<button class=\"slot active slot--grass\" data-type=\"grass\" aria-label=\"Grass block\">\n\t\t\t\t\t<span class=\"slotKey\">1</span>\n\t\t\t\t\t<span class=\"slotPreview\" aria-hidden=\"true\"></span>\n\t\t\t\t\t<span class=\"slotLabel\">Grass</span>\n\t\t\t\t</button>\n\t\t\t\t<button class=\"slot slot--dirt\" data-type=\"dirt\" aria-label=\"Dirt block\">\n\t\t\t\t\t<span class=\"slotKey\">2</span>\n\t\t\t\t\t<span class=\"slotPreview\" aria-hidden=\"true\"></span>\n\t\t\t\t\t<span class=\"slotLabel\">Dirt</span>\n\t\t\t\t</button>\n\t\t\t\t<button class=\"slot slot--stone\" data-type=\"stone\" aria-label=\"Stone block\">\n\t\t\t\t\t<span class=\"slotKey\">3</span>\n\t\t\t\t\t<span class=\"slotPreview\" aria-hidden=\"true\"></span>\n\t\t\t\t\t<span class=\"slotLabel\">Stone</span>\n\t\t\t\t</button>\n\t\t\t\t<button class=\"slot slot--wood\" data-type=\"wood\" aria-label=\"Wood block\">\n\t\t\t\t\t<span class=\"slotKey\">4</span>\n\t\t\t\t\t<span class=\"slotPreview\" aria-hidden=\"true\"></span>\n\t\t\t\t\t<span class=\"slotLabel\">Wood</span>\n\t\t\t\t</button>\n\t\t\t\t<button class=\"slot slot--leaves\" data-type=\"leaves\" aria-label=\"Leaves block\">\n\t\t\t\t\t<span class=\"slotKey\">5</span>\n\t\t\t\t\t<span class=\"slotPreview\" aria-hidden=\"true\"></span>\n\t\t\t\t\t<span class=\"slotLabel\">Leaves</span>\n\t\t\t\t</button>\n\t\t\t\t<button class=\"slot slot--water\" data-type=\"water\" aria-label=\"Water block\">\n\t\t\t\t\t<span class=\"slotKey\">6</span>\n\t\t\t\t\t<span class=\"slotPreview\" aria-hidden=\"true\"></span>\n\t\t\t\t\t<span class=\"slotLabel\">Water</span>\n\t\t\t\t</button>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<div id=\"mobileControls\">\n\t\t\t<div id=\"joystickZone\" aria-label=\"Movement joystick\">\n\t\t\t\t<div id=\"joystickBase\">\n\t\t\t\t\t<div id=\"joystickKnob\"></div>\n\t\t\t\t</div>\n\t\t\t</div>\n\n\t\t\t<div id=\"mobileActions\">\n\t\t\t\t<div class=\"mobileActions__cluster mobileActions__cluster--top\">\n\t\t\t\t\t<button class=\"mobile-action mobile-action--wide\" id=\"breakBtn\" aria-label=\"Break selected block\">Break</button>\n\t\t\t\t\t<button class=\"mobile-action mobile-action--wide\" id=\"placeBtn\" aria-label=\"Place selected block\">Place</button>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"mobileActions__cluster mobileActions__cluster--bottom\">\n\t\t\t\t\t<button class=\"mobile-action\" id=\"jumpBtn\" aria-label=\"Jump\">Jump</button>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<div id=\"lookZone\" aria-hidden=\"true\"></div>\n\t</div>\n</div>\n  <script type=\"module\" src=\"main.js\"></script>\n</body>\n</html>"},{"name":"main.js","content":"const scene = new THREE.Scene();\nscene.background = new THREE.Color(0x8fcfff);\nscene.fog = new THREE.Fog(0x8fcfff, 55, 210);\n\nconst isTouchDevice = matchMedia('(pointer: coarse)').matches || 'ontouchstart' in window || navigator.maxTouchPoints > 0;\ndocument.body.classList.add(isTouchDevice ? 'is-mobile' : 'is-desktop');\n\nconst camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1200);\nconst renderer = new THREE.WebGLRenderer({\n\tantialias: !isTouchDevice,\n\tpowerPreference: 'high-performance'\n});\nrenderer.setSize(window.innerWidth, window.innerHeight);\nrenderer.setPixelRatio(Math.min(window.devicePixelRatio, isTouchDevice ? 1.5 : 1.75));\nrenderer.shadowMap.enabled = false;\nrenderer.shadowMap.type = THREE.PCFSoftShadowMap;\nrenderer.outputColorSpace = THREE.SRGBColorSpace;\nrenderer.toneMapping = THREE.ACESFilmicToneMapping;\nrenderer.toneMappingExposure = 1.12;\ndocument.getElementById('game').prepend(renderer.domElement);\nrenderer.domElement.tabIndex = 0;\nrenderer.domElement.setAttribute('aria-label', 'BlockCraft game canvas');\nrenderer.domElement.style.outline = 'none';\n\nconst overlay = document.getElementById('overlay');\nconst startBtn = document.getElementById('startBtn');\nconst saveStateEl = document.getElementById('saveState');\nconst slots = [...document.querySelectorAll('.slot')];\nconst pauseBtn = document.getElementById('pauseBtn');\nconst saveBtn = document.getElementById('saveBtn');\nconst loadBtn = document.getElementById('loadBtn');\nconst breakBtn = document.getElementById('breakBtn');\nconst placeBtn = document.getElementById('placeBtn');\nconst jumpBtn = document.getElementById('jumpBtn');\nconst joystickZone = document.getElementById('joystickZone');\nconst joystickBase = document.getElementById('joystickBase');\nconst joystickKnob = document.getElementById('joystickKnob');\nconst lookZone = document.getElementById('lookZone');\n\nconst world = new Map();\nconst blockSize = 1;\nconst worldRadius = isTouchDevice ? 24 : 32;\nconst maxHeight = 10;\nconst waterLevel = 4;\nconst playerHeight = 1.7;\nconst playerRadius = 0.35;\nconst gravity = 20;\nconst saveKey = 'blockcraft-save-v5-sandbox';\nconst renderDistance = isTouchDevice ? 24 : 34;\nconst renderDistanceSq = renderDistance * renderDistance;\nlet visibleBlocks = [];\nlet visibilityUpdateTimer = 0;\nlet visibleCollidableBlocks = [];\n\nlet selectedBlockType = 'grass';\nlet gameActive = false;\nlet noclip = false;\nlet highlightMesh = null;\nlet audioStarted = false;\n\nconst raycaster = new THREE.Raycaster();\nconst pointer = new THREE.Vector2(0, 0);\nconst keys = new Set();\nconst collidableTypes = new Set(['grass', 'dirt', 'stone', 'wood', 'leaves', 'sand']);\n\nconst touchState = {\n\tjoystickId: null,\n\tlookId: null,\n\tjoystickVector: {\n\t\tx: 0,\n\t\ty: 0\n\t},\n\tjumpPressed: false,\n\tlookActive: false,\n\tlastLookX: 0,\n\tlastLookY: 0\n};\n\nconst listener = new THREE.AudioListener();\ncamera.add(listener);\nlet audioCtx = null;\nlet ambientGain = null;\n\nconst player = {\n\tposition: new THREE.Vector3(0, 12, 0),\n\tvelocity: new THREE.Vector3(),\n\tdirection: new THREE.Vector3(),\n\tonGround: false,\n\tyaw: 0,\n\tpitch: 0,\n\tspeed: isTouchDevice ? 5.6 : 6.5,\n\tprevYVelocity: 0\n};\n\nfunction setSaveState(text) {\n\tsaveStateEl.textContent = `World: ${text}`;\n}\n\nfunction initAudio() {\n\tif (audioStarted) return;\n\taudioCtx = listener.context;\n\tambientGain = audioCtx.createGain();\n\tambientGain.gain.value = 0.02;\n\tambientGain.connect(audioCtx.destination);\n\n\tconst noiseBuffer = audioCtx.createBuffer(1, audioCtx.sampleRate * 2, audioCtx.sampleRate);\n\tconst data = noiseBuffer.getChannelData(0);\n\tfor (let i = 0; i < data.length; i++) data[i] = (Math.random() * 2 - 1) * 0.35;\n\n\tconst source = audioCtx.createBufferSource();\n\tsource.buffer = noiseBuffer;\n\tsource.loop = true;\n\n\tconst lowpass = audioCtx.createBiquadFilter();\n\tlowpass.type = 'lowpass';\n\tlowpass.frequency.value = 400;\n\n\tsource.connect(lowpass);\n\tlowpass.connect(ambientGain);\n\tsource.start();\n\taudioStarted = true;\n}\n\nfunction playTone(freq = 220, duration = 0.08, type = 'square', volume = 0.03, ramp = 0.01) {\n\tif (!audioCtx) return;\n\tconst now = audioCtx.currentTime;\n\tconst osc = audioCtx.createOscillator();\n\tconst gain = audioCtx.createGain();\n\tosc.type = type;\n\tosc.frequency.setValueAtTime(freq, now);\n\tgain.gain.setValueAtTime(0.0001, now);\n\tgain.gain.exponentialRampToValueAtTime(volume, now + 0.005);\n\tgain.gain.exponentialRampToValueAtTime(0.0001, now + duration + ramp);\n\tosc.connect(gain);\n\tgain.connect(audioCtx.destination);\n\tosc.start(now);\n\tosc.stop(now + duration + ramp + 0.02);\n}\n\nfunction playStepSound() {\n\tplayTone(90 + Math.random() * 20, 0.04, 'triangle', 0.012);\n}\n\nfunction playBreakSound(type) {\n\tconst base = type === 'stone' ? 140 : type === 'wood' ? 180 : type === 'sand' ? 170 : 220;\n\tplayTone(base, 0.05, 'square', 0.02);\n\tplayTone(base * 0.7, 0.06, 'triangle', 0.015);\n}\n\nfunction playPlaceSound(type) {\n\tconst base = type === 'stone' ? 160 : type === 'wood' ? 200 : type === 'sand' ? 190 : 260;\n\tplayTone(base, 0.035, 'sawtooth', 0.016);\n}\n\nfunction playJumpSound() {\n\tplayTone(280, 0.06, 'triangle', 0.02);\n}\n\nslots.forEach((slot, index) => {\n\tslot.setAttribute('tabindex', '-1');\n\tslot.addEventListener('click', () => {\n\t\tselectBlock(slot.dataset.type);\n\t\tfocusGame();\n\t});\n\tslot.dataset.index = String(index + 1);\n});\n\nfunction selectBlock(type) {\n\tselectedBlockType = type;\n\tslots.forEach((slot) => slot.classList.toggle('active', slot.dataset.type === type));\n}\nselectBlock('grass');\nsetSaveState('unsaved');\n\nfunction textureCanvas(colors) {\n\tconst canvas = document.createElement('canvas');\n\tcanvas.width = 64;\n\tcanvas.height = 64;\n\tconst ctx = canvas.getContext('2d');\n\tctx.fillStyle = colors.base;\n\tctx.fillRect(0, 0, 64, 64);\n\tfor (let i = 0; i < 380; i++) {\n\t\tctx.fillStyle = i % 4 === 0 ? colors.dark : colors.light;\n\t\tconst x = Math.random() * 64;\n\t\tconst y = Math.random() * 64;\n\t\tconst s = 1 + Math.random() * 4;\n\t\tctx.fillRect(x, y, s, s);\n\t}\n\tif (colors.lines) {\n\t\tctx.strokeStyle = colors.lines;\n\t\tctx.globalAlpha = 0.2;\n\t\tfor (let i = 0; i < 8; i++) {\n\t\t\tctx.beginPath();\n\t\t\tctx.moveTo(0, i * 8 + Math.random() * 3);\n\t\t\tctx.lineTo(64, i * 8 + Math.random() * 3);\n\t\t\tctx.stroke();\n\t\t}\n\t\tctx.globalAlpha = 1;\n\t}\n\tconst texture = new THREE.CanvasTexture(canvas);\n\ttexture.magFilter = THREE.NearestFilter;\n\ttexture.minFilter = THREE.NearestFilter;\n\ttexture.wrapS = THREE.RepeatWrapping;\n\ttexture.wrapT = THREE.RepeatWrapping;\n\ttexture.colorSpace = THREE.SRGBColorSpace;\n\treturn texture;\n}\n\nconst grassTop = textureCanvas({\n\tbase: '#6cbe3b',\n\tdark: '#4f8d29',\n\tlight: '#8bd45d'\n});\nconst grassSide = textureCanvas({\n\tbase: '#7a5a3a',\n\tdark: '#5b4128',\n\tlight: '#8a6f4e'\n});\nconst dirtTex = textureCanvas({\n\tbase: '#7a5a3a',\n\tdark: '#5b4128',\n\tlight: '#93724f'\n});\nconst stoneTex = textureCanvas({\n\tbase: '#8c8f98',\n\tdark: '#6b6e75',\n\tlight: '#b4b7bd'\n});\nconst woodTex = textureCanvas({\n\tbase: '#8b5a2b',\n\tdark: '#6d431d',\n\tlight: '#a96f3a',\n\tlines: '#5f3818'\n});\nconst leavesTex = textureCanvas({\n\tbase: '#2f8f3a',\n\tdark: '#236e2c',\n\tlight: '#46b853'\n});\nconst sandTex = textureCanvas({\n\tbase: '#bfa56f',\n\tdark: '#9d8456',\n\tlight: '#dbc48b'\n});\nconst waterTex = textureCanvas({\n\tbase: '#3b78c8',\n\tdark: '#275ba0',\n\tlight: '#5a98eb'\n});\n\nconst materials = {\n\tsand: new THREE.MeshLambertMaterial({\n\t\tmap: sandTex\n\t}),\n\tgrass: [\n\t\tnew THREE.MeshLambertMaterial({\n\t\t\tmap: grassSide\n\t\t}),\n\t\tnew THREE.MeshLambertMaterial({\n\t\t\tmap: grassSide\n\t\t}),\n\t\tnew THREE.MeshLambertMaterial({\n\t\t\tmap: grassTop\n\t\t}),\n\t\tnew THREE.MeshLambertMaterial({\n\t\t\tmap: dirtTex\n\t\t}),\n\t\tnew THREE.MeshLambertMaterial({\n\t\t\tmap: grassSide\n\t\t}),\n\t\tnew THREE.MeshLambertMaterial({\n\t\t\tmap: grassSide\n\t\t})\n\t],\n\tdirt: new THREE.MeshLambertMaterial({\n\t\tmap: dirtTex\n\t}),\n\tstone: new THREE.MeshLambertMaterial({\n\t\tmap: stoneTex\n\t}),\n\twood: new THREE.MeshLambertMaterial({\n\t\tmap: woodTex\n\t}),\n\tleaves: new THREE.MeshLambertMaterial({\n\t\tmap: leavesTex,\n\t\ttransparent: true,\n\t\talphaTest: 0.18\n\t}),\n\twater: new THREE.MeshLambertMaterial({\n\t\tmap: waterTex,\n\t\ttransparent: true,\n\t\topacity: 0.78,\n\t\tdepthWrite: false,\n\t\tcolor: new THREE.Color(0x9ed6ff)\n\t})\n};\n\nconst cubeGeo = new THREE.BoxGeometry(blockSize, blockSize, blockSize);\nconst outlineGeo = new THREE.EdgesGeometry(cubeGeo);\nconst outlineMat = new THREE.LineBasicMaterial({\n\tcolor: 0xffffff\n});\nhighlightMesh = new THREE.LineSegments(outlineGeo, outlineMat);\nhighlightMesh.visible = false;\nscene.add(highlightMesh);\n\nconst skyGeo = new THREE.SphereGeometry(500, isTouchDevice ? 18 : 24, isTouchDevice ? 18 : 24);\nconst skyMat = new THREE.ShaderMaterial({\n\tside: THREE.BackSide,\n\tuniforms: {\n\t\ttopColor: {\n\t\t\tvalue: new THREE.Color(0x74b8ff)\n\t\t},\n\t\tbottomColor: {\n\t\t\tvalue: new THREE.Color(0xeaf8ff)\n\t\t},\n\t\toffset: {\n\t\t\tvalue: 20\n\t\t},\n\t\texponent: {\n\t\t\tvalue: 0.8\n\t\t}\n\t},\n\tvertexShader: `\n\t\tvarying vec3 vWorldPosition;\n\t\tvoid main() {\n\t\t\tvec4 worldPosition = modelMatrix * vec4(position, 1.0);\n\t\t\tvWorldPosition = worldPosition.xyz;\n\t\t\tgl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n\t\t}\n\t`,\n\tfragmentShader: `\n\t\tuniform vec3 topColor;\n\t\tuniform vec3 bottomColor;\n\t\tuniform float offset;\n\t\tuniform float exponent;\n\t\tvarying vec3 vWorldPosition;\n\t\tvoid main() {\n\t\t\tfloat h = normalize(vWorldPosition + offset).y;\n\t\t\tfloat mixFactor = max(pow(max(h, 0.0), exponent), 0.0);\n\t\t\tgl_FragColor = vec4(mix(bottomColor, topColor, mixFactor), 1.0);\n\t\t}\n\t`\n});\nconst sky = new THREE.Mesh(skyGeo, skyMat);\nscene.add(sky);\n\nfunction key(x, y, z) {\n\treturn `${x},${y},${z}`;\n}\n\nfunction isCollidableType(type) {\n\treturn collidableTypes.has(type);\n}\n\nfunction hasBlock(x, y, z) {\n\treturn world.has(key(x, y, z));\n}\n\nfunction getBlock(x, y, z) {\n\treturn world.get(key(x, y, z)) || null;\n}\n\nfunction addBlock(x, y, z, type = 'grass') {\n\tconst k = key(x, y, z);\n\tif (world.has(k)) return null;\n\tconst mesh = new THREE.Mesh(cubeGeo, materials[type] || materials.grass);\n\tmesh.position.set(x + 0.5, y + 0.5, z + 0.5);\n\tmesh.receiveShadow = true;\n\tmesh.castShadow = type !== 'water';\n\tmesh.userData = {\n\t\tx,\n\t\ty,\n\t\tz,\n\t\ttype\n\t};\n\tworld.set(k, mesh);\n\tscene.add(mesh);\n\treturn mesh;\n}\n\nfunction removeBlock(x, y, z) {\n\tconst k = key(x, y, z);\n\tconst mesh = world.get(k);\n\tif (!mesh) return;\n\tscene.remove(mesh);\n\tworld.delete(k);\n}\n\nfunction getGroundHeight(x, z) {\n\tconst n1 = Math.sin(x * 0.11) * 2.6;\n\tconst n2 = Math.cos(z * 0.12) * 2.1;\n\tconst n3 = Math.sin((x + z) * 0.05) * 2.8;\n\tconst n4 = Math.cos((x - z) * 0.035) * 3.0;\n\tconst ridge = Math.sin(x * 0.025) * Math.cos(z * 0.025) * 5.0;\n\treturn Math.max(2, Math.floor(maxHeight + n1 + n2 + n3 + n4 + ridge));\n}\n\nconst WORLD_GEN_LOOP_LIMIT = Infinity;\nlet worldGenLoopCount = 0;\n\nfunction maybeAddTree(x, h, z) {\n\tif (h <= waterLevel + 1 || Math.random() > 0.065) return;\n\tconst trunk = 3 + Math.floor(Math.random() * 4);\n\tfor (let i = 0; i < trunk; i++) {\n\t\tif (++worldGenLoopCount > WORLD_GEN_LOOP_LIMIT) return;\n\t\taddBlock(x, h + i, z, 'wood');\n\t}\n\tfor (let lx = -2; lx <= 2; lx++) {\n\t\tif (++worldGenLoopCount > WORLD_GEN_LOOP_LIMIT) return;\n\t\tfor (let lz = -2; lz <= 2; lz++) {\n\t\t\tif (++worldGenLoopCount > WORLD_GEN_LOOP_LIMIT) return;\n\t\t\tfor (let ly = trunk - 1; ly <= trunk + 2; ly++) {\n\t\t\t\tif (++worldGenLoopCount > WORLD_GEN_LOOP_LIMIT) return;\n\t\t\t\tconst dist = Math.abs(lx) + Math.abs(lz) + Math.abs(ly - trunk);\n\t\t\t\tif (dist <= 4 && !hasBlock(x + lx, h + ly, z + lz)) addBlock(x + lx, h + ly, z + lz, 'leaves');\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunction buildWorld() {\n\tworldGenLoopCount = 0;\n\tfor (let x = -worldRadius; x <= worldRadius; x++) {\n\t\tif (worldGenLoopCount > WORLD_GEN_LOOP_LIMIT) break;\n\t\tfor (let z = -worldRadius; z <= worldRadius; z++) {\n\t\t\tif (++worldGenLoopCount > WORLD_GEN_LOOP_LIMIT) break;\n\t\t\tconst h = getGroundHeight(x, z);\n\t\t\tfor (let y = 0; y < h; y++) {\n\t\t\t\tif (++worldGenLoopCount > WORLD_GEN_LOOP_LIMIT) break;\n\t\t\t\tif (y === h - 1) addBlock(x, y, z, h <= waterLevel ? 'sand' : 'grass');\n\t\t\t\telse if (y > h - 4) addBlock(x, y, z, 'dirt');\n\t\t\t\telse addBlock(x, y, z, 'stone');\n\t\t\t}\n\t\t\tfor (let y = h; y < waterLevel; y++) {\n\t\t\t\tif (++worldGenLoopCount > WORLD_GEN_LOOP_LIMIT) break;\n\t\t\t\tif (!hasBlock(x, y, z)) addBlock(x, y, z, 'water');\n\t\t\t}\n\t\t\tmaybeAddTree(x, h, z);\n\t\t}\n\t}\n}\n\nfunction updateChunkVisibility(force = false) {\n\tvisibilityUpdateTimer -= force ? 999 : 1 / 60;\n\tif (!force && visibilityUpdateTimer > 0) return;\n\tvisibilityUpdateTimer = isTouchDevice ? 0.25 : 0.2;\n\tvisibleBlocks.length = 0;\n\tvisibleCollidableBlocks.length = 0;\n\tconst px = player.position.x;\n\tconst pz = player.position.z;\n\tfor (const mesh of world.values()) {\n\t\tconst dx = mesh.position.x - px;\n\t\tconst dz = mesh.position.z - pz;\n\t\tmesh.visible = dx * dx + dz * dz < renderDistanceSq;\n\t\tif (mesh.visible) {\n\t\t\tvisibleBlocks.push(mesh);\n\t\t\tif (isCollidableType(mesh.userData.type)) visibleCollidableBlocks.push(mesh);\n\t\t}\n\t}\n}\n\nfunction collidesAt(x, y, z) {\n\tconst minX = Math.floor(x - playerRadius);\n\tconst maxX = Math.floor(x + playerRadius);\n\tconst minY = Math.floor(y - playerHeight);\n\tconst maxY = Math.floor(y - 0.001);\n\tconst minZ = Math.floor(z - playerRadius);\n\tconst maxZ = Math.floor(z + playerRadius);\n\n\tfor (let bx = minX; bx <= maxX; bx++) {\n\t\tfor (let by = minY; by <= maxY; by++) {\n\t\t\tfor (let bz = minZ; bz <= maxZ; bz++) {\n\t\t\t\tconst block = getBlock(bx, by, bz);\n\t\t\t\tif (block && isCollidableType(block.userData.type)) return true;\n\t\t\t}\n\t\t}\n\t}\n\treturn false;\n}\n\nfunction findSpawn() {\n\tconst h = getGroundHeight(0, 0);\n\tlet spawnY = h + playerHeight + 2.5;\n\tfor (let i = 0; i < 64 && collidesAt(0.5, spawnY, 0.5); i++) spawnY += 1;\n\tplayer.position.set(0.5, spawnY, 0.5);\n}\n\nbuildWorld();\nif (worldGenLoopCount > WORLD_GEN_LOOP_LIMIT) {\n\tconsole.warn('World generation was capped to avoid loop protection.');\n}\nfindSpawn();\nupdateChunkVisibility(true);\n\nconst ambient = new THREE.HemisphereLight(0xeaf7ff, 0x78915a, 1.02);\nscene.add(ambient);\n\nconst sun = new THREE.DirectionalLight(0xfff1cf, 1.22);\nsun.position.set(20, 35, 15);\nsun.castShadow = true;\nsun.shadow.mapSize.width = 512;\nsun.shadow.mapSize.height = 512;\nsun.shadow.camera.near = 1;\nsun.shadow.camera.far = 90;\nsun.shadow.camera.left = -28;\nsun.shadow.camera.right = 28;\nsun.shadow.camera.top = 28;\nsun.shadow.camera.bottom = -28;\nscene.add(sun);\nscene.add(sun.target);\n\nfunction focusGame() {\n\trenderer.domElement.focus();\n\twindow.focus();\n\tgameActive = true;\n\tinitAudio();\n\toverlay.classList.add('hidden');\n}\n\nfunction pauseGame() {\n\tgameActive = false;\n\toverlay.classList.remove('hidden');\n}\n\nfunction lockPointer() {\n\tfocusGame();\n\tif (!isTouchDevice && renderer.domElement.requestPointerLock) renderer.domElement.requestPointerLock();\n}\n\nstartBtn.addEventListener('click', () => {\n\tlockPointer();\n});\n\npauseBtn.addEventListener('click', () => {\n\tpauseGame();\n\tif (document.pointerLockElement === renderer.domElement) document.exitPointerLock();\n});\n\nsaveBtn.addEventListener('click', () => {\n\tfocusGame();\n\tsaveWorld();\n});\n\nloadBtn.addEventListener('click', () => {\n\tfocusGame();\n\tloadWorld();\n});\n\nrenderer.domElement.addEventListener('click', () => {\n\tif (isTouchDevice) return;\n\tfocusGame();\n\tif (document.pointerLockElement !== renderer.domElement) lockPointer();\n});\n\ndocument.addEventListener('pointerlockchange', () => {\n\tif (isTouchDevice) return;\n\tconst locked = document.pointerLockElement === renderer.domElement;\n\toverlay.classList.toggle('hidden', locked || gameActive);\n});\n\ndocument.addEventListener('mousemove', (e) => {\n\tif (document.pointerLockElement !== renderer.domElement) return;\n\tplayer.yaw -= e.movementX * 0.0022;\n\tplayer.pitch -= e.movementY * 0.0022;\n\tplayer.pitch = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, player.pitch));\n});\n\nwindow.addEventListener('keydown', (e) => {\n\tgameActive = true;\n\tkeys.add(e.code);\n\tif (['Space', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.code)) e.preventDefault();\n\tif (e.code === 'KeyN') {\n\t\tnoclip = !noclip;\n\t\tsetSaveState(noclip ? 'noclip on' : 'noclip off');\n\t}\n\tif (/Digit[1-9]/.test(e.code)) {\n\t\tconst index = Number(e.code.replace('Digit', '')) - 1;\n\t\tif (slots[index]) selectBlock(slots[index].dataset.type);\n\t}\n\tif (e.code === 'KeyK') saveWorld();\n\tif (e.code === 'KeyL') loadWorld();\n});\n\nwindow.addEventListener('keyup', (e) => {\n\tkeys.delete(e.code);\n});\n\nwindow.addEventListener('blur', () => {\n\tkeys.clear();\n\ttouchState.joystickVector.x = 0;\n\ttouchState.joystickVector.y = 0;\n\ttouchState.jumpPressed = false;\n\tresetJoystick();\n\tpauseGame();\n});\n\ndocument.addEventListener('contextmenu', (e) => e.preventDefault());\n\nfunction getTargetBlock() {\n\traycaster.setFromCamera(pointer, camera);\n\tconst intersects = raycaster.intersectObjects(visibleCollidableBlocks, false);\n\tif (!intersects.length) return null;\n\tconst block = intersects.find((i) => i.distance <= 8);\n\tif (!block) return null;\n\tconst place = block.point.clone().add(block.face.normal.clone().multiplyScalar(0.5));\n\treturn {\n\t\tblock,\n\t\tplace\n\t};\n}\n\nfunction breakTargetBlock() {\n\tconst hit = getTargetBlock();\n\tif (!hit) return;\n\tconst {\n\t\tx,\n\t\ty,\n\t\tz,\n\t\ttype\n\t} = hit.block.object.userData;\n\tif (type !== 'water') {\n\t\tremoveBlock(x, y, z);\n\t\tplayBreakSound(type);\n\t}\n}\n\nfunction placeTargetBlock() {\n\tconst hit = getTargetBlock();\n\tif (!hit) return;\n\tconst p = hit.place;\n\tconst px = Math.floor(p.x);\n\tconst py = Math.floor(p.y);\n\tconst pz = Math.floor(p.z);\n\tif (hasBlock(px, py, pz)) return;\n\n\tconst feetY = player.position.y - playerHeight;\n\tconst headY = player.position.y;\n\tconst intersectsPlayer =\n\t\tpx + 1 > player.position.x - playerRadius &&\n\t\tpx < player.position.x + playerRadius &&\n\t\tpy + 1 > feetY &&\n\t\tpy < headY &&\n\t\tpz + 1 > player.position.z - playerRadius &&\n\t\tpz < player.position.z + playerRadius;\n\n\tif (!intersectsPlayer) {\n\t\taddBlock(px, py, pz, selectedBlockType);\n\t\tplayPlaceSound(selectedBlockType);\n\t}\n}\n\ndocument.addEventListener('mousedown', (e) => {\n\tif (!gameActive || isTouchDevice) return;\n\tif (e.button === 0) breakTargetBlock();\n\tif (e.button === 2) placeTargetBlock();\n});\n\nbreakBtn.addEventListener('touchstart', (e) => {\n\te.preventDefault();\n\tfocusGame();\n\tbreakTargetBlock();\n}, {\n\tpassive: false\n});\n\nplaceBtn.addEventListener('touchstart', (e) => {\n\te.preventDefault();\n\tfocusGame();\n\tplaceTargetBlock();\n}, {\n\tpassive: false\n});\n\nbreakBtn.addEventListener('click', () => {\n\tfocusGame();\n\tbreakTargetBlock();\n});\n\nplaceBtn.addEventListener('click', () => {\n\tfocusGame();\n\tplaceTargetBlock();\n});\n\njumpBtn.addEventListener('pointerdown', (e) => {\n\te.preventDefault();\n\tfocusGame();\n\ttouchState.jumpPressed = true;\n});\n\nwindow.addEventListener('pointerup', () => {\n\ttouchState.jumpPressed = false;\n});\n\nwindow.addEventListener('pointercancel', () => {\n\ttouchState.jumpPressed = false;\n});\n\nfunction resetJoystick() {\n\ttouchState.joystickVector.x = 0;\n\ttouchState.joystickVector.y = 0;\n\tjoystickKnob.style.transform = 'translate(-50%, -50%)';\n}\n\nfunction updateJoystickFromEvent(e) {\n\tconst rect = joystickBase.getBoundingClientRect();\n\tconst centerX = rect.left + rect.width / 2;\n\tconst centerY = rect.top + rect.height / 2;\n\tconst dx = e.clientX - centerX;\n\tconst dy = e.clientY - centerY;\n\tconst max = rect.width * 0.32;\n\tconst dist = Math.hypot(dx, dy);\n\tconst clamped = Math.min(dist, max);\n\tconst angle = Math.atan2(dy, dx);\n\tconst x = dist > 0 ? Math.cos(angle) * clamped : 0;\n\tconst y = dist > 0 ? Math.sin(angle) * clamped : 0;\n\ttouchState.joystickVector.x = x / max;\n\ttouchState.joystickVector.y = y / max;\n\tjoystickKnob.style.transform = `translate(calc(-50% + ${x}px), calc(-50% + ${y}px))`;\n}\n\njoystickZone.addEventListener('pointerdown', (e) => {\n\tif (!isTouchDevice) return;\n\te.preventDefault();\n\tfocusGame();\n\ttouchState.joystickId = e.pointerId;\n\tjoystickZone.setPointerCapture(e.pointerId);\n\tupdateJoystickFromEvent(e);\n});\n\njoystickZone.addEventListener('pointermove', (e) => {\n\tif (touchState.joystickId !== e.pointerId) return;\n\te.preventDefault();\n\tupdateJoystickFromEvent(e);\n});\n\nfunction endJoystick(e) {\n\tif (touchState.joystickId !== e.pointerId) return;\n\ttouchState.joystickId = null;\n\tresetJoystick();\n}\n\njoystickZone.addEventListener('pointerup', endJoystick);\njoystickZone.addEventListener('pointercancel', endJoystick);\n\nlookZone.addEventListener('pointerdown', (e) => {\n\tif (!isTouchDevice) return;\n\te.preventDefault();\n\tfocusGame();\n\ttouchState.lookId = e.pointerId;\n\ttouchState.lookActive = true;\n\ttouchState.lastLookX = e.clientX;\n\ttouchState.lastLookY = e.clientY;\n\tlookZone.setPointerCapture(e.pointerId);\n});\n\nlookZone.addEventListener('pointermove', (e) => {\n\tif (!isTouchDevice || touchState.lookId !== e.pointerId || !touchState.lookActive) return;\n\te.preventDefault();\n\tconst dx = e.clientX - touchState.lastLookX;\n\tconst dy = e.clientY - touchState.lastLookY;\n\ttouchState.lastLookX = e.clientX;\n\ttouchState.lastLookY = e.clientY;\n\tplayer.yaw -= dx * 0.009;\n\tplayer.pitch -= dy * 0.009;\n\tplayer.pitch = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, player.pitch));\n});\n\nfunction endLook(e) {\n\tif (touchState.lookId !== e.pointerId) return;\n\ttouchState.lookId = null;\n\ttouchState.lookActive = false;\n}\n\nlookZone.addEventListener('pointerup', endLook);\nlookZone.addEventListener('pointercancel', endLook);\n\nlet stepTimer = 0;\nlet jumpLatch = false;\n\nfunction movePlayer(dt) {\n\tlet inputX = 0;\n\tlet inputZ = 0;\n\tif (keys.has('KeyW') || keys.has('ArrowUp')) inputZ += 1;\n\tif (keys.has('KeyS') || keys.has('ArrowDown')) inputZ -= 1;\n\tif (keys.has('KeyA') || keys.has('ArrowLeft')) inputX -= 1;\n\tif (keys.has('KeyD') || keys.has('ArrowRight')) inputX += 1;\n\n\tif (isTouchDevice) {\n\t\tinputX += touchState.joystickVector.x;\n\t\tinputZ += -touchState.joystickVector.y;\n\t}\n\n\tconst forward = new THREE.Vector3(-Math.sin(player.yaw), 0, -Math.cos(player.yaw));\n\tconst right = new THREE.Vector3(-forward.z, 0, forward.x);\n\tplayer.direction.set(0, 0, 0);\n\tplayer.direction.addScaledVector(forward, inputZ);\n\tplayer.direction.addScaledVector(right, inputX);\n\tif (player.direction.lengthSq() > 0) player.direction.normalize();\n\tconst sprinting = keys.has('ShiftLeft') || keys.has('ShiftRight');\n\tconst speed = sprinting ? player.speed * 1.55 : player.speed;\n\tplayer.velocity.x = player.direction.x * speed;\n\tplayer.velocity.z = player.direction.z * speed;\n\n\tconst wantsJump = keys.has('Space') || touchState.jumpPressed;\n\n\tif (noclip) {\n\t\tconst flyY = (wantsJump ? 1 : 0) + ((keys.has('ControlLeft') || keys.has('ControlRight')) ? -1 : 0);\n\t\tplayer.position.x += player.velocity.x * dt;\n\t\tplayer.position.z += player.velocity.z * dt;\n\t\tplayer.position.y += flyY * speed * dt;\n\t\tplayer.velocity.y = 0;\n\t\tplayer.onGround = false;\n\t\treturn;\n\t}\n\n\tplayer.prevYVelocity = player.velocity.y;\n\tplayer.velocity.y -= gravity * dt;\n\tif (wantsJump && player.onGround && !jumpLatch) {\n\t\tplayer.velocity.y = 8.8;\n\t\tplayer.onGround = false;\n\t\tjumpLatch = true;\n\t\tplayJumpSound();\n\t}\n\tif (!wantsJump) jumpLatch = false;\n\n\tconst nextX = player.position.x + player.velocity.x * dt;\n\tif (!collidesAt(nextX, player.position.y, player.position.z)) player.position.x = nextX;\n\n\tconst nextZ = player.position.z + player.velocity.z * dt;\n\tif (!collidesAt(player.position.x, player.position.y, nextZ)) player.position.z = nextZ;\n\n\tconst nextY = player.position.y + player.velocity.y * dt;\n\tif (!collidesAt(player.position.x, nextY, player.position.z)) {\n\t\tplayer.position.y = nextY;\n\t\tplayer.onGround = false;\n\t} else {\n\t\tif (player.velocity.y < 0) player.onGround = true;\n\t\tplayer.velocity.y = 0;\n\t}\n\n\tif (player.onGround && player.direction.lengthSq() > 0.01) {\n\t\tstepTimer -= dt;\n\t\tif (stepTimer <= 0) {\n\t\t\tplayStepSound();\n\t\t\tstepTimer = sprinting ? 0.24 : 0.32;\n\t\t}\n\t} else {\n\t\tstepTimer = 0;\n\t}\n\n\tif (player.position.y < -30) {\n\t\trespawnPlayer();\n\t}\n}\n\nfunction respawnPlayer() {\n\tfindSpawn();\n\tplayer.velocity.set(0, 0, 0);\n\tfor (let i = 0; i < 64 && collidesAt(player.position.x, player.position.y, player.position.z); i++) player.position.y += 1;\n}\n\nfunction updateBlockHighlight() {\n\tconst hit = getTargetBlock();\n\tif (!hit) {\n\t\thighlightMesh.visible = false;\n\t\treturn;\n\t}\n\tconst {\n\t\tx,\n\t\ty,\n\t\tz\n\t} = hit.block.object.userData;\n\thighlightMesh.visible = true;\n\thighlightMesh.position.set(x + 0.5, y + 0.5, z + 0.5);\n}\n\nfunction saveWorld() {\n\ttry {\n\t\tconst blocks = [...world.values()].map((mesh) => mesh.userData);\n\t\tconst data = {\n\t\t\tblocks,\n\t\t\tplayer: {\n\t\t\t\tx: player.position.x,\n\t\t\t\ty: player.position.y,\n\t\t\t\tz: player.position.z,\n\t\t\t\tyaw: player.yaw,\n\t\t\t\tpitch: player.pitch\n\t\t\t},\n\t\t\tselectedBlockType\n\t\t};\n\t\tlocalStorage.setItem(saveKey, JSON.stringify(data));\n\t\tsetSaveState('saved');\n\t} catch {\n\t\tsetSaveState('save failed');\n\t}\n}\n\nfunction clearWorldMeshes() {\n\tfor (const mesh of [...world.values()]) scene.remove(mesh);\n\tworld.clear();\n}\n\nfunction loadWorld() {\n\tconst raw = localStorage.getItem(saveKey);\n\tif (!raw) {\n\t\tsetSaveState('no save');\n\t\treturn;\n\t}\n\n\ttry {\n\t\tconst data = JSON.parse(raw);\n\t\tclearWorldMeshes();\n\t\tfor (const b of data.blocks) addBlock(b.x, b.y, b.z, b.type);\n\n\t\tplayer.position.set(data.player.x, data.player.y, data.player.z);\n\t\tplayer.yaw = data.player.yaw || 0;\n\t\tplayer.pitch = data.player.pitch || 0;\n\t\tplayer.velocity.set(0, 0, 0);\n\t\tselectedBlockType = data.selectedBlockType || 'grass';\n\t\tselectBlock(selectedBlockType);\n\t\tupdateChunkVisibility(true);\n\t\tsetSaveState('loaded');\n\t} catch {\n\t\tsetSaveState('load failed');\n\t}\n}\n\nconst clock = new THREE.Clock();\n\nfunction animate() {\n\trequestAnimationFrame(animate);\n\tconst dt = Math.min(clock.getDelta(), 0.05);\n\tconst t = clock.elapsedTime * 0.05;\n\n\tconst daylight = 0.58 + Math.sin(t) * 0.24;\n\tscene.fog.near = 42;\n\tscene.fog.far = 195 + daylight * 30;\n\tsun.position.x = Math.sin(t) * 50;\n\tsun.position.y = 30 + Math.cos(t) * 20;\n\tsun.intensity = 0.82 + Math.max(0, daylight) * 0.62;\n\tambient.intensity = 0.56 + Math.max(0, daylight) * 0.58;\n\tsky.material.uniforms.topColor.value.setHSL(0.58, 0.78, 0.5 + daylight * 0.14);\n\tsky.material.uniforms.bottomColor.value.setHSL(0.56, 0.52, 0.9 - Math.max(0, -daylight) * 0.18);\n\n\tif (gameActive) {\n\t\tmovePlayer(dt);\n\t\tupdateBlockHighlight();\n\t} else {\n\t\thighlightMesh.visible = false;\n\t}\n\n\tupdateChunkVisibility();\n\tcamera.position.copy(player.position);\n\tcamera.rotation.order = 'YXZ';\n\tcamera.rotation.y = player.yaw;\n\tcamera.rotation.x = player.pitch;\n\tsun.target.position.copy(player.position);\n\tsky.position.copy(player.position);\n\n\trenderer.render(scene, camera);\n}\n\nanimate();\n\nfunction resize() {\n\tcamera.aspect = window.innerWidth / window.innerHeight;\n\tcamera.updateProjectionMatrix();\n\trenderer.setSize(window.innerWidth, window.innerHeight);\n}\n\nwindow.addEventListener('resize', resize);\nwindow.addEventListener('orientationchange', () => setTimeout(resize, 120));"},{"name":"style.css","content":":root {\n\tcolor-scheme: dark;\n\tfont-family: Inter, system-ui, sans-serif;\n\t--ui-bg: rgba(12, 16, 24, 0.62);\n\t--ui-border: rgba(255, 255, 255, 0.14);\n\t--ui-border-strong: rgba(255, 255, 255, 0.26);\n\t--ui-text: #f4f7ff;\n\t--ui-muted: rgba(244, 247, 255, 0.78);\n\t--accent: #7cff7c;\n\t--accent-soft: rgba(124, 255, 124, 0.18);\n\t--danger: #ff847c;\n\t--shadow: 0 18px 40px rgba(0, 0, 0, 0.34);\n\t--shadow-soft: 0 10px 24px rgba(0, 0, 0, 0.22);\n\t--overlay-glow: rgba(124, 255, 124, 0.22);\n\t--sky-top: #6fb7ff;\n\t--sky-bottom: #dff4ff;\n}\n\n* {\n\tbox-sizing: border-box;\n\t-webkit-tap-highlight-color: transparent;\n}\n\nhtml,\nbody {\n\tmargin: 0;\n\twidth: 100%;\n\theight: 100%;\n\toverflow: hidden;\n\tbackground: linear-gradient(180deg, var(--sky-top), var(--sky-bottom));\n\ttouch-action: none;\n}\n\nbody {\n\tfont-family: Inter, system-ui, sans-serif;\n\toverscroll-behavior: none;\n\tuser-select: none;\n}\n\ncanvas {\n\tdisplay: block;\n\tposition: absolute;\n\tinset: 0;\n\twidth: 100%;\n\theight: 100%;\n\tz-index: 0;\n}\n\nbutton {\n\tfont: inherit;\n}\n\n#game {\n\tposition: fixed;\n\tinset: 0;\n\toverflow: hidden;\n\tbackground: linear-gradient(180deg, var(--sky-top), var(--sky-bottom));\n}\n\n#overlay {\n\tposition: absolute;\n\tinset: 0;\n\tdisplay: grid;\n\tplace-items: center;\n\tpadding: 1.25rem;\n\tbackground:\n\t\tradial-gradient(circle at 20% 20%, rgba(124, 255, 124, 0.12), transparent 28%),\n\t\tradial-gradient(circle at 80% 18%, rgba(255, 255, 255, 0.08), transparent 26%),\n\t\tlinear-gradient(180deg, rgba(8, 11, 18, 0.3), rgba(8, 11, 18, 0.84));\n\tz-index: 30;\n}\n\n#overlay.hidden {\n\tdisplay: none;\n}\n\n.overlay__panel {\n\tposition: relative;\n\toverflow: hidden;\n\twidth: min(100%, 34rem);\n\tdisplay: grid;\n\tgap: 0.9rem;\n\tpadding: 1.45rem 1.2rem;\n\tborder: 1px solid var(--ui-border);\n\tborder-radius: 1.6rem;\n\tbackground: linear-gradient(180deg, rgba(24, 30, 44, 0.84), rgba(11, 15, 23, 0.88));\n\tbackdrop-filter: blur(14px);\n\tbox-shadow: var(--shadow), 0 0 0 1px rgba(255, 255, 255, 0.05) inset;\n\tcolor: white;\n\ttext-align: center;\n}\n\n.overlay__panel::before {\n\tcontent: \"\";\n\tposition: absolute;\n\tinset: -20% auto auto -10%;\n\twidth: 12rem;\n\theight: 12rem;\n\tborder-radius: 50%;\n\tbackground: radial-gradient(circle, var(--overlay-glow), transparent 70%);\n\tpointer-events: none;\n}\n\n.overlay__panel::after {\n\tcontent: \"\";\n\tposition: absolute;\n\tinset: auto -8% -18% auto;\n\twidth: 13rem;\n\theight: 13rem;\n\tborder-radius: 50%;\n\tbackground: radial-gradient(circle, rgba(94, 164, 255, 0.16), transparent 72%);\n\tpointer-events: none;\n}\n\n.overlay__eyebrow,\n.overlay__stats,\n.overlay__actions,\n#overlay h1,\n.overlay__subtitle,\n#overlay ul {\n\tposition: relative;\n\tz-index: 1;\n}\n\n.overlay__eyebrow {\n\tjustify-self: center;\n\tpadding: 0.38rem 0.7rem;\n\tborder: 1px solid rgba(255, 255, 255, 0.16);\n\tborder-radius: 999px;\n\tbackground: rgba(255, 255, 255, 0.06);\n\tfont-size: 0.76rem;\n\tfont-weight: 700;\n\tletter-spacing: 0.12em;\n\ttext-transform: uppercase;\n\tcolor: rgba(255, 255, 255, 0.88);\n}\n\n#overlay h1 {\n\tmargin: 0;\n\tfont-size: clamp(2.1rem, 6vw, 4.4rem);\n\tline-height: 0.95;\n\tletter-spacing: -0.04em;\n\ttext-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);\n}\n\n.overlay__subtitle {\n\tmargin: 0;\n\tfont-size: 0.98rem;\n\tcolor: var(--ui-muted);\n\tletter-spacing: 0.04em;\n\ttext-transform: uppercase;\n}\n\n.overlay__stats {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(3, minmax(0, 1fr));\n\tgap: 0.65rem;\n}\n\n.overlay__stat {\n\tdisplay: grid;\n\tgap: 0.24rem;\n\tpadding: 0.72rem 0.6rem;\n\tborder: 1px solid rgba(255, 255, 255, 0.1);\n\tborder-radius: 1rem;\n\tbackground: rgba(255, 255, 255, 0.05);\n\tbox-shadow: var(--shadow-soft);\n}\n\n.overlay__stat strong {\n\tfont-size: 0.92rem;\n}\n\n.overlay__stat span {\n\tfont-size: 0.75rem;\n\tcolor: rgba(255, 255, 255, 0.74);\n}\n\n#overlay ul {\n\tmargin: 0;\n\tpadding: 0.85rem 0.9rem;\n\tlist-style: none;\n\tdisplay: grid;\n\tgap: 0.5rem;\n\tborder: 1px solid rgba(255, 255, 255, 0.1);\n\tborder-radius: 1rem;\n\tbackground: rgba(255, 255, 255, 0.04);\n\ttext-align: left;\n\tfont-size: 0.96rem;\n\tcolor: rgba(255, 255, 255, 0.92);\n}\n\n#startBtn,\n.mobile-btn,\n.mobile-action {\n\tappearance: none;\n\tborder: 1px solid var(--ui-border-strong);\n\tcolor: var(--ui-text);\n\tbackground: linear-gradient(180deg, rgba(255, 255, 255, 0.16), rgba(255, 255, 255, 0.06));\n\tbackdrop-filter: blur(10px);\n\tbox-shadow: var(--shadow);\n}\n\n.overlay__actions {\n\tdisplay: grid;\n\tgap: 0.5rem;\n}\n\n#startBtn {\n\tborder-radius: 999px;\n\tpadding: 0.95rem 1.4rem;\n\tfont-weight: 800;\n\tletter-spacing: 0.01em;\n\ttransition: transform 0.18s ease, box-shadow 0.18s ease, filter 0.18s ease;\n\tbackground: linear-gradient(180deg, #9cff84, #62d85b);\n\tcolor: #102010;\n\tborder-color: rgba(255, 255, 255, 0.28);\n\tbox-shadow: 0 14px 28px rgba(80, 180, 70, 0.26), 0 0 0 1px rgba(255, 255, 255, 0.08) inset;\n}\n\n#startBtn:hover,\n#startBtn:focus-visible {\n\ttransform: translateY(-1px);\n\tfilter: brightness(1.04);\n}\n\n.overlay__hint {\n\tmargin: 0;\n\tfont-size: 0.78rem;\n\tcolor: rgba(255, 255, 255, 0.64);\n}\n\n#hud {\n\tposition: absolute;\n\tinset: 0;\n\tpointer-events: none;\n\tz-index: 20;\n}\n\n#game>canvas {\n\tpointer-events: auto;\n}\n\n#crosshair {\n\tposition: absolute;\n\tleft: 50%;\n\ttop: 50%;\n\twidth: 28px;\n\theight: 28px;\n\ttransform: translate(-50%, -50%);\n\tz-index: 12;\n\tfilter: drop-shadow(0 0 8px rgba(0, 0, 0, 0.8));\n}\n\n#crosshair::before,\n#crosshair::after {\n\tcontent: \"\";\n\tposition: absolute;\n\tleft: 50%;\n\ttop: 50%;\n\tbackground: #fff;\n\ttransform: translate(-50%, -50%);\n\tborder-radius: 999px;\n}\n\n#crosshair::before {\n\twidth: 3px;\n\theight: 28px;\n\tbox-shadow: 0 0 0 1px rgba(0, 0, 0, 0.45);\n}\n\n#crosshair::after {\n\twidth: 28px;\n\theight: 3px;\n\tbox-shadow: 0 0 0 1px rgba(0, 0, 0, 0.45);\n}\n\n#hudTop,\n#mobileTopActions {\n\tposition: absolute;\n\ttop: env(safe-area-inset-top, 0);\n\tpadding-top: 0.9rem;\n}\n\n#hudTop {\n\tleft: 1rem;\n\tdisplay: grid;\n\tgap: 0.5rem;\n\tcolor: white;\n\tfont-weight: 700;\n\ttext-shadow: 0 2px 10px rgba(0, 0, 0, 0.8);\n}\n\n#health,\n#saveState {\n\tpadding: 0.5rem 0.78rem;\n\tborder-radius: 0.85rem;\n\tbackground: linear-gradient(180deg, rgba(15, 19, 28, 0.8), rgba(15, 19, 28, 0.58));\n\tborder: 1px solid var(--ui-border);\n\tbackdrop-filter: blur(8px);\n}\n\n#mobileTopActions {\n\tright: 1rem;\n\tdisplay: flex;\n\tgap: 0.55rem;\n\tpointer-events: auto;\n}\n\n.mobile-btn {\n\tpadding: 0.72rem 0.92rem;\n\tborder-radius: 999px;\n\tfont-weight: 700;\n\tmin-width: 4.3rem;\n}\n\n#hotbarWrap {\n\tposition: absolute;\n\tleft: 50%;\n\tbottom: max(1rem, env(safe-area-inset-bottom, 0px));\n\ttransform: translateX(-50%);\n\twidth: min(100vw - 1rem, 42rem);\n\tpointer-events: none;\n}\n\n#hotbar {\n\tdisplay: flex;\n\tgap: 0.55rem;\n\tpadding: 0.7rem;\n\tborder-radius: 1.15rem;\n\tbackground: linear-gradient(180deg, rgba(20, 23, 31, 0.74), rgba(14, 16, 22, 0.58));\n\tbackdrop-filter: blur(10px);\n\tpointer-events: auto;\n\tbox-shadow: 0 14px 38px rgba(0, 0, 0, 0.32);\n\toverflow-x: auto;\n\tscrollbar-width: none;\n}\n\n#hotbar::-webkit-scrollbar {\n\tdisplay: none;\n}\n\n.slot {\n\tappearance: none;\n\tposition: relative;\n\tdisplay: grid;\n\tjustify-items: center;\n\tgap: 0.45rem;\n\tmin-width: 82px;\n\tpadding: 0.55rem 0.5rem 0.65rem;\n\tborder: 2px solid rgba(255, 255, 255, 0.16);\n\tbackground: linear-gradient(180deg, rgba(255, 255, 255, 0.12), rgba(255, 255, 255, 0.06));\n\tcolor: white;\n\tborder-radius: 0.9rem;\n\tfont-weight: 700;\n\tcursor: pointer;\n\ttransition: transform 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease;\n}\n\n.slot.active {\n\tborder-color: var(--accent);\n\tbackground: linear-gradient(180deg, rgba(124, 255, 124, 0.18), rgba(255, 255, 255, 0.08));\n\tbox-shadow: 0 0 0 2px rgba(124, 255, 124, 0.18) inset, 0 10px 24px rgba(0, 0, 0, 0.28);\n}\n\n.slotKey {\n\tposition: absolute;\n\ttop: 0.35rem;\n\tleft: 0.4rem;\n\tpadding: 0.12rem 0.34rem;\n\tborder-radius: 999px;\n\tfont-size: 0.72rem;\n\tline-height: 1;\n\tbackground: rgba(0, 0, 0, 0.4);\n\tcolor: rgba(255, 255, 255, 0.92);\n}\n\n.slotPreview {\n\twidth: 42px;\n\theight: 42px;\n\tmargin-top: 0.35rem;\n\tborder-radius: 0.45rem;\n\tborder: 1px solid rgba(255, 255, 255, 0.16);\n\tbox-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.18), 0 4px 12px rgba(0, 0, 0, 0.22);\n\tbackground-size: cover;\n\tbackground-position: center;\n}\n\n.slotLabel {\n\tfont-size: 0.8rem;\n\tletter-spacing: 0.02em;\n\tcolor: rgba(255, 255, 255, 0.94);\n}\n\n.slot--grass .slotPreview {\n\tbackground:\n\t\tlinear-gradient(180deg, #78d64d 0 34%, #5baa34 34% 48%, #7a5a3a 48% 100%),\n\t\trepeating-linear-gradient(45deg, rgba(255, 255, 255, 0.08) 0 4px, rgba(0, 0, 0, 0.08) 4px 8px);\n}\n\n.slot--dirt .slotPreview {\n\tbackground:\n\t\tradial-gradient(circle at 25% 30%, #9a7953 0 10%, transparent 11%),\n\t\tradial-gradient(circle at 70% 62%, #5b4128 0 10%, transparent 11%),\n\t\tradial-gradient(circle at 52% 24%, #7f5d3c 0 12%, transparent 13%),\n\t\tlinear-gradient(135deg, #8b6845, #6a4b2f);\n}\n\n.slot--stone .slotPreview {\n\tbackground:\n\t\tradial-gradient(circle at 28% 28%, #c4c7ce 0 9%, transparent 10%),\n\t\tradial-gradient(circle at 64% 64%, #70747d 0 10%, transparent 11%),\n\t\tradial-gradient(circle at 72% 24%, #9ea3ac 0 9%, transparent 10%),\n\t\tlinear-gradient(135deg, #9ba0a8, #6e727a);\n}\n\n.slot--wood .slotPreview {\n\tbackground:\n\t\trepeating-linear-gradient(0deg, rgba(91, 54, 20, 0.45) 0 3px, rgba(169, 111, 58, 0.18) 3px 6px),\n\t\tlinear-gradient(135deg, #b17337, #7a4d24);\n}\n\n.slot--leaves .slotPreview {\n\tbackground:\n\t\tradial-gradient(circle at 28% 30%, #6fe27d 0 10%, transparent 11%),\n\t\tradial-gradient(circle at 68% 38%, #2f8f3a 0 13%, transparent 14%),\n\t\tradial-gradient(circle at 52% 72%, #46b853 0 12%, transparent 13%),\n\t\tlinear-gradient(135deg, #3ea548, #226b2b);\n}\n\n.slot--water .slotPreview {\n\tbackground:\n\t\tlinear-gradient(180deg, rgba(255, 255, 255, 0.35), rgba(255, 255, 255, 0.05)),\n\t\trepeating-linear-gradient(135deg, rgba(255, 255, 255, 0.14) 0 5px, rgba(255, 255, 255, 0.02) 5px 10px),\n\t\tlinear-gradient(135deg, #5ea4ff, #2f63c8);\n}\n\n#mobileControls {\n\tposition: absolute;\n\tleft: 0;\n\tright: 0;\n\tbottom: calc(6.7rem + env(safe-area-inset-bottom, 0px));\n\tdisplay: flex;\n\tjustify-content: space-between;\n\talign-items: flex-end;\n\tpadding: 0 1rem;\n\tpointer-events: none;\n\tz-index: 21;\n}\n\n#joystickZone,\n#mobileActions {\n\tpointer-events: auto;\n}\n\n#joystickZone {\n\twidth: min(40vw, 170px);\n\theight: min(40vw, 170px);\n\tdisplay: grid;\n\tplace-items: center;\n}\n\n#joystickBase {\n\tposition: relative;\n\twidth: min(34vw, 138px);\n\theight: min(34vw, 138px);\n\tborder-radius: 50%;\n\tbackground: radial-gradient(circle at 30% 30%, rgba(255, 255, 255, 0.1), rgba(15, 18, 28, 0.38));\n\tborder: 1px solid rgba(255, 255, 255, 0.14);\n\tbackdrop-filter: blur(10px);\n\tbox-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.04), var(--shadow);\n}\n\n#joystickKnob {\n\tposition: absolute;\n\tleft: 50%;\n\ttop: 50%;\n\twidth: min(15vw, 62px);\n\theight: min(15vw, 62px);\n\tborder-radius: 50%;\n\ttransform: translate(-50%, -50%);\n\tbackground: radial-gradient(circle at 35% 30%, rgba(255, 255, 255, 0.28), rgba(255, 255, 255, 0.08));\n\tborder: 1px solid rgba(255, 255, 255, 0.2);\n\tbox-shadow: 0 10px 24px rgba(0, 0, 0, 0.32);\n}\n\n#mobileActions {\n\tdisplay: grid;\n\tgap: 0.8rem;\n\tjustify-items: end;\n}\n\n.mobileActions__cluster {\n\tdisplay: flex;\n\tgap: 0.75rem;\n}\n\n.mobile-action {\n\tmin-width: 4.8rem;\n\tmin-height: 4.8rem;\n\tpadding: 0.9rem;\n\tborder-radius: 1.2rem;\n\tfont-weight: 800;\n\tpointer-events: auto;\n}\n\n.mobile-action--wide {\n\tmin-width: 5.6rem;\n}\n\n#lookZone {\n\tposition: absolute;\n\tright: 0;\n\ttop: 0;\n\twidth: 52vw;\n\theight: calc(100% - 7.2rem - env(safe-area-inset-bottom, 0px));\n\tpointer-events: auto;\n\tbackground: transparent;\n\tz-index: 19;\n}\n\nbody:not(.is-mobile) #mobileControls,\nbody:not(.is-mobile) #mobileTopActions,\nbody:not(.is-mobile) #lookZone {\n\tdisplay: none;\n}\n\n.is-mobile #crosshair {\n\topacity: 0.92;\n}\n\n@media (max-width: 900px) {\n\t#hudTop {\n\t\tleft: 0.7rem;\n\t\tpadding-top: 0.7rem;\n\t}\n\n\t#mobileTopActions {\n\t\tright: 0.7rem;\n\t\tpadding-top: 0.7rem;\n\t}\n\n\t#health,\n\t#saveState {\n\t\tfont-size: 0.88rem;\n\t\tpadding: 0.46rem 0.66rem;\n\t}\n\n\t.mobile-btn {\n\t\tpadding: 0.68rem 0.84rem;\n\t\tfont-size: 0.88rem;\n\t}\n\n\t#hotbarWrap {\n\t\twidth: calc(100vw - 0.8rem);\n\t\tbottom: max(0.4rem, env(safe-area-inset-bottom, 0px));\n\t}\n\n\t#hotbar {\n\t\tgap: 0.42rem;\n\t\tpadding: 0.5rem;\n\t\tborder-radius: 1rem;\n\t}\n\n\t.slot {\n\t\tmin-width: 68px;\n\t\tpadding: 0.44rem 0.38rem 0.54rem;\n\t}\n\n\t.slotPreview {\n\t\twidth: 34px;\n\t\theight: 34px;\n\t}\n\n\t.slotLabel {\n\t\tfont-size: 0.7rem;\n\t}\n\n\t.slotKey {\n\t\tfont-size: 0.65rem;\n\t}\n\n\t#mobileControls {\n\t\tpadding: 0 0.7rem;\n\t\tbottom: calc(5.9rem + env(safe-area-inset-bottom, 0px));\n\t}\n\n\t.mobile-action {\n\t\tmin-width: 4.2rem;\n\t\tmin-height: 4.2rem;\n\t\tborder-radius: 1rem;\n\t\tfont-size: 0.9rem;\n\t}\n}\n\n@media (max-width: 560px) {\n\t.overlay__panel {\n\t\tpadding: 1.1rem 0.95rem;\n\t\tgap: 0.75rem;\n\t}\n\n\t.overlay__stats {\n\t\tgrid-template-columns: 1fr;\n\t}\n\n\t#overlay ul {\n\t\tfont-size: 0.88rem;\n\t}\n\n\t#crosshair {\n\t\twidth: 22px;\n\t\theight: 22px;\n\t}\n\n\t#crosshair::before {\n\t\theight: 22px;\n\t}\n\n\t#crosshair::after {\n\t\twidth: 22px;\n\t}\n}"}],"folders":[]},"variants":null,"createdAt":"2026-03-06T17:23:50.314Z","updatedAt":"2026-03-06T23:28:38.700Z","hasThumbnail":true}}