|
<!DOCTYPE html> |
|
<html> |
|
<head> |
|
<title>Forest Explorer</title> |
|
<style> |
|
body { margin: 0; background: black; overflow: hidden; } |
|
canvas { width: 100vw; height: 100vh; } |
|
.ui { |
|
position: fixed; |
|
color: white; |
|
text-shadow: 2px 2px 2px rgba(0,0,0,0.5); |
|
pointer-events: none; |
|
font-family: Arial, sans-serif; |
|
} |
|
#stamina { |
|
bottom: 20px; |
|
left: 20px; |
|
width: 200px; |
|
height: 5px; |
|
background: rgba(0,0,0,0.5); |
|
border-radius: 3px; |
|
} |
|
#stamina-bar { |
|
width: 100%; |
|
height: 100%; |
|
background: #4CAF50; |
|
border-radius: 3px; |
|
transition: width 0.2s; |
|
} |
|
#crosshair { |
|
top: 50%; |
|
left: 50%; |
|
transform: translate(-50%, -50%); |
|
font-size: 20px; |
|
} |
|
#flashlight-status { |
|
top: 20px; |
|
right: 20px; |
|
} |
|
.vignette { |
|
position: fixed; |
|
top: 0; |
|
left: 0; |
|
width: 100%; |
|
height: 100%; |
|
background: radial-gradient(circle, transparent 50%, rgba(0,0,0,0.4) 100%); |
|
pointer-events: none; |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<div id="stamina" class="ui"><div id="stamina-bar"></div></div> |
|
<div id="crosshair" class="ui">+</div> |
|
<div id="flashlight-status" class="ui">Flashlight [F]</div> |
|
<div class="vignette"></div> |
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> |
|
<script> |
|
const SPAWN_POSITION = new THREE.Vector3(0, 1.7, 30); |
|
const CAVE_ENTRANCE = new THREE.Vector3(0, 1.7, -15); |
|
|
|
let camera, scene, renderer, clock; |
|
let player = { |
|
position: SPAWN_POSITION.clone(), |
|
velocity: new THREE.Vector3(), |
|
speed: { |
|
walk: 3.5, |
|
sprint: 7, |
|
current: 3.5 |
|
}, |
|
stamina: 100, |
|
headBob: { |
|
value: 0, |
|
intensity: 0.07, |
|
speed: 8 |
|
}, |
|
inCave: false |
|
}; |
|
|
|
let controls = { |
|
forward: false, |
|
backward: false, |
|
left: false, |
|
right: false, |
|
sprint: false |
|
}; |
|
|
|
let flashlight; |
|
let isFlashlightOn = false; |
|
|
|
|
|
init(); |
|
animate(); |
|
|
|
async function init() { |
|
|
|
scene = new THREE.Scene(); |
|
camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 1000); |
|
clock = new THREE.Clock(); |
|
|
|
|
|
renderer = new THREE.WebGLRenderer({antialias: true}); |
|
renderer.setSize(window.innerWidth, window.innerHeight); |
|
renderer.shadowMap.enabled = true; |
|
renderer.shadowMap.type = THREE.PCFSoftShadowMap; |
|
document.body.appendChild(renderer.domElement); |
|
|
|
|
|
createForest(); |
|
createCave(); |
|
setupLighting(); |
|
setupParticles(); |
|
setupAudio(); |
|
|
|
|
|
camera.position.copy(player.position); |
|
document.addEventListener('keydown', onKeyDown); |
|
document.addEventListener('keyup', onKeyUp); |
|
document.addEventListener('mousemove', onMouseMove); |
|
document.addEventListener('click', () => document.body.requestPointerLock()); |
|
} |
|
|
|
function createForest() { |
|
|
|
const ground = new THREE.Mesh( |
|
new THREE.PlaneGeometry(200, 200), |
|
new THREE.MeshStandardMaterial({ |
|
color: 0x33aa33, |
|
roughness: 0.8 |
|
}) |
|
); |
|
ground.rotation.x = -Math.PI/2; |
|
ground.receiveShadow = true; |
|
scene.add(ground); |
|
|
|
|
|
for(let i = 0; i < 500; i++) { |
|
const distance = Math.random() * 80 + 20; |
|
const angle = Math.random() * Math.PI * 2; |
|
const x = Math.cos(angle) * distance; |
|
const z = Math.sin(angle) * distance; |
|
|
|
if(Math.abs(x) > 10 || z > 0) { |
|
createTree(x, z); |
|
} |
|
} |
|
|
|
|
|
for(let i = 0; i < 1000; i++) { |
|
const x = Math.random() * 180 - 90; |
|
const z = Math.random() * 180 - 90; |
|
if(Math.abs(x) > 5 || z > 0) { |
|
createVegetation(x, z); |
|
} |
|
} |
|
} |
|
|
|
function createTree(x, z) { |
|
const trunk = new THREE.Mesh( |
|
new THREE.CylinderGeometry(0.5, 0.7, 5), |
|
new THREE.MeshStandardMaterial({color: 0x885533}) |
|
); |
|
const leaves = new THREE.Mesh( |
|
new THREE.SphereGeometry(2), |
|
new THREE.MeshStandardMaterial({color: 0x227722}) |
|
); |
|
|
|
trunk.position.set(x, 2.5, z); |
|
leaves.position.set(x, 6, z); |
|
|
|
trunk.castShadow = true; |
|
leaves.castShadow = true; |
|
|
|
scene.add(trunk); |
|
scene.add(leaves); |
|
} |
|
|
|
function createVegetation(x, z) { |
|
const color = Math.random() > 0.5 ? 0xff99cc : 0xffff99; |
|
const flower = new THREE.Mesh( |
|
new THREE.SphereGeometry(0.2), |
|
new THREE.MeshStandardMaterial({ |
|
color: color, |
|
emissive: color, |
|
emissiveIntensity: 0.2 |
|
}) |
|
); |
|
flower.position.set(x, 0.2, z); |
|
scene.add(flower); |
|
} |
|
|
|
function createCave() { |
|
|
|
const points = []; |
|
for(let i = 0; i < 10; i++) { |
|
points.push( |
|
new THREE.Vector3( |
|
Math.sin(i/2) * 2, |
|
Math.cos(i/3) * 1.5, |
|
-15 - i * 5 |
|
) |
|
); |
|
} |
|
|
|
const tunnelGeometry = new THREE.TubeGeometry( |
|
new THREE.CatmullRomCurve3(points), |
|
60, |
|
3, |
|
8, |
|
false |
|
); |
|
|
|
const caveMaterial = new THREE.MeshStandardMaterial({ |
|
color: 0x333333, |
|
roughness: 1, |
|
side: THREE.BackSide |
|
}); |
|
|
|
const tunnel = new THREE.Mesh(tunnelGeometry, caveMaterial); |
|
scene.add(tunnel); |
|
|
|
|
|
const markingGeometry = new THREE.CircleGeometry(0.3, 16); |
|
const markingMaterial = new THREE.MeshStandardMaterial({ |
|
color: 0xff0000, |
|
emissive: 0xff0000, |
|
emissiveIntensity: 0.5 |
|
}); |
|
|
|
for(let i = 0; i < 15; i++) { |
|
const marking = new THREE.Mesh(markingGeometry, markingMaterial); |
|
marking.position.set( |
|
Math.random() * 4 - 2, |
|
Math.random() * 2 + 1, |
|
-20 - Math.random() * 30 |
|
); |
|
marking.rotation.y = Math.random() * Math.PI; |
|
scene.add(marking); |
|
} |
|
} |
|
|
|
function setupLighting() { |
|
|
|
const ambientLight = new THREE.AmbientLight(0x666666); |
|
scene.add(ambientLight); |
|
|
|
const sunLight = new THREE.DirectionalLight(0xffffbb, 1); |
|
sunLight.position.set(50, 100, 50); |
|
sunLight.castShadow = true; |
|
scene.add(sunLight); |
|
|
|
|
|
flashlight = new THREE.SpotLight(0xffffff, 1); |
|
flashlight.angle = Math.PI/6; |
|
flashlight.penumbra = 0.1; |
|
flashlight.decay = 2; |
|
flashlight.distance = 30; |
|
flashlight.visible = false; |
|
camera.add(flashlight); |
|
scene.add(camera); |
|
} |
|
|
|
function setupParticles() { |
|
|
|
const particleCount = 1000; |
|
const positions = new Float32Array(particleCount * 3); |
|
const colors = new Float32Array(particleCount * 3); |
|
|
|
for(let i = 0; i < particleCount * 3; i += 3) { |
|
positions[i] = Math.random() * 200 - 100; |
|
positions[i+1] = Math.random() * 20; |
|
positions[i+2] = Math.random() * 200 - 100; |
|
|
|
colors[i] = 1; |
|
colors[i+1] = 1; |
|
colors[i+2] = 0.8; |
|
} |
|
|
|
const geometry = new THREE.BufferGeometry(); |
|
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); |
|
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3)); |
|
|
|
const material = new THREE.PointsMaterial({ |
|
size: 0.1, |
|
vertexColors: true, |
|
transparent: true, |
|
opacity: 0.6 |
|
}); |
|
|
|
const particles = new THREE.Points(geometry, material); |
|
scene.add(particles); |
|
} |
|
|
|
function setupAudio() { |
|
|
|
} |
|
|
|
function update(delta) { |
|
updatePlayer(delta); |
|
updateEnvironment(); |
|
} |
|
|
|
function updatePlayer(delta) { |
|
if(!document.pointerLockElement) return; |
|
|
|
|
|
const direction = new THREE.Vector3(); |
|
if(controls.forward) direction.z -= 1; |
|
if(controls.backward) direction.z += 1; |
|
if(controls.left) direction.x -= 1; |
|
if(controls.right) direction.x += 1; |
|
direction.normalize(); |
|
|
|
|
|
if(controls.sprint && player.stamina > 0 && direction.length() > 0) { |
|
player.speed.current = player.speed.sprint; |
|
player.stamina = Math.max(0, player.stamina - delta * 30); |
|
} else { |
|
player.speed.current = player.speed.walk; |
|
player.stamina = Math.min(100, player.stamina + delta * 10); |
|
} |
|
|
|
document.getElementById('stamina-bar').style.width = |
|
player.stamina + '%'; |
|
|
|
|
|
if(direction.length() > 0) { |
|
const moveX = direction.x * player.speed.current * delta; |
|
const moveZ = direction.z * player.speed.current * delta; |
|
|
|
camera.position.x += moveX * Math.cos(camera.rotation.y) + |
|
moveZ * Math.sin(camera.rotation.y); |
|
camera.position.z += moveZ * Math.cos(camera.rotation.y) - |
|
moveX * Math.sin(camera.rotation.y); |
|
|
|
|
|
player.headBob.value += delta * player.headBob.speed; |
|
camera.position.y = player.position.y + |
|
Math.sin(player.headBob.value) * player.headBob.intensity; |
|
} |
|
|
|
|
|
if(isFlashlightOn) { |
|
flashlight.position.copy(camera.position); |
|
flashlight.rotation.copy(camera.rotation); |
|
} |
|
} |
|
|
|
function updateEnvironment() { |
|
|
|
const inCaveNow = camera.position.z < -15; |
|
if(inCaveNow !== player.inCave) { |
|
player.inCave = inCaveNow; |
|
transitionEnvironment(); |
|
} |
|
} |
|
|
|
function transitionEnvironment() { |
|
if(player.inCave) { |
|
scene.fog = new THREE.FogExp2(0x000000, 0.15); |
|
scene.background = new THREE.Color(0x000000); |
|
} else { |
|
scene.fog = null; |
|
scene.background = new THREE.Color(0x88ccff); |
|
} |
|
} |
|
|
|
function animate() { |
|
requestAnimationFrame(animate); |
|
const delta = clock.getDelta(); |
|
update(delta); |
|
renderer.render(scene, camera); |
|
} |
|
|
|
|
|
function onKeyDown(event) { |
|
switch(event.code) { |
|
case 'KeyW': controls.forward = true; break; |
|
case 'KeyS': controls.backward = true; break; |
|
case 'KeyA': controls.left = true; break; |
|
case 'KeyD': controls.right = true; break; |
|
case 'ShiftLeft': controls.sprint = true; break; |
|
case 'KeyF': toggleFlashlight(); break; |
|
} |
|
} |
|
|
|
function onKeyUp(event) { |
|
switch(event.code) { |
|
case 'KeyW': controls.forward = false; break; |
|
case 'KeyS': controls.backward = false; break; |
|
case 'KeyA': controls.left = false; break; |
|
case 'KeyD': controls.right = false; break; |
|
case 'ShiftLeft': controls.sprint = false; break; |
|
} |
|
} |
|
|
|
function onMouseMove(event) { |
|
if(document.pointerLockElement) { |
|
camera.rotation.y -= event.movementX * 0.002; |
|
camera.rotation.x = Math.max( |
|
-Math.PI/2, |
|
Math.min(Math.PI/2, |
|
camera.rotation.x - event.movementY * 0.002) |
|
); |
|
} |
|
} |
|
|
|
function toggleFlashlight() { |
|
isFlashlightOn = !isFlashlightOn; |
|
flashlight.visible = isFlashlightOn; |
|
document.getElementById('flashlight-status').textContent = |
|
`Flashlight [F] ${isFlashlightOn ? 'ON' : 'OFF'}`; |
|
} |
|
</script> |
|
</body> |
|
</html> |