Spaces:
Running
Running
<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); // Spawn point | |
const CAVE_ENTRANCE = new THREE.Vector3(0, 1.7, -15); // Cave entrance position | |
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; | |
// Initialize game | |
init(); | |
animate(); | |
async function init() { | |
// Setup basic scene | |
scene = new THREE.Scene(); | |
camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 1000); | |
clock = new THREE.Clock(); | |
// Setup renderer | |
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); | |
// Setup scene elements | |
createForest(); | |
createCave(); | |
setupLighting(); | |
setupParticles(); | |
setupAudio(); | |
// Initialize player | |
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() { | |
// Ground | |
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); | |
// Trees | |
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) { // Keep path to cave clear | |
createTree(x, z); | |
} | |
} | |
// Flowers and grass | |
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() { | |
// Main tunnel | |
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); | |
// Cave markings | |
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() { | |
// Forest lighting | |
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 | |
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() { | |
// Pollen/leaves particle system | |
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() { | |
// Implement audio setup | |
} | |
function update(delta) { | |
updatePlayer(delta); | |
updateEnvironment(); | |
} | |
function updatePlayer(delta) { | |
if(!document.pointerLockElement) return; | |
// Movement direction | |
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(); | |
// Speed and stamina | |
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 + '%'; | |
// Move player | |
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); | |
// Head bob | |
player.headBob.value += delta * player.headBob.speed; | |
camera.position.y = player.position.y + | |
Math.sin(player.headBob.value) * player.headBob.intensity; | |
} | |
// Update flashlight | |
if(isFlashlightOn) { | |
flashlight.position.copy(camera.position); | |
flashlight.rotation.copy(camera.rotation); | |
} | |
} | |
function updateEnvironment() { | |
// Check if entering/leaving cave | |
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); | |
} | |
// Event handlers | |
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> |