|
|
|
|
|
|
|
|
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; |
|
|
} |
|
|
|
|
|
|
|
|
canvas.width = window.innerWidth; |
|
|
canvas.height = window.innerHeight; |
|
|
|
|
|
|
|
|
const camera = { |
|
|
position: [0, 1.6, 5], |
|
|
rotation: [0, 0, 0], |
|
|
fov: 75, |
|
|
near: 0.1, |
|
|
far: 1000, |
|
|
speed: 0.2, |
|
|
height: 1.6 |
|
|
}; |
|
|
|
|
|
|
|
|
const game = { |
|
|
score: 0, |
|
|
collectedItems: 0, |
|
|
maxItems: 10, |
|
|
isRunning: true, |
|
|
startTime: Date.now() |
|
|
}; |
|
|
|
|
|
const movement = { |
|
|
forward: false, |
|
|
backward: false, |
|
|
left: false, |
|
|
right: false, |
|
|
jump: false, |
|
|
isGrounded: false, |
|
|
velocityY: 0, |
|
|
gravity: -0.005 |
|
|
}; |
|
|
|
|
|
|
|
|
const physics = { |
|
|
friction: 0.98, |
|
|
maxSpeed: 0.3, |
|
|
jumpForce: 0.15 |
|
|
}; |
|
|
|
|
|
let isMouseLocked = false; |
|
|
let mouseSensitivity = 0.002; |
|
|
const raycaster = new THREE.Raycaster(); |
|
|
const pointer = new THREE.Vector2(); |
|
|
|
|
|
const scene = new THREE.Scene(); |
|
|
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true }); |
|
|
renderer.setSize(canvas.width, canvas.height); |
|
|
renderer.setClearColor(0x222222); |
|
|
|
|
|
|
|
|
const threeCamera = new THREE.PerspectiveCamera( |
|
|
camera.fov, |
|
|
canvas.width / canvas.height, |
|
|
camera.near, |
|
|
camera.far |
|
|
); |
|
|
threeCamera.position.set(...camera.position); |
|
|
|
|
|
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); |
|
|
|
|
|
|
|
|
const hemiLight = new THREE.HemisphereLight(0xffffbb, 0x080820, 0.5); |
|
|
scene.add(hemiLight); |
|
|
|
|
|
|
|
|
scene.fog = new THREE.FogExp2(0x222222, 0.02); |
|
|
|
|
|
createEnvironment(scene); |
|
|
|
|
|
|
|
|
|
|
|
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; |
|
|
} |
|
|
}); |
|
|
|
|
|
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 |
|
|
)); |
|
|
}); |
|
|
|
|
|
|
|
|
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); |
|
|
}); |
|
|
|
|
|
const clock = new THREE.Clock(); |
|
|
function animate() { |
|
|
if (!game.isRunning) return; |
|
|
|
|
|
requestAnimationFrame(animate); |
|
|
const delta = clock.getDelta(); |
|
|
|
|
|
|
|
|
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(); |
|
|
|
|
|
|
|
|
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); |
|
|
|
|
|
|
|
|
velocity.multiplyScalar(camera.speed); |
|
|
velocity.y = movement.velocityY; |
|
|
|
|
|
|
|
|
if (velocity.length() > physics.maxSpeed) { |
|
|
velocity.normalize().multiplyScalar(physics.maxSpeed); |
|
|
} |
|
|
|
|
|
|
|
|
velocity.multiplyScalar(physics.friction); |
|
|
|
|
|
|
|
|
threeCamera.position.add(velocity); |
|
|
|
|
|
|
|
|
if (!movement.isGrounded) { |
|
|
movement.velocityY += movement.gravity; |
|
|
} else { |
|
|
movement.velocityY = 0; |
|
|
} |
|
|
|
|
|
|
|
|
checkGroundCollision(); |
|
|
|
|
|
|
|
|
threeCamera.rotation.set( |
|
|
camera.rotation[0], |
|
|
camera.rotation[1], |
|
|
0, |
|
|
'YXZ' |
|
|
); |
|
|
renderer.render(scene, threeCamera); |
|
|
} |
|
|
|
|
|
animate(); |
|
|
}); |
|
|
|
|
|
function createEnvironment(scene) { |
|
|
const tileSize = 5; |
|
|
const gridSize = 11; |
|
|
const wallHeight = 3; |
|
|
|
|
|
|
|
|
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); |
|
|
|
|
|
|
|
|
const wallMaterial = new THREE.MeshStandardMaterial({ |
|
|
color: 0xcccccc, |
|
|
roughness: 0.7, |
|
|
metalness: 0.1 |
|
|
}); |
|
|
|
|
|
|
|
|
const mazeGrid = Array(gridSize).fill().map(() => Array(gridSize).fill(0)); |
|
|
|
|
|
|
|
|
for (let x = 0; x < gridSize; x++) { |
|
|
for (let z = 0; z < gridSize; z++) { |
|
|
|
|
|
if (x === 0 || x === gridSize-1 || z === 0 || z === gridSize-1) { |
|
|
mazeGrid[x][z] = 1; |
|
|
} |
|
|
|
|
|
else if (Math.random() > 0.7 && !(x === 1 && z === 1)) { |
|
|
mazeGrid[x][z] = 1; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
mazeGrid[1][1] = 0; |
|
|
mazeGrid[gridSize-2][gridSize-2] = 0; |
|
|
|
|
|
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); |
|
|
}); |
|
|
|
|
|
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); |
|
|
} |
|
|
|
|
|
|
|
|
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); |
|
|
} |
|
|
|
|
|
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; |
|
|
} |
|
|
|
|
|
|
|
|
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) { |
|
|
|
|
|
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.isRunning = false; |
|
|
const timeElapsed = Math.floor((Date.now() - game.startTime) / 1000); |
|
|
document.getElementById('title').innerHTML = ` |
|
|
<h1 class="text-xl font-bold">Game Completed!</h1> |
|
|
<p>Score: ${game.score + timeElapsed * 10}</p> |
|
|
<p>Time: ${timeElapsed}s</p> |
|
|
`; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function updateUI() { |
|
|
document.getElementById('score').textContent = `Score: ${game.score}`; |
|
|
document.getElementById('items').textContent = `Items: ${game.collectedItems}/${game.maxItems}`; |
|
|
} |
|
|
|
|
|
|
|
|
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 = ` |
|
|
<div id="score">Score: 0</div> |
|
|
<div id="items">Items: 0/${game.maxItems}</div> |
|
|
<div id="hint">Collect all green spheres!</div> |
|
|
`; |
|
|
ui.appendChild(gameInfo); |
|
|
|
|
|
|
|
|
const threeScript = document.createElement('script'); |
|
|
threeScript.src = 'https://cdn.jsdelivr.net/npm/three@0.132.2/build/three.min.js'; |
|
|
document.head.appendChild(threeScript); |
|
|
|
|
|
const pointerLockScript = document.createElement('script'); |
|
|
pointerLockScript.src = 'https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/controls/PointerLockControls.js'; |
|
|
document.head.appendChild(pointerLockScript); |
|
|
|