Spaces:
Running
Running
| <html lang="fr"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Hytale - Voxel Adventure</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/simplex-noise/2.4.0/simplex-noise.min.js"></script> | |
| <link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;700;900&family=Inter:wght@300;400;600&display=swap" rel="stylesheet"> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| overflow: hidden; | |
| font-family: 'Inter', sans-serif; | |
| background: #0a0a0f; | |
| } | |
| .hytale-font { | |
| font-family: 'Cinzel', serif; | |
| } | |
| #game-container { | |
| position: relative; | |
| width: 100vw; | |
| height: 100vh; | |
| } | |
| #canvas-container { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: linear-gradient(to bottom, #1a1a2e 0%, #16213e 50%, #0f3460 100%); | |
| } | |
| .glass-panel { | |
| background: rgba(20, 20, 35, 0.85); | |
| backdrop-filter: blur(12px); | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); | |
| } | |
| .hytale-btn { | |
| background: linear-gradient(135deg, #e94560 0%, #c23a51 100%); | |
| border: 2px solid #ff6b6b; | |
| color: white; | |
| text-transform: uppercase; | |
| letter-spacing: 2px; | |
| font-weight: 700; | |
| transition: all 0.3s ease; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .hytale-btn:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 10px 20px rgba(233, 69, 96, 0.4); | |
| border-color: #ff8585; | |
| } | |
| .hytale-btn::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: -100%; | |
| width: 100%; | |
| height: 100%; | |
| background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent); | |
| transition: left 0.5s; | |
| } | |
| .hytale-btn:hover::before { | |
| left: 100%; | |
| } | |
| .hytale-btn.secondary { | |
| background: linear-gradient(135deg, #533483 0%, #3d2463 100%); | |
| border-color: #7b68ee; | |
| } | |
| .hytale-btn.secondary:hover { | |
| box-shadow: 0 10px 20px rgba(83, 52, 131, 0.4); | |
| } | |
| #crosshair { | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| width: 20px; | |
| height: 20px; | |
| pointer-events: none; | |
| opacity: 0; | |
| transition: opacity 0.3s; | |
| } | |
| #crosshair::before, | |
| #crosshair::after { | |
| content: ''; | |
| position: absolute; | |
| background: rgba(255, 255, 255, 0.9); | |
| } | |
| #crosshair::before { | |
| width: 2px; | |
| height: 20px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| } | |
| #crosshair::after { | |
| width: 20px; | |
| height: 2px; | |
| top: 50%; | |
| transform: translateY(-50%); | |
| } | |
| .hotbar { | |
| position: absolute; | |
| bottom: 30px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| display: flex; | |
| gap: 10px; | |
| opacity: 0; | |
| transition: opacity 0.3s; | |
| } | |
| .hotbar-slot { | |
| width: 60px; | |
| height: 60px; | |
| background: rgba(0, 0, 0, 0.6); | |
| border: 3px solid #444; | |
| border-radius: 8px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| position: relative; | |
| } | |
| .hotbar-slot.active { | |
| border-color: #e94560; | |
| transform: scale(1.1); | |
| box-shadow: 0 0 20px rgba(233, 69, 96, 0.5); | |
| } | |
| .hotbar-slot:hover { | |
| border-color: #666; | |
| } | |
| .block-icon { | |
| width: 40px; | |
| height: 40px; | |
| border-radius: 4px; | |
| } | |
| #block-highlight { | |
| position: absolute; | |
| border: 2px solid rgba(255, 255, 255, 0.5); | |
| pointer-events: none; | |
| display: none; | |
| box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.5); | |
| } | |
| .loading-screen { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: #0a0a0f; | |
| z-index: 9999; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| transition: opacity 0.5s; | |
| } | |
| .loading-bar { | |
| width: 300px; | |
| height: 4px; | |
| background: rgba(255, 255, 255, 0.1); | |
| border-radius: 2px; | |
| overflow: hidden; | |
| margin-top: 20px; | |
| } | |
| .loading-progress { | |
| height: 100%; | |
| background: linear-gradient(90deg, #e94560, #ff6b6b); | |
| width: 0%; | |
| transition: width 0.3s; | |
| box-shadow: 0 0 10px rgba(233, 69, 96, 0.5); | |
| } | |
| .title-glow { | |
| text-shadow: 0 0 20px rgba(233, 69, 96, 0.5), 0 0 40px rgba(233, 69, 96, 0.3); | |
| } | |
| .floating { | |
| animation: float 6s ease-in-out infinite; | |
| } | |
| @keyframes float { | |
| 0%, 100% { transform: translateY(0px); } | |
| 50% { transform: translateY(-20px); } | |
| } | |
| .particle { | |
| position: absolute; | |
| width: 4px; | |
| height: 4px; | |
| background: rgba(255, 255, 255, 0.5); | |
| border-radius: 50%; | |
| pointer-events: none; | |
| } | |
| #anycoder-link { | |
| position: absolute; | |
| top: 20px; | |
| right: 20px; | |
| color: rgba(255, 255, 255, 0.6); | |
| text-decoration: none; | |
| font-size: 0.9rem; | |
| transition: color 0.3s; | |
| z-index: 100; | |
| } | |
| #anycoder-link:hover { | |
| color: #e94560; | |
| } | |
| .controls-hint { | |
| position: absolute; | |
| bottom: 100px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| color: rgba(255, 255, 255, 0.6); | |
| font-size: 0.9rem; | |
| text-align: center; | |
| opacity: 0; | |
| transition: opacity 0.3s; | |
| } | |
| .inventory-grid { | |
| display: grid; | |
| grid-template-columns: repeat(9, 1fr); | |
| gap: 10px; | |
| padding: 20px; | |
| } | |
| .health-bar { | |
| position: absolute; | |
| bottom: 110px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| display: flex; | |
| gap: 5px; | |
| opacity: 0; | |
| transition: opacity 0.3s; | |
| } | |
| .heart { | |
| width: 24px; | |
| height: 24px; | |
| background: #e94560; | |
| transform: rotate(45deg); | |
| position: relative; | |
| box-shadow: 0 0 10px rgba(233, 69, 96, 0.5); | |
| } | |
| .heart::before, | |
| .heart::after { | |
| content: ''; | |
| width: 24px; | |
| height: 24px; | |
| position: absolute; | |
| background: #e94560; | |
| border-radius: 50%; | |
| } | |
| .heart::before { | |
| left: -12px; | |
| } | |
| .heart::after { | |
| top: -12px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="game-container"> | |
| <canvas id="canvas-container"></canvas> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" id="anycoder-link">Built with anycoder</a> | |
| <div id="crosshair"></div> | |
| <div class="health-bar" id="health-bar"> | |
| <div class="heart"></div> | |
| <div class="heart"></div> | |
| <div class="heart"></div> | |
| <div class="heart"></div> | |
| <div class="heart"></div> | |
| <div class="heart"></div> | |
| <div class="heart"></div> | |
| <div class="heart"></div> | |
| <div class="heart"></div> | |
| <div class="heart"></div> | |
| </div> | |
| <div class="hotbar" id="hotbar"> | |
| <div class="hotbar-slot active" data-block="grass"> | |
| <div class="block-icon" style="background: linear-gradient(135deg, #567d46, #3d5c2d);"></div> | |
| <span style="position: absolute; bottom: 2px; left: 4px; font-size: 10px; color: white; font-weight: bold;">1</span> | |
| </div> | |
| <div class="hotbar-slot" data-block="dirt"> | |
| <div class="block-icon" style="background: linear-gradient(135deg, #8b4513, #654321);"></div> | |
| <span style="position: absolute; bottom: 2px; left: 4px; font-size: 10px; color: white; font-weight: bold;">2</span> | |
| </div> | |
| <div class="hotbar-slot" data-block="stone"> | |
| <div class="block-icon" style="background: linear-gradient(135deg, #808080, #606060);"></div> | |
| <span style="position: absolute; bottom: 2px; left: 4px; font-size: 10px; color: white; font-weight: bold;">3</span> | |
| </div> | |
| <div class="hotbar-slot" data-block="wood"> | |
| <div class="block-icon" style="background: linear-gradient(135deg, #8b5a2b, #6b4423);"></div> | |
| <span style="position: absolute; bottom: 2px; left: 4px; font-size: 10px; color: white; font-weight: bold;">4</span> | |
| </div> | |
| <div class="hotbar-slot" data-block="leaves"> | |
| <div class="block-icon" style="background: linear-gradient(135deg, #228b22, #196f19);"></div> | |
| <span style="position: absolute; bottom: 2px; left: 4px; font-size: 10px; color: white; font-weight: bold;">5</span> | |
| </div> | |
| <div class="hotbar-slot" data-block="sand"> | |
| <div class="block-icon" style="background: linear-gradient(135deg, #f4a460, #d2691e);"></div> | |
| <span style="position: absolute; bottom: 2px; left: 4px; font-size: 10px; color: white; font-weight: bold;">6</span> | |
| </div> | |
| <div class="hotbar-slot" data-block="water"> | |
| <div class="block-icon" style="background: linear-gradient(135deg, #4682b4, #2f5680);"></div> | |
| <span style="position: absolute; bottom: 2px; left: 4px; font-size: 10px; color: white; font-weight: bold;">7</span> | |
| </div> | |
| <div class="hotbar-slot" data-block="gold"> | |
| <div class="block-icon" style="background: linear-gradient(135deg, #ffd700, #daa520);"></div> | |
| <span style="position: absolute; bottom: 2px; left: 4px; font-size: 10px; color: white; font-weight: bold;">8</span> | |
| </div> | |
| <div class="hotbar-slot" data-block="diamond"> | |
| <div class="block-icon" style="background: linear-gradient(135deg, #00ffff, #00ced1);"></div> | |
| <span style="position: absolute; bottom: 2px; left: 4px; font-size: 10px; color: white; font-weight: bold;">9</span> | |
| </div> | |
| </div> | |
| <div class="controls-hint" id="controls-hint"> | |
| <p>Click to break/place blocks • WASD to move • SPACE to jump</p> | |
| </div> | |
| <div id="main-menu" class="absolute inset-0 flex items-center justify-center z-50"> | |
| <div class="glass-panel p-12 rounded-2xl max-w-2xl w-full mx-4 floating"> | |
| <div class="text-center mb-8"> | |
| <h1 class="hytale-font text-6xl font-black text-white mb-2 title-glow tracking-wider">HYTALE</h1> | |
| <p class="text-gray-400 text-lg tracking-widest uppercase">Voxel Adventure Engine</p> | |
| </div> | |
| <div class="space-y-4"> | |
| <button onclick="startGame()" class="hytale-btn w-full py-4 rounded-lg text-lg font-bold"> | |
| Play Solo | |
| </button> | |
| <button class="hytale-btn secondary w-full py-4 rounded-lg text-lg font-bold"> | |
| Multiplayer | |
| </button> | |
| <div class="grid grid-cols-2 gap-4 mt-6"> | |
| <button class="hytale-btn secondary py-3 rounded-lg text-sm"> | |
| Settings | |
| </button> | |
| <button class="hytale-btn secondary py-3 rounded-lg text-sm"> | |
| Mods | |
| </button> | |
| </div> | |
| </div> | |
| <div class="mt-8 pt-6 border-t border-gray-700"> | |
| <div class="flex justify-between text-sm text-gray-500"> | |
| <span>Version 1.0.0</span> | |
| <span>Not affiliated with Hypixel Studios</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="loading-screen" id="loading-screen" style="display: none;"> | |
| <h2 class="hytale-font text-4xl text-white mb-4">Generating World...</h2> | |
| <div class="loading-bar"> | |
| <div class="loading-progress" id="loading-progress"></div> | |
| </div> | |
| <p class="text-gray-400 mt-4" id="loading-text">Preparing chunks...</p> | |
| </div> | |
| <div id="pause-menu" class="absolute inset-0 flex items-center justify-center z-50 hidden" style="background: rgba(0,0,0,0.7);"> | |
| <div class="glass-panel p-8 rounded-2xl max-w-md w-full mx-4"> | |
| <h2 class="hytale-font text-3xl text-white text-center mb-6">Game Menu</h2> | |
| <div class="space-y-3"> | |
| <button onclick="resumeGame()" class="hytale-btn w-full py-3 rounded-lg">Resume</button> | |
| <button onclick="returnToMenu()" class="hytale-btn secondary w-full py-3 rounded-lg">Save & Quit</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Game State | |
| let scene, camera, renderer; | |
| let world = {}; | |
| let chunks = {}; | |
| let player = { | |
| x: 0, | |
| y: 30, | |
| z: 0, | |
| vx: 0, | |
| vy: 0, | |
| vz: 0, | |
| onGround: false, | |
| height: 1.8, | |
| width: 0.6 | |
| }; | |
| let keys = {}; | |
| let selectedBlock = 'grass'; | |
| let isPaused = true; | |
| let isLocked = false; | |
| let raycaster = new THREE.Raycaster(); | |
| let mouse = new THREE.Vector2(); | |
| let highlightMesh; | |
| let simplex = new SimplexNoise(); | |
| const BLOCK_SIZE = 1; | |
| const CHUNK_SIZE = 16; | |
| const RENDER_DISTANCE = 4; | |
| const blockTypes = { | |
| grass: { color: 0x567d46, name: 'Grass' }, | |
| dirt: { color: 0x8b4513, name: 'Dirt' }, | |
| stone: { color: 0x808080, name: 'Stone' }, | |
| wood: { color: 0x8b5a2b, name: 'Wood' }, | |
| leaves: { color: 0x228b22, name: 'Leaves', transparent: true }, | |
| sand: { color: 0xf4a460, name: 'Sand' }, | |
| water: { color: 0x4682b4, name: 'Water', transparent: true }, | |
| gold: { color: 0xffd700, name: 'Gold Ore' }, | |
| diamond: { color: 0x00ffff, name: 'Diamond' } | |
| }; | |
| let materials = {}; | |
| let geometry = new THREE.BoxGeometry(BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE); | |
| // Initialize Three.js | |
| function init() { | |
| const canvas = document.getElementById('canvas-container'); | |
| scene = new THREE.Scene(); | |
| scene.fog = new THREE.Fog(0x1a1a2e, 10, 50); | |
| camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); | |
| camera.position.set(player.x, player.y, player.z); | |
| renderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: true }); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| renderer.setPixelRatio(window.devicePixelRatio); | |
| renderer.shadowMap.enabled = true; | |
| renderer.shadowMap.type = THREE.PCFSoftShadowMap; | |
| // Lighting | |
| const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); | |
| scene.add(ambientLight); | |
| const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); | |
| directionalLight.position.set(50, 100, 50); | |
| directionalLight.castShadow = true; | |
| directionalLight.shadow.camera.left = -50; | |
| directionalLight.shadow.camera.right = 50; | |
| directionalLight.shadow.camera.top = 50; | |
| directionalLight.shadow.camera.bottom = -50; | |
| directionalLight.shadow.camera.near = 0.1; | |
| directionalLight.shadow.camera.far = 200; | |
| scene.add(directionalLight); | |
| // Create materials | |
| for (let key in blockTypes) { | |
| materials[key] = new THREE.MeshLambertMaterial({ | |
| color: blockTypes[key].color, | |
| transparent: blockTypes[key].transparent || false, | |
| opacity: blockTypes[key].transparent ? 0.8 : 1.0 | |
| }); | |
| } | |
| // Highlight mesh | |
| const highlightGeo = new THREE.BoxGeometry(BLOCK_SIZE + 0.05, BLOCK_SIZE + 0.05, BLOCK_SIZE + 0.05); | |
| const highlightMat = new THREE.MeshBasicMaterial({ | |
| color: 0xffffff, | |
| wireframe: true, | |
| transparent: true, | |
| opacity: 0.5 | |
| }); | |
| highlightMesh = new THREE.Mesh(highlightGeo, highlightMat); | |
| scene.add(highlightMesh); | |
| highlightMesh.visible = false; | |
| // Event listeners | |
| window.addEventListener('resize', onWindowResize); | |
| document.addEventListener('keydown', onKeyDown); | |
| document.addEventListener('keyup', onKeyUp); | |
| document.addEventListener('mousedown', onMouseDown); | |
| document.addEventListener('mousemove', onMouseMove); | |
| document.addEventListener('pointerlockchange', onPointerLockChange); | |
| // Hotbar selection | |
| document.querySelectorAll('.hotbar-slot').forEach((slot, index) => { | |
| slot.addEventListener('click', () => selectSlot(index)); | |
| }); | |
| window.addEventListener('wheel', (e) => { | |
| const slots = document.querySelectorAll('.hotbar-slot'); | |
| let current = Array.from(slots).findIndex(s => s.classList.contains('active')); | |
| let next = current + (e.deltaY > 0 ? 1 : -1); | |
| if (next < 0) next = slots.length - 1; | |
| if (next >= slots.length) next = 0; | |
| selectSlot(next); | |
| }); | |
| } | |
| function selectSlot(index) { | |
| document.querySelectorAll('.hotbar-slot').forEach(s => s.classList.remove('active')); | |
| const slots = document.querySelectorAll('.hotbar-slot'); | |
| slots[index].classList.add('active'); | |
| selectedBlock = slots[index].dataset.block; | |
| } | |
| // World Generation | |
| function generateChunk(cx, cz) { | |
| const chunkKey = `${cx},${cz}`; | |
| if (chunks[chunkKey]) return; | |
| const chunk = new THREE.Group(); | |
| chunk.position.set(cx * CHUNK_SIZE * BLOCK_SIZE, 0, cz * CHUNK_SIZE * BLOCK_SIZE); | |
| for (let x = 0; x < CHUNK_SIZE; x++) { | |
| for (let z = 0; z < CHUNK_SIZE; z++) { | |
| const worldX = cx * CHUNK_SIZE + x; | |
| const worldZ = cz * CHUNK_SIZE + z; | |
| // Terrain height using noise | |
| const noise = simplex.noise2D(worldX * 0.05, worldZ * 0.05); | |
| const height = Math.floor(20 + noise * 10); | |
| // Generate column | |
| for (let y = 0; y <= height; y++) { | |
| let type = 'stone'; | |
| if (y === height) type = 'grass'; | |
| else if (y > height - 3) type = 'dirt'; | |
| // Caves | |
| if (y < height - 5) { | |
| const caveNoise = simplex.noise3D(worldX * 0.1, y * 0.1, worldZ * 0.1); | |
| if (caveNoise > 0.7) continue; | |
| } | |
| // Ores | |
| if (type === 'stone' && y < height - 10) { | |
| const oreNoise = simplex.noise3D(worldX * 0.2, y * 0.2, worldZ * 0.2); | |
| if (oreNoise > 0.8) type = 'gold'; | |
| else if (oreNoise < -0.8) type = 'diamond'; | |
| } | |
| createBlock(worldX, y, worldZ, type, chunk); | |
| } | |
| // Trees | |
| if (Math.random() < 0.02 && height > 15) { | |
| generateTree(worldX, height + 1, worldZ, chunk); | |
| } | |
| // Water | |
| if (height < 18) { | |
| for (let wy = height + 1; wy <= 18; wy++) { | |
| createBlock(worldX, wy, worldZ, 'water', chunk); | |
| } | |
| } | |
| } | |
| } | |
| scene.add(chunk); | |
| chunks[chunkKey] = chunk; | |
| } | |
| function generateTree(x, y, z, chunk) { | |
| // Trunk | |
| for (let i = 0; i < 4; i++) { | |
| createBlock(x, y + i, z, 'wood', chunk); | |
| } | |
| // Leaves | |
| for (let lx = -2; lx <= 2; lx++) { | |
| for (let lz = -2; lz <= 2; lz++) { | |
| for (let ly = 3; ly <= 5; ly++) { | |
| if (Math.abs(lx) + Math.abs(lz) + Math.abs(ly - 4) < 4) { | |
| createBlock(x + lx, y + ly, z + lz, 'leaves', chunk); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| function createBlock(x, y, z, type, chunk) { | |
| const mesh = new THREE.Mesh(geometry, materials[type]); | |
| mesh.position.set(x * BLOCK_SIZE, y * BLOCK_SIZE, z * BLOCK_SIZE); | |
| mesh.castShadow = true; | |
| mesh.receiveShadow = true; | |
| mesh.userData = { type: type, x, y, z }; | |
| chunk.add(mesh); | |
| const key = `${x},${y},${z}`; | |
| world[key] = { type, mesh, chunk }; | |
| } | |
| // Game Loop | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| if (!isPaused && isLocked) { | |
| updatePhysics(); | |
| updateChunks(); | |
| updateHighlight(); | |
| } | |
| renderer.render(scene, camera); | |
| } | |
| function updatePhysics() { | |
| // Gravity | |
| player.vy -= 0.02; | |
| // Movement | |
| const speed = 0.15; | |
| const direction = new THREE.Vector3(); | |
| if (keys['w']) direction.z -= 1; | |
| if (keys['s']) direction.z += 1; | |
| if (keys['a']) direction.x -= 1; | |
| if (keys['d']) direction.x += 1; | |
| direction.normalize().multiplyScalar(speed); | |
| direction.applyEuler(new THREE.Euler(0, camera.rotation.y, 0)); | |
| player.vx = direction.x; | |
| player.vz = direction.z; | |
| // Apply velocity with collision | |
| tryMove(player.vx, 0, 0); | |
| tryMove(0, 0, player.vz); | |
| tryMove(0, player.vy, 0); | |
| // Jump | |
| if (keys[' '] && player.onGround) { | |
| player.vy = 0.4; | |
| player.onGround = false; | |
| } | |
| camera.position.set(player.x, player.y + player.height, player.z); | |
| } | |
| function tryMove(dx, dy, dz) { | |
| const newX = player.x + dx; | |
| const newY = player.y + dy; | |
| const newZ = player.z + dz; | |
| if (!checkCollision(newX, newY, newZ)) { | |
| player.x = newX; | |
| player.y = newY; | |
| player.z = newZ; | |
| player.onGround = dy === 0 && player.vy <= 0; | |
| } else if (dy < 0) { | |
| player.vy = 0; | |
| player.onGround = true; | |
| } else if (dy > 0) { | |
| player.vy = 0; | |
| } | |
| } | |
| function checkCollision(x, y, z) { | |
| const minX = Math.floor(x - player.width/2); | |
| const maxX = Math.floor(x + player.width/2); | |
| const minY = Math.floor(y); | |
| const maxY = Math.floor(y + player.height); | |
| const minZ = Math.floor(z - player.width/2); | |
| const maxZ = Math.floor(z + player.width/2); | |
| for (let bx = minX; bx <= maxX; bx++) { | |
| for (let by = minY; by <= maxY; by++) { | |
| for (let bz = minZ; bz <= maxZ; bz++) { | |
| const key = `${bx},${by},${bz}`; | |
| if (world[key] && world[key].type !== 'water' && world[key].type !== 'leaves') { | |
| return true; | |
| } | |
| } | |
| } | |
| } | |
| return false; | |
| } | |
| function updateChunks() { | |
| const cx = Math.floor(player.x / CHUNK_SIZE); | |
| const cz = Math.floor(player.z / CHUNK_SIZE); | |
| for (let dx = -RENDER_DISTANCE; dx <= RENDER_DISTANCE; dx++) { | |
| for (let dz = -RENDER_DISTANCE; dz <= RENDER_DISTANCE; dz++) { | |
| generateChunk(cx + dx, cz + dz); | |
| } | |
| } | |
| } | |
| function updateHighlight() { | |
| raycaster.setFromCamera(new THREE.Vector2(0, 0), camera); | |
| const intersects = raycaster.intersectObjects(scene.children, true); | |
| for (let hit of intersects) { | |
| if (hit.object !== highlightMesh && hit.object.geometry) { | |
| highlightMesh.position.copy(hit.object.position); | |
| highlightMesh.visible = true; | |
| return; | |
| } | |
| } | |
| highlightMesh.visible = false; | |
| } | |
| // Input Handling | |
| function onKeyDown(e) { | |
| keys[e.key.toLowerCase()] = true; | |
| if (e.key === 'Escape') { | |
| if (isLocked) { | |
| pauseGame(); | |
| } | |
| } | |
| if (e.key >= '1' && e.key <= '9') { | |
| selectSlot(parseInt(e.key) - 1); | |
| } | |
| } | |
| function onKeyUp(e) { | |
| keys[e.key.toLowerCase()] = false; | |
| } | |
| function onMouseDown(e) { | |
| if (!isLocked || isPaused) return; | |
| raycaster.setFromCamera(new THREE.Vector2(0, 0), camera); | |
| const intersects = raycaster.intersectObjects(scene.children, true); | |
| if (intersects.length > 0) { | |
| const hit = intersects[0]; | |
| if (hit.object === highlightMesh) return; | |
| const pos = hit.object.position; | |
| if (e.button === 0) { // Break | |
| const key = `${pos.x/BLOCK_SIZE},${pos.y/BLOCK_SIZE},${pos.z/BLOCK_SIZE}`; | |
| if (world[key]) { | |
| hit.object.parent.remove(hit.object); | |
| delete world[key]; | |
| createParticles(pos, hit.object.material.color); | |
| } | |
| } else if (e.button === 2) { // Place | |
| const normal = hit.face.normal; | |
| const newX = pos.x + normal.x * BLOCK_SIZE; | |
| const newY = pos.y + normal.y * BLOCK_SIZE; | |
| const newZ = pos.z + normal.z * BLOCK_SIZE; | |
| // Don't place inside player | |
| const px = Math.floor(player.x); | |
| const py = Math.floor(player.y); | |
| const pz = Math.floor(player.z); | |
| const nx = newX / BLOCK_SIZE; | |
| const ny = newY / BLOCK_SIZE; | |
| const nz = newZ / BLOCK_SIZE; | |
| if (Math.abs(px - nx) < 1 && Math.abs(py - ny) < 2 && Math.abs(pz - nz) < 1) { | |
| return; | |
| } | |
| const cx = Math.floor(nx / CHUNK_SIZE); | |
| const cz = Math.floor(nz / CHUNK_SIZE); | |
| const chunkKey = `${cx},${cz}`; | |
| if (chunks[chunkKey]) { | |
| createBlock(nx, ny, nz, selectedBlock, chunks[chunkKey]); | |
| } | |
| } | |
| } | |
| } | |
| function createParticles(pos, color) { | |
| for (let i = 0; i < 8; i++) { | |
| const particle = document.createElement('div'); | |
| particle.className = 'particle'; | |
| particle.style.left = (window.innerWidth / 2) + 'px'; | |
| particle.style.top = (window.innerHeight / 2) + 'px'; | |
| particle.style.background = '#' + color.getHexString(); | |
| document.body.appendChild(particle); | |
| const angle = (Math.PI * 2 * i) / 8; | |
| const velocity = 5; | |
| let x = 0, y = 0; | |
| let opacity = 1; | |
| const animate = () => { | |
| x += Math.cos(angle) * velocity; | |
| y += Math.sin(angle) * velocity + 2; | |
| opacity -= 0.02; | |
| particle.style.transform = `translate(${x}px, ${y}px)`; | |
| particle.style.opacity = opacity; | |
| if (opacity > 0) { | |
| requestAnimationFrame(animate); | |
| } else { | |
| particle.remove(); | |
| } | |
| }; | |
| requestAnimationFrame(animate); | |
| } | |
| } | |
| function onMouseMove(e) { | |
| if (!isLocked) return; | |
| camera.rotation.y -= e.movementX * 0.002; | |
| camera.rotation.x -= e.movementY * 0.002; | |
| camera.rotation.x = Math.max(-Math.PI/2, Math.min(Math.PI/2, camera.rotation.x)); | |
| } | |
| function onPointerLockChange() { | |
| isLocked = document.pointerLockElement === document.body; | |
| if (!isLocked && !isPaused) { | |
| pauseGame(); | |
| } | |
| } | |
| function onWindowResize() { | |
| camera.aspect = window.innerWidth / window.innerHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| } | |
| // Game Flow | |
| async function startGame() { | |
| document.getElementById('main-menu').style.display = 'none'; | |
| document.getElementById('loading-screen').style.display = 'flex'; | |
| init(); | |
| // Simulate loading | |
| let progress = 0; | |
| const interval = setInterval(() => { | |
| progress += Math.random() * 15; | |
| if (progress >= 100) { | |
| progress = 100; | |
| clearInterval(interval); | |
| setTimeout(() => { | |
| document.getElementById('loading-screen').style.display = 'none'; | |
| document.body.requestPointerLock(); | |
| isPaused = false; | |
| document.getElementById('crosshair').style.opacity = 1; | |
| document.getElementById('hotbar').style.opacity = 1; | |
| document.getElementById('controls-hint').style.opacity = 1; | |
| document.getElementById('health-bar').style.opacity = 1; | |
| animate(); | |
| }, 500); | |
| } | |
| document.getElementById('loading-progress').style.width = progress + '%'; | |
| }, 200); | |
| } | |
| function pauseGame() { | |
| isPaused = true; | |
| document.exitPointerLock(); | |
| document.getElementById('pause-menu').classList.remove('hidden'); | |
| document.getElementById('crosshair').style.opacity = 0; | |
| } | |
| function resumeGame() { | |
| isPaused = false; | |
| document.body.requestPointerLock(); | |
| document.getElementById('pause-menu').classList.add('hidden'); | |
| document.getElementById('crosshair').style.opacity = 1; | |
| } | |
| function returnToMenu() { | |
| location.reload(); | |
| } | |
| // Prevent context menu | |
| document.addEventListener('contextmenu', e => e.preventDefault()); | |
| </script> | |
| </body> | |
| </html> |