{"project":{"id":"z8j2Wlh","userId":"davidyarham@gmail.com","username":null,"userPicture":null,"name":"Crossy Road Clone","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<script src=\"https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js\"></script>\n</head>\n<body>\n<div id=\"game-container\">\n\t<canvas id=\"game-canvas\"></canvas>\n\t<div id=\"ui-overlay\">\n\t\t<div id=\"score\">0</div>\n\t\t<div id=\"high-score\">BEST: 0</div>\n\t</div>\n\t<div id=\"start-screen\">\n\t\t<h1>CROSSY<br>ROAD</h1>\n\t\t<p>TAP or ARROW KEYS to move</p>\n\t\t<button id=\"start-btn\">START</button>\n\t</div>\n\t<div id=\"game-over\" class=\"hidden\">\n\t\t<h2>SPLAT!</h2>\n\t\t<p id=\"final-score\">Score: 0</p>\n\t\t<button id=\"retry-btn\">RETRY</button>\n\t</div>\n</div>\n  <script type=\"module\" src=\"main.js\"></script>\n</body>\n</html>"},{"name":"main.js","content":"const canvas = document.getElementById('game-canvas');\nconst scoreEl = document.getElementById('score');\nconst highScoreEl = document.getElementById('high-score');\nconst startScreen = document.getElementById('start-screen');\nconst gameOverScreen = document.getElementById('game-over');\nconst finalScoreEl = document.getElementById('final-score');\nconst startBtn = document.getElementById('start-btn');\nconst retryBtn = document.getElementById('retry-btn');\n\n// Audio\nconst audioCtx = new(window.AudioContext || window.webkitAudioContext)();\n\nfunction playSound(freq, dur, type = 'square', vol = 0.15) {\n\tconst osc = audioCtx.createOscillator();\n\tconst gain = audioCtx.createGain();\n\tosc.type = type;\n\tosc.frequency.setValueAtTime(freq, audioCtx.currentTime);\n\tgain.gain.setValueAtTime(vol, audioCtx.currentTime);\n\tgain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + dur);\n\tosc.connect(gain);\n\tgain.connect(audioCtx.destination);\n\tosc.start();\n\tosc.stop(audioCtx.currentTime + dur);\n}\n\nfunction hopSound() {\n\tplaySound(440, 0.08, 'square', 0.12);\n\tsetTimeout(() => playSound(580, 0.08, 'square', 0.1), 40);\n}\n\nfunction scoreSound() {\n\tplaySound(660, 0.1, 'square', 0.1);\n\tsetTimeout(() => playSound(880, 0.15, 'square', 0.1), 60);\n}\n\nfunction deathSound() {\n\tplaySound(200, 0.3, 'sawtooth', 0.2);\n\tsetTimeout(() => playSound(120, 0.4, 'sawtooth', 0.15), 150);\n}\n\nfunction splashSound() {\n\tconst bufferSize = audioCtx.sampleRate * 0.3;\n\tconst buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);\n\tconst data = buffer.getChannelData(0);\n\tfor (let i = 0; i < bufferSize; i++) {\n\t\tdata[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / bufferSize, 2);\n\t}\n\tconst src = audioCtx.createBufferSource();\n\tconst gain = audioCtx.createGain();\n\tsrc.buffer = buffer;\n\tgain.gain.setValueAtTime(0.15, audioCtx.currentTime);\n\tsrc.connect(gain);\n\tgain.connect(audioCtx.destination);\n\tsrc.start();\n}\n\n// Three.js Setup\nconst scene = new THREE.Scene();\nscene.background = new THREE.Color(0x87ceeb);\nscene.fog = new THREE.Fog(0x87ceeb, 25, 55);\n\nconst camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 100);\nconst renderer = new THREE.WebGLRenderer({\n\tcanvas,\n\tantialias: false\n});\nrenderer.setSize(window.innerWidth, window.innerHeight);\nrenderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));\nrenderer.shadowMap.enabled = true;\nrenderer.shadowMap.type = THREE.BasicShadowMap;\n\n// Lighting\nconst ambientLight = new THREE.AmbientLight(0xffffff, 0.6);\nscene.add(ambientLight);\n\nconst dirLight = new THREE.DirectionalLight(0xffffff, 0.8);\ndirLight.position.set(10, 20, 10);\ndirLight.castShadow = true;\ndirLight.shadow.mapSize.set(1024, 1024);\ndirLight.shadow.camera.near = 0.5;\ndirLight.shadow.camera.far = 60;\ndirLight.shadow.camera.left = -20;\ndirLight.shadow.camera.right = 20;\ndirLight.shadow.camera.top = 20;\ndirLight.shadow.camera.bottom = -20;\nscene.add(dirLight);\n\n// Materials\nconst mat = (color) => new THREE.MeshLambertMaterial({\n\tcolor\n});\n\n// Colors\nconst GRASS_COLORS = [0x4a7c3f, 0x3d6b34, 0x558b47];\nconst ROAD_COLOR = 0x444444;\nconst WATER_COLOR = 0x2980b9;\nconst SIDEWALK_COLOR = 0x666666;\n\n// Game State\nlet gameState = 'menu';\nlet score = 0;\nlet highScore = parseInt(localStorage.getItem('crossyHighScore') || '0');\nlet playerRow = 0;\nlet playerCol = 0;\nlet maxRow = 0;\nlet isHopping = false;\nlet hopProgress = 0;\nlet hopFrom = {\n\tx: 0,\n\tz: 0\n};\nlet hopTo = {\n\tx: 0,\n\tz: 0\n};\nlet hopDir = 'forward';\nlet lanes = [];\nlet player;\nlet isDead = false;\n\nhighScoreEl.textContent = `BEST: ${highScore}`;\n\n// Player (Chicken)\nfunction createPlayer() {\n\tconst group = new THREE.Group();\n\n\t// Body\n\tconst body = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.55, 0.6), mat(0xffffff));\n\tbody.position.y = 0.45;\n\tbody.castShadow = true;\n\tgroup.add(body);\n\n\t// Head\n\tconst head = new THREE.Mesh(new THREE.BoxGeometry(0.4, 0.4, 0.4), mat(0xffffff));\n\thead.position.set(0, 0.9, -0.1);\n\thead.castShadow = true;\n\tgroup.add(head);\n\n\t// Beak\n\tconst beak = new THREE.Mesh(new THREE.BoxGeometry(0.15, 0.1, 0.15), mat(0xffa500));\n\tbeak.position.set(0, 0.82, -0.35);\n\tgroup.add(beak);\n\n\t// Comb\n\tconst comb = new THREE.Mesh(new THREE.BoxGeometry(0.12, 0.15, 0.2), mat(0xff3333));\n\tcomb.position.set(0, 1.15, -0.05);\n\tgroup.add(comb);\n\n\t// Eyes\n\tconst eyeGeo = new THREE.BoxGeometry(0.08, 0.08, 0.05);\n\tconst eyeMat = mat(0x111111);\n\tconst leftEye = new THREE.Mesh(eyeGeo, eyeMat);\n\tleftEye.position.set(-0.12, 0.92, -0.32);\n\tgroup.add(leftEye);\n\tconst rightEye = new THREE.Mesh(eyeGeo, eyeMat);\n\trightEye.position.set(0.12, 0.92, -0.32);\n\tgroup.add(rightEye);\n\n\t// Feet\n\tconst footGeo = new THREE.BoxGeometry(0.15, 0.08, 0.2);\n\tconst footMat = mat(0xffa500);\n\tconst leftFoot = new THREE.Mesh(footGeo, footMat);\n\tleftFoot.position.set(-0.14, 0.04, -0.05);\n\tgroup.add(leftFoot);\n\tconst rightFoot = new THREE.Mesh(footGeo, footMat);\n\trightFoot.position.set(0.14, 0.04, -0.05);\n\tgroup.add(rightFoot);\n\n\t// Wings\n\tconst wingGeo = new THREE.BoxGeometry(0.1, 0.35, 0.4);\n\tconst wingMat = mat(0xeeeeee);\n\tconst leftWing = new THREE.Mesh(wingGeo, wingMat);\n\tleftWing.position.set(-0.32, 0.5, 0);\n\tgroup.add(leftWing);\n\tconst rightWing = new THREE.Mesh(wingGeo, wingMat);\n\trightWing.position.set(0.32, 0.5, 0);\n\tgroup.add(rightWing);\n\n\tgroup.position.set(0, 0, 0);\n\tscene.add(group);\n\treturn group;\n}\n\n// Lane types\nconst LANE_TYPES = ['grass', 'road', 'water'];\n\nfunction randomLaneType(row) {\n\tif (row <= 0) return 'grass';\n\tif (row <= 2) return Math.random() < 0.5 ? 'grass' : 'road';\n\tconst r = Math.random();\n\tif (r < 0.35) return 'grass';\n\tif (r < 0.72) return 'road';\n\treturn 'water';\n}\n\nfunction createGrassLane(row) {\n\tconst group = new THREE.Group();\n\tconst colorIdx = Math.floor(Math.random() * GRASS_COLORS.length);\n\tconst ground = new THREE.Mesh(\n\t\tnew THREE.BoxGeometry(30, 0.2, 1),\n\t\tmat(GRASS_COLORS[colorIdx])\n\t);\n\tground.position.set(0, -0.1, -row);\n\tground.receiveShadow = true;\n\tgroup.add(ground);\n\n\t// Trees\n\tconst treePositions = [];\n\tfor (let i = -7; i <= 7; i++) {\n\t\tif (Math.abs(i) <= 2 && row < 3) continue;\n\t\tif (Math.random() < 0.3) {\n\t\t\ttreePositions.push(i);\n\t\t\tconst tree = createTree();\n\t\t\ttree.position.set(i, 0, -row);\n\t\t\tgroup.add(tree);\n\t\t}\n\t}\n\n\tscene.add(group);\n\treturn {\n\t\ttype: 'grass',\n\t\trow,\n\t\tgroup,\n\t\tobstacles: treePositions,\n\t\tvehicles: [],\n\t\tlogs: []\n\t};\n}\n\nfunction createTree() {\n\tconst group = new THREE.Group();\n\tconst trunkH = 0.3 + Math.random() * 0.3;\n\tconst trunk = new THREE.Mesh(\n\t\tnew THREE.BoxGeometry(0.25, trunkH, 0.25),\n\t\tmat(0x8B4513)\n\t);\n\ttrunk.position.y = trunkH / 2;\n\ttrunk.castShadow = true;\n\tgroup.add(trunk);\n\n\tconst leafSize = 0.5 + Math.random() * 0.3;\n\tconst leaves = new THREE.Mesh(\n\t\tnew THREE.BoxGeometry(leafSize, leafSize, leafSize),\n\t\tmat(0x228B22)\n\t);\n\tleaves.position.y = trunkH + leafSize / 2 - 0.05;\n\tleaves.castShadow = true;\n\tgroup.add(leaves);\n\n\tif (Math.random() > 0.4) {\n\t\tconst topLeaves = new THREE.Mesh(\n\t\t\tnew THREE.BoxGeometry(leafSize * 0.6, leafSize * 0.5, leafSize * 0.6),\n\t\t\tmat(0x2d9e2d)\n\t\t);\n\t\ttopLeaves.position.y = trunkH + leafSize + 0.1;\n\t\ttopLeaves.castShadow = true;\n\t\tgroup.add(topLeaves);\n\t}\n\n\treturn group;\n}\n\nfunction createRoadLane(row) {\n\tconst group = new THREE.Group();\n\tconst road = new THREE.Mesh(\n\t\tnew THREE.BoxGeometry(30, 0.2, 1),\n\t\tmat(ROAD_COLOR)\n\t);\n\troad.position.set(0, -0.1, -row);\n\troad.receiveShadow = true;\n\tgroup.add(road);\n\n\t// Lane markings\n\tfor (let i = -14; i <= 14; i += 2) {\n\t\tconst marking = new THREE.Mesh(\n\t\t\tnew THREE.BoxGeometry(0.5, 0.01, 0.06),\n\t\t\tmat(0xcccccc)\n\t\t);\n\t\tmarking.position.set(i, 0.01, -row);\n\t\tgroup.add(marking);\n\t}\n\n\tconst speed = (0.02 + Math.random() * 0.04) * (Math.random() < 0.5 ? 1 : -1);\n\tconst vehicleCount = 2 + Math.floor(Math.random() * 2);\n\tconst spacing = 30 / vehicleCount;\n\tconst vehicles = [];\n\n\tfor (let i = 0; i < vehicleCount; i++) {\n\t\tconst isTruck = Math.random() < 0.35;\n\t\tconst vehicle = createVehicle(isTruck);\n\t\tvehicle.position.set(-15 + i * spacing + Math.random() * 3, 0, -row);\n\t\tif (speed < 0) vehicle.rotation.y = Math.PI;\n\t\tgroup.add(vehicle);\n\t\tvehicles.push({\n\t\t\tmesh: vehicle,\n\t\t\tspeed,\n\t\t\twidth: isTruck ? 2.0 : 1.2\n\t\t});\n\t}\n\n\tscene.add(group);\n\treturn {\n\t\ttype: 'road',\n\t\trow,\n\t\tgroup,\n\t\tobstacles: [],\n\t\tvehicles,\n\t\tlogs: []\n\t};\n}\n\nfunction createVehicle(isTruck) {\n\tconst group = new THREE.Group();\n\tconst colors = [0xe74c3c, 0x3498db, 0xf39c12, 0x9b59b6, 0x1abc9c, 0xe67e22, 0x2ecc71];\n\tconst color = colors[Math.floor(Math.random() * colors.length)];\n\n\tif (isTruck) {\n\t\tconst cab = new THREE.Mesh(new THREE.BoxGeometry(0.6, 0.5, 0.7), mat(color));\n\t\tcab.position.set(0.5, 0.35, 0);\n\t\tcab.castShadow = true;\n\t\tgroup.add(cab);\n\n\t\tconst bed = new THREE.Mesh(new THREE.BoxGeometry(1.3, 0.4, 0.75), mat(0x777777));\n\t\tbed.position.set(-0.3, 0.3, 0);\n\t\tbed.castShadow = true;\n\t\tgroup.add(bed);\n\n\t\tconst wheel1 = new THREE.Mesh(new THREE.BoxGeometry(0.2, 0.2, 0.8), mat(0x222222));\n\t\twheel1.position.set(0.6, 0.1, 0);\n\t\tgroup.add(wheel1);\n\t\tconst wheel2 = new THREE.Mesh(new THREE.BoxGeometry(0.2, 0.2, 0.8), mat(0x222222));\n\t\twheel2.position.set(-0.6, 0.1, 0);\n\t\tgroup.add(wheel2);\n\t} else {\n\t\tconst body = new THREE.Mesh(new THREE.BoxGeometry(0.9, 0.3, 0.6), mat(color));\n\t\tbody.position.y = 0.25;\n\t\tbody.castShadow = true;\n\t\tgroup.add(body);\n\n\t\tconst roof = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.22, 0.55), mat(darkenColor(color, 0.8)));\n\t\troof.position.set(0.05, 0.52, 0);\n\t\troof.castShadow = true;\n\t\tgroup.add(roof);\n\n\t\tconst windshield = new THREE.Mesh(new THREE.BoxGeometry(0.02, 0.18, 0.48), mat(0xaaddff));\n\t\twindshield.position.set(0.26, 0.5, 0);\n\t\tgroup.add(windshield);\n\n\t\tconst wheel1 = new THREE.Mesh(new THREE.BoxGeometry(0.15, 0.15, 0.65), mat(0x222222));\n\t\twheel1.position.set(0.3, 0.08, 0);\n\t\tgroup.add(wheel1);\n\t\tconst wheel2 = new THREE.Mesh(new THREE.BoxGeometry(0.15, 0.15, 0.65), mat(0x222222));\n\t\twheel2.position.set(-0.3, 0.08, 0);\n\t\tgroup.add(wheel2);\n\t}\n\n\treturn group;\n}\n\nfunction darkenColor(hex, factor) {\n\tconst r = ((hex >> 16) & 255) * factor;\n\tconst g = ((hex >> 8) & 255) * factor;\n\tconst b = (hex & 255) * factor;\n\treturn (Math.floor(r) << 16) | (Math.floor(g) << 8) | Math.floor(b);\n}\n\nfunction createWaterLane(row) {\n\tconst group = new THREE.Group();\n\tconst water = new THREE.Mesh(\n\t\tnew THREE.BoxGeometry(30, 0.2, 1),\n\t\tmat(WATER_COLOR)\n\t);\n\twater.position.set(0, -0.15, -row);\n\twater.receiveShadow = true;\n\tgroup.add(water);\n\n\tconst speed = (0.015 + Math.random() * 0.025) * (Math.random() < 0.5 ? 1 : -1);\n\tconst logCount = 3 + Math.floor(Math.random() * 2);\n\tconst spacing = 30 / logCount;\n\tconst logs = [];\n\n\tfor (let i = 0; i < logCount; i++) {\n\t\tconst logLen = 1.5 + Math.random() * 2;\n\t\tconst log = createLog(logLen);\n\t\tlog.position.set(-15 + i * spacing + Math.random() * 3, -0.02, -row);\n\t\tgroup.add(log);\n\t\tlogs.push({\n\t\t\tmesh: log,\n\t\t\tspeed,\n\t\t\twidth: logLen\n\t\t});\n\t}\n\n\tscene.add(group);\n\treturn {\n\t\ttype: 'water',\n\t\trow,\n\t\tgroup,\n\t\tobstacles: [],\n\t\tvehicles: [],\n\t\tlogs\n\t};\n}\n\nfunction createLog(len) {\n\tconst group = new THREE.Group();\n\tconst main = new THREE.Mesh(\n\t\tnew THREE.BoxGeometry(len, 0.25, 0.6),\n\t\tmat(0x8B6914)\n\t);\n\tmain.position.y = 0.12;\n\tmain.castShadow = true;\n\tgroup.add(main);\n\n\tconst bark = new THREE.Mesh(\n\t\tnew THREE.BoxGeometry(len * 0.9, 0.05, 0.65),\n\t\tmat(0x6B4E0A)\n\t);\n\tbark.position.y = 0.26;\n\tgroup.add(bark);\n\n\treturn group;\n}\n\n// Lane management\nconst LANE_BUFFER = 25;\n\nfunction generateLanes() {\n\tfor (let r = -3; r <= LANE_BUFFER; r++) {\n\t\taddLane(r);\n\t}\n}\n\nfunction addLane(row) {\n\tconst existing = lanes.find(l => l.row === row);\n\tif (existing) return;\n\n\tconst type = randomLaneType(row);\n\tlet lane;\n\tswitch (type) {\n\t\tcase 'grass':\n\t\t\tlane = createGrassLane(row);\n\t\t\tbreak;\n\t\tcase 'road':\n\t\t\tlane = createRoadLane(row);\n\t\t\tbreak;\n\t\tcase 'water':\n\t\t\tlane = createWaterLane(row);\n\t\t\tbreak;\n\t}\n\tlanes.push(lane);\n}\n\nfunction cleanupLanes() {\n\tlanes = lanes.filter(l => {\n\t\tif (l.row < playerRow - 8) {\n\t\t\tscene.remove(l.group);\n\t\t\tl.group.traverse(child => {\n\t\t\t\tif (child.geometry) child.geometry.dispose();\n\t\t\t\tif (child.material) child.material.dispose();\n\t\t\t});\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t});\n}\n\nfunction ensureLanes() {\n\tconst maxNeeded = playerRow + LANE_BUFFER;\n\tfor (let r = playerRow - 3; r <= maxNeeded; r++) {\n\t\taddLane(r);\n\t}\n\tcleanupLanes();\n}\n\n// Camera\nfunction updateCamera() {\n\tconst targetX = player.position.x * 0.3;\n\tconst targetZ = player.position.z - 6;\n\tcamera.position.x += (targetX + 5 - camera.position.x) * 0.08;\n\tcamera.position.y += (9 - camera.position.y) * 0.08;\n\tcamera.position.z += (targetZ + 8 - camera.position.z) * 0.08;\n\tcamera.lookAt(player.position.x * 0.5, 0, player.position.z - 2);\n}\n\n// Movement\nfunction tryMove(dRow, dCol) {\n\tif (isHopping || isDead || gameState !== 'playing') return;\n\n\tconst targetRow = playerRow + dRow;\n\tconst targetCol = playerCol + dCol;\n\n\tif (targetCol < -6 || targetCol > 6) return;\n\n\t// Check tree collision\n\tconst targetLane = lanes.find(l => l.row === targetRow);\n\tif (targetLane && targetLane.type === 'grass') {\n\t\tif (targetLane.obstacles.includes(targetCol)) return;\n\t}\n\n\thopFrom = {\n\t\tx: player.position.x,\n\t\tz: player.position.z\n\t};\n\thopTo = {\n\t\tx: targetCol,\n\t\tz: -targetRow\n\t};\n\thopProgress = 0;\n\tisHopping = true;\n\n\tif (dRow > 0) hopDir = 'forward';\n\telse if (dRow < 0) hopDir = 'back';\n\telse if (dCol > 0) hopDir = 'right';\n\telse hopDir = 'left';\n\n\t// Rotate chicken\n\tif (dRow > 0) player.rotation.y = 0;\n\telse if (dRow < 0) player.rotation.y = Math.PI;\n\telse if (dCol > 0) player.rotation.y = -Math.PI / 2;\n\telse if (dCol < 0) player.rotation.y = Math.PI / 2;\n\n\tplayerRow = targetRow;\n\tplayerCol = targetCol;\n\n\thopSound();\n\n\tif (playerRow > maxRow) {\n\t\tmaxRow = playerRow;\n\t\tscore = maxRow;\n\t\tscoreEl.textContent = score;\n\t\tif (score > 0 && score % 5 === 0) scoreSound();\n\t}\n\n\tensureLanes();\n}\n\nfunction updateHop(dt) {\n\tif (!isHopping) return;\n\thopProgress += dt * 5.5;\n\tif (hopProgress >= 1) {\n\t\thopProgress = 1;\n\t\tisHopping = false;\n\t}\n\n\tconst t = hopProgress;\n\tconst ease = t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;\n\tplayer.position.x = hopFrom.x + (hopTo.x - hopFrom.x) * ease;\n\tplayer.position.z = hopFrom.z + (hopTo.z - hopFrom.z) * ease;\n\n\t// Hop arc\n\tconst hopHeight = Math.sin(t * Math.PI) * 0.4;\n\tplayer.position.y = hopHeight;\n}\n\n// Collision detection\nfunction checkCollisions() {\n\tif (isDead) return;\n\n\tconst lane = lanes.find(l => l.row === playerRow);\n\tif (!lane) return;\n\n\tconst px = player.position.x;\n\n\tif (lane.type === 'road') {\n\t\tfor (const v of lane.vehicles) {\n\t\t\tconst vx = v.mesh.position.x;\n\t\t\tconst hw = v.width / 2;\n\t\t\tif (px > vx - hw - 0.2 && px < vx + hw + 0.2) {\n\t\t\t\tdie('road');\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}\n\n\tif (lane.type === 'water' && !isHopping) {\n\t\tlet onLog = false;\n\t\tfor (const log of lane.logs) {\n\t\t\tconst lx = log.mesh.position.x;\n\t\t\tconst hw = log.width / 2;\n\t\t\tif (px > lx - hw + 0.1 && px < lx + hw - 0.1) {\n\t\t\t\tonLog = true;\n\t\t\t\tplayer.position.x += log.speed;\n\t\t\t\tplayerCol = Math.round(player.position.x);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tif (!onLog) {\n\t\t\tdie('water');\n\t\t}\n\t}\n}\n\nfunction die(cause) {\n\tisDead = true;\n\tif (cause === 'water') {\n\t\tsplashSound();\n\t\t// Sink\n\t\tplayer.position.y = -0.3;\n\t} else {\n\t\tdeathSound();\n\t\t// Squish\n\t\tplayer.scale.y = 0.15;\n\t\tplayer.position.y = 0;\n\t}\n\n\tsetTimeout(() => {\n\t\tif (score > highScore) {\n\t\t\thighScore = score;\n\t\t\tlocalStorage.setItem('crossyHighScore', highScore.toString());\n\t\t\thighScoreEl.textContent = `BEST: ${highScore}`;\n\t\t}\n\t\tfinalScoreEl.textContent = `Score: ${score}`;\n\t\tgameOverScreen.classList.remove('hidden');\n\t\tgameState = 'gameover';\n\t}, 800);\n}\n\n// Update vehicles & logs\nfunction updateMovingObjects() {\n\tfor (const lane of lanes) {\n\t\tfor (const v of lane.vehicles) {\n\t\t\tv.mesh.position.x += v.speed;\n\t\t\tif (v.speed > 0 && v.mesh.position.x > 16) v.mesh.position.x = -16;\n\t\t\tif (v.speed < 0 && v.mesh.position.x < -16) v.mesh.position.x = 16;\n\t\t}\n\t\tfor (const log of lane.logs) {\n\t\t\tlog.mesh.position.x += log.speed;\n\t\t\tif (log.speed > 0 && log.mesh.position.x > 16) log.mesh.position.x = -16;\n\t\t\tif (log.speed < 0 && log.mesh.position.x < -16) log.mesh.position.x = 16;\n\t\t}\n\t}\n}\n\n// Input\ndocument.addEventListener('keydown', (e) => {\n\tif (gameState !== 'playing') return;\n\tswitch (e.key) {\n\t\tcase 'ArrowUp':\n\t\tcase 'w':\n\t\tcase 'W':\n\t\t\ttryMove(1, 0);\n\t\t\tbreak;\n\t\tcase 'ArrowDown':\n\t\tcase 's':\n\t\tcase 'S':\n\t\t\ttryMove(-1, 0);\n\t\t\tbreak;\n\t\tcase 'ArrowLeft':\n\t\tcase 'a':\n\t\tcase 'A':\n\t\t\ttryMove(0, -1);\n\t\t\tbreak;\n\t\tcase 'ArrowRight':\n\t\tcase 'd':\n\t\tcase 'D':\n\t\t\ttryMove(0, 1);\n\t\t\tbreak;\n\t}\n});\n\n// Touch/swipe\nlet touchStartX = 0,\n\ttouchStartY = 0;\ncanvas.addEventListener('touchstart', (e) => {\n\ttouchStartX = e.touches[0].clientX;\n\ttouchStartY = e.touches[0].clientY;\n}, {\n\tpassive: true\n});\n\ncanvas.addEventListener('touchend', (e) => {\n\tif (gameState !== 'playing') return;\n\tconst dx = e.changedTouches[0].clientX - touchStartX;\n\tconst dy = e.changedTouches[0].clientY - touchStartY;\n\tconst absDx = Math.abs(dx);\n\tconst absDy = Math.abs(dy);\n\n\tif (absDx < 15 && absDy < 15) {\n\t\ttryMove(1, 0);\n\t\treturn;\n\t}\n\n\tif (absDx > absDy) {\n\t\ttryMove(0, dx > 0 ? 1 : -1);\n\t} else {\n\t\ttryMove(dy < 0 ? 1 : -1, 0);\n\t}\n}, {\n\tpassive: true\n});\n\n// Game init\nfunction resetGame() {\n\t// Clear existing\n\tfor (const lane of lanes) {\n\t\tscene.remove(lane.group);\n\t\tlane.group.traverse(child => {\n\t\t\tif (child.geometry) child.geometry.dispose();\n\t\t\tif (child.material) child.material.dispose();\n\t\t});\n\t}\n\tlanes = [];\n\tif (player) scene.remove(player);\n\n\tscore = 0;\n\tplayerRow = 0;\n\tplayerCol = 0;\n\tmaxRow = 0;\n\tisHopping = false;\n\tisDead = false;\n\tscoreEl.textContent = '0';\n\n\tplayer = createPlayer();\n\tplayer.position.set(0, 0, 0);\n\n\tgenerateLanes();\n\tupdateCamera();\n\tcamera.position.set(5, 9, 8);\n}\n\nfunction startGame() {\n\tif (audioCtx.state === 'suspended') audioCtx.resume();\n\tresetGame();\n\tstartScreen.classList.add('hidden');\n\tgameOverScreen.classList.add('hidden');\n\tgameState = 'playing';\n}\n\nstartBtn.addEventListener('click', startGame);\nretryBtn.addEventListener('click', startGame);\n\n// Main loop\nlet lastTime = 0;\n\nfunction animate(time) {\n\trequestAnimationFrame(animate);\n\tconst dt = Math.min((time - lastTime) / 1000, 0.05);\n\tlastTime = time;\n\n\tif (gameState === 'playing') {\n\t\tupdateHop(dt);\n\t\tupdateMovingObjects();\n\t\tcheckCollisions();\n\t\tupdateCamera();\n\t\tdirLight.position.set(player.position.x + 10, 20, player.position.z + 10);\n\t\tdirLight.target.position.set(player.position.x, 0, player.position.z);\n\t\tdirLight.target.updateMatrixWorld();\n\t}\n\n\trenderer.render(scene, camera);\n}\n\nrequestAnimationFrame(animate);\n\n// Resize\nwindow.addEventListener('resize', () => {\n\tcamera.aspect = window.innerWidth / window.innerHeight;\n\tcamera.updateProjectionMatrix();\n\trenderer.setSize(window.innerWidth, window.innerHeight);\n});\n\n// Initial scene for menu\nresetGame();\ncamera.position.set(5, 9, 8);\ncamera.lookAt(0, 0, -2);"},{"name":"style.css","content":":root {\n\tcolor-scheme: dark;\n\tfont-family: 'Courier New', monospace;\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: #1a1a2e;\n}\n\n#game-container {\n\tposition: relative;\n\twidth: 100%;\n\theight: 100vh;\n}\n\n#game-canvas {\n\tdisplay: block;\n\twidth: 100%;\n\theight: 100%;\n}\n\n#ui-overlay {\n\tposition: absolute;\n\ttop: 20px;\n\tleft: 0;\n\tright: 0;\n\ttext-align: center;\n\tpointer-events: none;\n\tz-index: 10;\n}\n\n#score {\n\tfont-size: 72px;\n\tfont-weight: 900;\n\tcolor: #fff;\n\ttext-shadow:\n\t\t3px 3px 0 #000,\n\t\t-1px -1px 0 #000,\n\t\t1px -1px 0 #000,\n\t\t-1px 1px 0 #000;\n\tletter-spacing: 2px;\n}\n\n#high-score {\n\tfont-size: 16px;\n\tfont-weight: 700;\n\tcolor: #ffcc00;\n\ttext-shadow: 2px 2px 0 #000;\n\tmargin-top: 4px;\n}\n\n#start-screen,\n#game-over {\n\tposition: absolute;\n\tinset: 0;\n\tdisplay: flex;\n\tflex-direction: column;\n\talign-items: center;\n\tjustify-content: center;\n\tbackground: rgba(10, 10, 30, 0.85);\n\tz-index: 20;\n}\n\n#start-screen h1 {\n\tfont-size: 64px;\n\tfont-weight: 900;\n\tcolor: #ffcc00;\n\ttext-shadow: 4px 4px 0 #b8860b, 6px 6px 0 #000;\n\ttext-align: center;\n\tline-height: 1.1;\n\tmargin-bottom: 20px;\n}\n\n#start-screen p {\n\tfont-size: 16px;\n\tcolor: #aaa;\n\tmargin-bottom: 30px;\n}\n\n#game-over h2 {\n\tfont-size: 56px;\n\tfont-weight: 900;\n\tcolor: #ff4444;\n\ttext-shadow: 3px 3px 0 #8b0000, 5px 5px 0 #000;\n\tmargin-bottom: 10px;\n}\n\n#game-over #final-score {\n\tfont-size: 24px;\n\tcolor: #fff;\n\tmargin-bottom: 30px;\n}\n\nbutton {\n\tfont-family: 'Courier New', monospace;\n\tfont-size: 22px;\n\tfont-weight: 900;\n\tpadding: 14px 48px;\n\tborder: none;\n\tborder-radius: 4px;\n\tcursor: pointer;\n\ttext-transform: uppercase;\n\tletter-spacing: 2px;\n\ttransition: transform 0.1s;\n}\n\nbutton:active {\n\ttransform: scale(0.95);\n}\n\n#start-btn {\n\tbackground: #ffcc00;\n\tcolor: #1a1a2e;\n\tbox-shadow: 0 4px 0 #b8860b;\n}\n\n#retry-btn {\n\tbackground: #ff4444;\n\tcolor: #fff;\n\tbox-shadow: 0 4px 0 #8b0000;\n}\n\n.hidden {\n\tdisplay: none !important;\n}"}],"folders":[]},"variants":null,"createdAt":"2026-03-12T21:22:53.212Z","updatedAt":"2026-03-12T21:23:00.911Z","hasThumbnail":true}}