// 3D First Person Game with Physics and Interactions document.addEventListener('DOMContentLoaded', () => { const canvas = document.getElementById('gameCanvas'); const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl'); if (!gl) { alert('WebGL not supported in your browser!'); return; } // Set canvas to full window size canvas.width = window.innerWidth; canvas.height = window.innerHeight; // Camera settings const camera = { position: [0, 1.6, 5], rotation: [0, 0, 0], fov: 75, near: 0.1, far: 1000, speed: 0.2, height: 1.6 }; // Game state const game = { score: 0, collectedItems: 0, maxItems: 10, isRunning: true, startTime: Date.now() }; // Player movement state const movement = { forward: false, backward: false, left: false, right: false, jump: false, isGrounded: false, velocityY: 0, gravity: -0.005 }; // Physics settings const physics = { friction: 0.98, maxSpeed: 0.3, jumpForce: 0.15 }; // Game controls let isMouseLocked = false; let mouseSensitivity = 0.002; const raycaster = new THREE.Raycaster(); const pointer = new THREE.Vector2(); // Initialize scene const scene = new THREE.Scene(); const renderer = new THREE.WebGLRenderer({ canvas, antialias: true }); renderer.setSize(canvas.width, canvas.height); renderer.setClearColor(0x222222); // Camera (using Three.js PerspectiveCamera) const threeCamera = new THREE.PerspectiveCamera( camera.fov, canvas.width / canvas.height, camera.near, camera.far ); threeCamera.position.set(...camera.position); // Enhanced lighting const ambientLight = new THREE.AmbientLight(0x404040, 0.5); scene.add(ambientLight); const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); directionalLight.position.set(1, 5, 1); directionalLight.castShadow = true; directionalLight.shadow.mapSize.width = 2048; directionalLight.shadow.mapSize.height = 2048; scene.add(directionalLight); // Hemisphere light for natural outdoor lighting const hemiLight = new THREE.HemisphereLight(0xffffbb, 0x080820, 0.5); scene.add(hemiLight); // Fog for depth scene.fog = new THREE.FogExp2(0x222222, 0.02); // Create a simple maze environment createEnvironment(scene); // Event listeners for keyboard // Enhanced controls with jump window.addEventListener('keydown', (e) => { switch (e.key.toLowerCase()) { case 'w': movement.forward = true; break; case 's': movement.backward = true; break; case 'a': movement.left = true; break; case 'd': movement.right = true; break; case ' ': if (movement.isGrounded) { movement.jump = true; movement.isGrounded = false; movement.velocityY = physics.jumpForce; } break; case 'e': checkInteraction(); break; } }); window.addEventListener('keyup', (e) => { switch (e.key.toLowerCase()) { case 'w': movement.forward = false; break; case 's': movement.backward = false; break; case 'a': movement.left = false; break; case 'd': movement.right = false; break; case ' ': movement.jump = false; break; } }); // Mouse movement controls canvas.addEventListener('click', () => { canvas.requestPointerLock = canvas.requestPointerLock || canvas.mozRequestPointerLock || canvas.webkitRequestPointerLock; canvas.requestPointerLock(); }); document.addEventListener('pointerlockchange', lockChangeAlert, false); document.addEventListener('mozpointerlockchange', lockChangeAlert, false); function lockChangeAlert() { isMouseLocked = document.pointerLockElement === canvas || document.mozPointerLockElement === canvas; } document.addEventListener('mousemove', (e) => { if (!isMouseLocked) return; camera.rotation[1] -= e.movementX * mouseSensitivity; camera.rotation[0] = Math.max(-Math.PI/2.5, Math.min(Math.PI/2.5, camera.rotation[0] - e.movementY * mouseSensitivity )); }); // Handle window resize window.addEventListener('resize', () => { canvas.width = window.innerWidth; canvas.height = window.innerHeight; threeCamera.aspect = canvas.width / canvas.height; threeCamera.updateProjectionMatrix(); renderer.setSize(canvas.width, canvas.height); }); // Enhanced game loop with physics const clock = new THREE.Clock(); function animate() { if (!game.isRunning) return; requestAnimationFrame(animate); const delta = clock.getDelta(); // Movement vectors const forwardVector = new THREE.Vector3( Math.sin(camera.rotation[1]), 0, Math.cos(camera.rotation[1]) ).normalize(); const sideVector = new THREE.Vector3( Math.sin(camera.rotation[1] + Math.PI/2), 0, Math.cos(camera.rotation[1] + Math.PI/2) ).normalize(); // Apply forces const velocity = new THREE.Vector3(); if (movement.forward) velocity.add(forwardVector); if (movement.backward) velocity.sub(forwardVector); if (movement.left) velocity.sub(sideVector); if (movement.right) velocity.add(sideVector); // Apply physics velocity.multiplyScalar(camera.speed); velocity.y = movement.velocityY; // Limit speed if (velocity.length() > physics.maxSpeed) { velocity.normalize().multiplyScalar(physics.maxSpeed); } // Apply friction velocity.multiplyScalar(physics.friction); // Update position threeCamera.position.add(velocity); // Gravity if (!movement.isGrounded) { movement.velocityY += movement.gravity; } else { movement.velocityY = 0; } // Ground check checkGroundCollision(); // Update camera rotation threeCamera.rotation.set( camera.rotation[0], camera.rotation[1], 0, 'YXZ' ); renderer.render(scene, threeCamera); } animate(); }); // Create game environment with collectibles and obstacles function createEnvironment(scene) { const tileSize = 5; const gridSize = 11; const wallHeight = 3; // Floor with texture const floorGeometry = new THREE.PlaneGeometry(tileSize * gridSize, tileSize * gridSize); const floorTexture = new THREE.TextureLoader().load('https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/textures/terrain/grasslight-big.jpg'); floorTexture.wrapS = floorTexture.wrapT = THREE.RepeatWrapping; floorTexture.repeat.set(gridSize, gridSize); const floorMaterial = new THREE.MeshStandardMaterial({ map: floorTexture, roughness: 0.9, metalness: 0 }); const floor = new THREE.Mesh(floorGeometry, floorMaterial); floor.rotation.x = -Math.PI / 2; floor.receiveShadow = true; scene.add(floor); // Walls with better materials const wallMaterial = new THREE.MeshStandardMaterial({ color: 0xcccccc, roughness: 0.7, metalness: 0.1 }); // Create maze grid const mazeGrid = Array(gridSize).fill().map(() => Array(gridSize).fill(0)); // Basic maze structure for (let x = 0; x < gridSize; x++) { for (let z = 0; z < gridSize; z++) { // Border walls if (x === 0 || x === gridSize-1 || z === 0 || z === gridSize-1) { mazeGrid[x][z] = 1; } // Inner pattern else if (Math.random() > 0.7 && !(x === 1 && z === 1)) { mazeGrid[x][z] = 1; } } } // Ensure there's always a path mazeGrid[1][1] = 0; // Start position mazeGrid[gridSize-2][gridSize-2] = 0; // Exit position // Build maze from grid for (let x = 0; x < gridSize; x++) { for (let z = 0; z < gridSize; z++) { if (mazeGrid[x][z]) { const wallX = (x - (gridSize-1)/2) * tileSize; const wallZ = (z - (gridSize-1)/2) * tileSize; const wallGeometry = new THREE.BoxGeometry( tileSize, wallHeight, tileSize ); const wallMesh = new THREE.Mesh(wallGeometry, wallMaterial); wallMesh.position.set(wallX, wallHeight/2, wallZ); wallMesh.castShadow = true; wallMesh.receiveShadow = true; scene.add(wallMesh); } } } walls.forEach(wall => { const wallGeometry = new THREE.BoxGeometry(...wall.size); const wallMesh = new THREE.Mesh(wallGeometry, wallMaterial); wallMesh.position.set(...wall.position); scene.add(wallMesh); }); // Create collectible items for (let i = 0; i < game.maxItems; i++) { let x, z; do { x = Math.floor(Math.random() * gridSize); z = Math.floor(Math.random() * gridSize); } while (mazeGrid[x][z] || (x === 1 && z === 1)); const itemX = (x - (gridSize-1)/2) * tileSize; const itemZ = (z - (gridSize-1)/2) * tileSize; const itemGeometry = new THREE.SphereGeometry(0.5, 16, 16); const itemMaterial = new THREE.MeshStandardMaterial({ color: 0x00ff00, emissive: 0x00ff00, emissiveIntensity: 0.5, metalness: 0.3, roughness: 0.4 }); const item = new THREE.Mesh(itemGeometry, itemMaterial); item.position.set(itemX, 1, itemZ); item.userData.isCollectible = true; item.castShadow = true; scene.add(item); } // Create exit door const exitX = ((gridSize-2) - (gridSize-1)/2) * tileSize; const exitZ = ((gridSize-2) - (gridSize-1)/2) * tileSize; const exitGeometry = new THREE.BoxGeometry(tileSize/2, wallHeight/2, tileSize/2); const exitMaterial = new THREE.MeshStandardMaterial({ color: 0xff0000, emissive: 0xff0000, emissiveIntensity: 0.3 }); const exit = new THREE.Mesh(exitGeometry, exitMaterial); exit.position.set(exitX, wallHeight/4, exitZ); exit.userData.isExit = true; scene.add(exit); } // Collision detection function checkGroundCollision() { const groundRay = new THREE.Raycaster( threeCamera.position, new THREE.Vector3(0, -1, 0), 0, camera.height * 1.1 ); const intersects = groundRay.intersectObjects(scene.children.filter(obj => obj !== floor)); movement.isGrounded = intersects.length > 0; } // Interaction system function checkInteraction() { raycaster.setFromCamera(pointer, threeCamera); const intersects = raycaster.intersectObjects(scene.children); if (intersects.length > 0) { const obj = intersects[0].object; if (obj.userData.isCollectible) { // Collect item scene.remove(obj); game.collectedItems++; game.score += 100; updateUI(); if (game.collectedItems === game.maxItems) { document.getElementById('hint').textContent = "Find the red exit!"; } } else if (obj.userData.isExit && game.collectedItems === game.maxItems) { // Game completed game.isRunning = false; const timeElapsed = Math.floor((Date.now() - game.startTime) / 1000); document.getElementById('title').innerHTML = `
Score: ${game.score + timeElapsed * 10}
Time: ${timeElapsed}s
`; } } } // UI update function updateUI() { document.getElementById('score').textContent = `Score: ${game.score}`; document.getElementById('items').textContent = `Items: ${game.collectedItems}/${game.maxItems}`; } // Initialize UI elements const ui = document.getElementById('ui'); const gameInfo = document.createElement('div'); gameInfo.id = 'game-info'; gameInfo.style.position = 'absolute'; gameInfo.style.top = '20px'; gameInfo.style.right = '20px'; gameInfo.style.backgroundColor = 'rgba(0,0,0,0.5)'; gameInfo.style.padding = '10px'; gameInfo.style.borderRadius = '5px'; gameInfo.style.color = 'white'; gameInfo.innerHTML = `