Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Treasure Hunt 3D - Time Attack!</title> | |
<style> | |
:root { | |
--primary-color: #ffd700; /* Gold */ | |
--secondary-color: #a0522d; /* Sienna */ | |
--text-color: #ffffff; | |
--bg-color: #1a1a2e; | |
--fog-color: #161625; | |
--ui-bg: rgba(0, 0, 0, 0.7); | |
--danger-color: #ff4d4d; /* Red for timer */ | |
} | |
body { | |
margin: 0; | |
overflow: hidden; | |
background-color: var(--bg-color); | |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
color: var(--text-color); | |
} | |
canvas { | |
display: block; | |
} | |
/* UI Elements */ | |
#ui-container { | |
position: absolute; | |
top: 15px; | |
left: 15px; | |
right: 15px; | |
display: flex; | |
justify-content: space-between; | |
align-items: center; /* Align items vertically */ | |
pointer-events: none; /* Allow clicks to pass through */ | |
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8); | |
gap: 10px; /* Space between UI elements */ | |
} | |
#score-info, #treasure-info, #timer-info { | |
background: var(--ui-bg); | |
padding: 10px 15px; | |
border-radius: 8px; | |
font-size: 1.1em; | |
white-space: nowrap; /* Prevent wrapping */ | |
} | |
#timer-info { | |
font-weight: bold; | |
/* Position timer in the center if desired */ | |
/* flex-grow: 1; */ | |
text-align: center; | |
} | |
#timer-info.low-time { | |
color: var(--danger-color); | |
animation: pulse 1s infinite; | |
} | |
@keyframes pulse { | |
0% { transform: scale(1); } | |
50% { transform: scale(1.05); } | |
100% { transform: scale(1); } | |
} | |
#crosshair { | |
position: absolute; | |
top: 50%; | |
left: 50%; | |
width: 4px; | |
height: 4px; | |
background-color: rgba(255, 255, 255, 0.5); | |
border-radius: 50%; | |
transform: translate(-50%, -50%); | |
pointer-events: none; | |
display: none; /* Hidden until game starts */ | |
} | |
/* Menu / Overlay */ | |
#menu-overlay { | |
position: absolute; | |
inset: 0; | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
flex-direction: column; | |
background: rgba(10, 10, 20, 0.95); | |
color: var(--primary-color); | |
text-align: center; | |
transition: opacity 0.5s ease-in-out; | |
z-index: 10; | |
} | |
#menu-overlay.hidden { | |
opacity: 0; | |
pointer-events: none; | |
} | |
#menu-overlay h1 { | |
font-size: 3em; | |
margin-bottom: 10px; | |
letter-spacing: 2px; | |
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); | |
} | |
#menu-overlay h1.game-over { | |
color: var(--danger-color); | |
} | |
#menu-overlay p { | |
font-size: 1.2em; | |
max-width: 400px; | |
margin-bottom: 30px; | |
color: var(--text-color); | |
} | |
#menu-overlay .instructions { | |
font-size: 0.9em; | |
color: #aaa; | |
margin-top: 20px; | |
} | |
.play-button { | |
padding: 15px 30px; | |
background: var(--primary-color); | |
color: var(--bg-color); | |
border: none; | |
border-radius: 8px; | |
cursor: pointer; | |
font-size: 1.4em; | |
font-weight: bold; | |
transition: background-color 0.3s ease, transform 0.1s ease; | |
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); | |
} | |
.play-button:hover { | |
background-color: #ffec80; /* Lighter gold */ | |
} | |
.play-button:active { | |
transform: scale(0.98); | |
} | |
/* Responsive adjustments */ | |
@media (max-width: 768px) { /* Wider breakpoint for UI changes */ | |
#ui-container { | |
flex-direction: column; /* Stack UI elements vertically */ | |
align-items: flex-start; /* Align to left */ | |
top: 10px; | |
left: 10px; | |
right: auto; /* Remove right constraint */ | |
} | |
#timer-info { | |
text-align: left; /* Align timer text left */ | |
} | |
} | |
@media (max-width: 600px) { | |
#menu-overlay h1 { | |
font-size: 2em; | |
} | |
#menu-overlay p { | |
font-size: 1em; | |
} | |
.play-button { | |
padding: 12px 25px; | |
font-size: 1.2em; | |
} | |
#score-info, #treasure-info, #timer-info { | |
font-size: 0.9em; | |
padding: 8px 12px; | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<!-- UI Elements --> | |
<div id="ui-container"> | |
<div id="treasure-info">Treasures: 0/5</div> | |
<div id="timer-info">Time: 00:00</div> | |
<div id="score-info">Score: 0</div> | |
</div> | |
<div id="crosshair"></div> | |
<!-- Menu Overlay --> | |
<div id="menu-overlay"> | |
<h1 id="menu-title">TREASURE HUNT</h1> | |
<p id="menu-message">Explore the misty landscape and find all the hidden treasure chests before time runs out!</p> | |
<button id="play-btn" class="play-button">PLAY</button> | |
<p class="instructions"> | |
Controls: [W][A][S][D] to Move | [SPACE] to Jump | [MOUSE] to Look | [ESC] to Pause | |
</p> | |
</div> | |
<!-- Three.js canvas will be appended here --> | |
<script src="https://cdn.jsdelivr.net/npm/three@0.137.5/build/three.min.js"></script> | |
<script src="https://cdn.jsdelivr.net/npm/three@0.137.5/examples/js/controls/PointerLockControls.js"></script> | |
<script> | |
// --- Configuration --- | |
const CONFIG = { | |
TREASURE_COUNT: 5, | |
TIMER_DURATION: 120, // seconds (2 minutes) | |
TIME_LOW_THRESHOLD: 15, // seconds when timer turns red | |
MAP_SIZE: 200, | |
FOG_NEAR: 50, | |
FOG_FAR: 150, | |
PLAYER_SPEED: 6, | |
PLAYER_JUMP_FORCE: 8, | |
GRAVITY: 18, | |
OBJECT_DENSITY: 0.3, | |
TREASURE_VALUE: 100, | |
TREASURE_GLOW_INTENSITY: 1.5, | |
TREASURE_REACH_DISTANCE: 2.5, | |
CAMERA_HEIGHT: 1.7, | |
}; | |
// --- DOM Elements --- | |
const uiContainer = document.getElementById('ui-container'); | |
const treasureInfo = document.getElementById('treasure-info'); | |
const scoreInfo = document.getElementById('score-info'); | |
const timerInfo = document.getElementById('timer-info'); // Timer UI element | |
const crosshair = document.getElementById('crosshair'); | |
const menuOverlay = document.getElementById('menu-overlay'); | |
const menuTitle = document.getElementById('menu-title'); | |
const menuMessage = document.getElementById('menu-message'); | |
const playButton = document.getElementById('play-btn'); | |
// --- Game State --- | |
let scene, camera, renderer, controls; | |
let lastFrameTime = 0; | |
let gameActive = false; | |
const gameState = { | |
score: 0, | |
treasuresFound: 0, | |
treasuresTotal: CONFIG.TREASURE_COUNT, | |
remainingTime: CONFIG.TIMER_DURATION, // Timer state | |
playerVelocity: new THREE.Vector3(), | |
canJump: false, | |
keys: {}, | |
objects: [], | |
treasures: [], | |
}; | |
// --- Initialization --- | |
function init() { | |
setupScene(); | |
setupLighting(); | |
setupPlayer(); | |
setupControls(); | |
createWorld(); | |
addEventListeners(); | |
updateUI(); // Initial UI state, including timer | |
renderer.setAnimationLoop(animate); | |
console.log("Game initialized."); | |
} | |
function setupScene() { | |
scene = new THREE.Scene(); | |
scene.background = new THREE.Color(getComputedStyle(document.body).getPropertyValue('--bg-color').trim()); | |
scene.fog = new THREE.Fog(getComputedStyle(document.body).getPropertyValue('--fog-color').trim(), CONFIG.FOG_NEAR, CONFIG.FOG_FAR); | |
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); | |
camera.position.y = CONFIG.CAMERA_HEIGHT; | |
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); | |
} | |
function setupLighting() { | |
scene.add(new THREE.AmbientLight(0x606080, 0.6)); | |
const sun = new THREE.DirectionalLight(0xffffff, 0.8); | |
sun.position.set(50, 80, 30); | |
sun.castShadow = true; | |
sun.shadow.mapSize.width = 1024; | |
sun.shadow.mapSize.height = 1024; | |
sun.shadow.camera.near = 50; | |
sun.shadow.camera.far = 200; | |
sun.shadow.camera.left = -CONFIG.MAP_SIZE / 2; | |
sun.shadow.camera.right = CONFIG.MAP_SIZE / 2; | |
sun.shadow.camera.top = CONFIG.MAP_SIZE / 2; | |
sun.shadow.camera.bottom = -CONFIG.MAP_SIZE / 2; | |
scene.add(sun); | |
} | |
function setupPlayer() { | |
// Initial state handled in gameState | |
} | |
function setupControls() { | |
controls = new THREE.PointerLockControls(camera, document.body); | |
controls.addEventListener('lock', () => { | |
// Only truly start/resume if not game over | |
if (gameState.remainingTime > 0 && gameState.treasuresFound < gameState.treasuresTotal) { | |
gameActive = true; | |
menuOverlay.classList.add('hidden'); | |
crosshair.style.display = 'block'; | |
console.log("Pointer locked, game running."); | |
} else { | |
// If game was over, locking shouldn't resume it without reset | |
controls.unlock(); // Immediately unlock if trying to resume a finished game | |
console.log("Game over state, cannot resume via lock."); | |
} | |
}); | |
controls.addEventListener('unlock', () => { | |
gameActive = false; // Always pause updates when unlocked | |
menuOverlay.classList.remove('hidden'); | |
crosshair.style.display = 'none'; | |
// Update menu based on current game state (paused, won, or time up) | |
updateMenuState(); | |
console.log("Pointer unlocked, game paused/ended."); | |
}); | |
// Click listener to request lock | |
document.body.addEventListener('click', () => { | |
// Only try to lock if the menu is visible and it's not a "game over" state button | |
if (!controls.isLocked && !menuOverlay.classList.contains('hidden')) { | |
// Check button text to prevent locking if game is over and needs reset | |
const buttonText = playButton.textContent; | |
if (buttonText === "PLAY" || buttonText === "RESUME") { | |
controls.lock(); | |
} else { | |
console.log("Click ignored, game needs reset via button."); | |
} | |
} | |
}, false); | |
} | |
// --- World Creation --- | |
function createWorld() { | |
createGround(); | |
createSkybox(); | |
populateEnvironment(); | |
createTreasures(); | |
} | |
function createGround() { | |
const groundMaterial = new THREE.MeshStandardMaterial({ | |
color: 0x3c5d3c, roughness: 0.9, metalness: 0.1, | |
}); | |
const groundGeometry = new THREE.PlaneGeometry(CONFIG.MAP_SIZE, CONFIG.MAP_SIZE); | |
const ground = new THREE.Mesh(groundGeometry, groundMaterial); | |
ground.rotation.x = -Math.PI / 2; | |
ground.receiveShadow = true; | |
scene.add(ground); | |
} | |
function createSkybox() { | |
const loader = new THREE.CubeTextureLoader(); | |
const texture = loader.load([ | |
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkWL3/DwACOQGyfKR0WgAAAABJRU5ErkJggg==', // px | |
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkWL3/DwACOQGyfKR0WgAAAABJRU5ErkJggg==', // nx | |
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mMQHOv/DwACcQG7W1L0RgAAAABJRU5ErkJggg==', // py | |
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPj/HwADBQEB9/JvZQAAAABJRU5ErkJggg==', // ny | |
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkWL3/DwACOQGyfKR0WgAAAABJRU5ErkJggg==', // pz | |
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkWL3/DwACOQGyfKR0WgAAAABJRU5ErkJggg==' // nz | |
]); | |
texture.mapping = THREE.CubeReflectionMapping; | |
scene.background = texture; | |
} | |
function populateEnvironment() { | |
const area = CONFIG.MAP_SIZE * CONFIG.MAP_SIZE; | |
const objectCount = Math.floor(area * CONFIG.OBJECT_DENSITY / 100); | |
const halfMap = CONFIG.MAP_SIZE / 2 - 5; | |
const treeMaterial = new THREE.MeshStandardMaterial({ color: 0x2e8b57, roughness: 0.8 }); | |
const treeGeometry = new THREE.ConeGeometry(1.5, 5, 8); | |
createObjects(Math.floor(objectCount * 0.6), treeGeometry, treeMaterial, 2.5, halfMap); | |
const rockMaterial = new THREE.MeshStandardMaterial({ color: 0x666677, roughness: 0.7, metalness: 0.2 }); | |
const rockGeometry1 = new THREE.DodecahedronGeometry(1.2); | |
const rockGeometry2 = new THREE.IcosahedronGeometry(0.9); | |
createObjects(Math.floor(objectCount * 0.2), rockGeometry1, rockMaterial, 0.6, halfMap); | |
createObjects(Math.floor(objectCount * 0.2), rockGeometry2, rockMaterial, 0.45, halfMap); | |
} | |
function createObjects(count, geometry, material, yPos, range) { | |
for (let i = 0; i < count; i++) { | |
const mesh = new THREE.Mesh(geometry, material); | |
mesh.position.set( | |
(Math.random() - 0.5) * range * 2, | |
yPos, | |
(Math.random() - 0.5) * range * 2 | |
); | |
mesh.rotation.y = Math.random() * Math.PI * 2; | |
mesh.castShadow = true; | |
scene.add(mesh); | |
gameState.objects.push(mesh); | |
} | |
} | |
function createTreasures() { | |
// Clear existing treasures if any (for reset) | |
gameState.treasures.forEach(chest => scene.remove(chest)); | |
gameState.treasures = []; | |
const chestMaterialBase = new THREE.MeshStandardMaterial({ color: 0x8B4513, roughness: 0.6, metalness: 0.2 }); | |
const chestMaterialLid = new THREE.MeshStandardMaterial({ color: 0xCD853F, roughness: 0.6, metalness: 0.2 }); | |
const chestMaterialMetal = new THREE.MeshStandardMaterial({ color: 0xFFD700, roughness: 0.3, metalness: 0.8, emissive: 0xccac00, emissiveIntensity: 0.1 }); | |
const baseGeometry = new THREE.BoxGeometry(1.2, 0.8, 0.8); | |
const lidGeometry = new THREE.BoxGeometry(1.2, 0.4, 0.8); | |
const strapGeometry = new THREE.BoxGeometry(1.22, 0.15, 0.15); | |
const lockGeometry = new THREE.BoxGeometry(0.2, 0.25, 0.1); | |
const halfMap = CONFIG.MAP_SIZE / 2 - 15; | |
for (let i = 0; i < CONFIG.TREASURE_COUNT; i++) { | |
const chest = new THREE.Group(); | |
const base = new THREE.Mesh(baseGeometry, chestMaterialBase); | |
base.position.y = 0.4; base.castShadow = true; chest.add(base); | |
const lid = new THREE.Mesh(lidGeometry, chestMaterialLid); | |
lid.position.set(0, 1.0, -0.1); lid.rotation.x = -0.1; lid.castShadow = true; chest.add(lid); | |
const strap1 = new THREE.Mesh(strapGeometry, chestMaterialMetal); | |
strap1.position.set(0, 0.4, 0.4 - 0.15/2); chest.add(strap1); | |
const strap2 = strap1.clone(); strap2.position.z = -0.4 + 0.15/2; chest.add(strap2); | |
const lock = new THREE.Mesh(lockGeometry, chestMaterialMetal); | |
lock.position.set(0, 0.6, 0.4 + 0.05); chest.add(lock); | |
chest.position.set( | |
(Math.random() - 0.5) * halfMap * 2, 0, (Math.random() - 0.5) * halfMap * 2 | |
); | |
chest.rotation.y = Math.random() * Math.PI * 2; | |
const glow = new THREE.PointLight(0xFFA500, CONFIG.TREASURE_GLOW_INTENSITY, 5); | |
glow.position.y = 0.8; chest.add(glow); | |
chest.userData = { found: false, initialScale: chest.scale.clone() }; | |
scene.add(chest); | |
gameState.treasures.push(chest); | |
} | |
gameState.treasuresTotal = gameState.treasures.length; | |
} | |
// --- Game Loop --- | |
function animate(time) { | |
const currentTime = time * 0.001; | |
const deltaTime = Math.min(0.05, currentTime - (lastFrameTime || currentTime)); | |
lastFrameTime = currentTime; | |
// Update game state only if active | |
if (gameActive) { | |
update(deltaTime); | |
} | |
// Always render | |
renderer.render(scene, camera); | |
} | |
function update(deltaTime) { | |
// Timer update | |
gameState.remainingTime -= deltaTime; | |
if (gameState.remainingTime <= 0) { | |
gameState.remainingTime = 0; | |
handleGameOver("Time's Up!"); // End game if timer runs out | |
updateUI(); // Update UI one last time to show 00:00 | |
return; // Stop further updates this frame | |
} | |
handleMovement(deltaTime); | |
handlePhysics(deltaTime); | |
checkTreasureCollection(); | |
updateUI(); // Update UI elements including timer | |
} | |
// --- Game Logic --- | |
function handleMovement(deltaTime) { | |
const moveSpeed = CONFIG.PLAYER_SPEED * deltaTime; | |
let moveForward = 0; | |
let moveRight = 0; | |
if (gameState.keys['KeyW'] || gameState.keys['ArrowUp']) moveForward -= 1; | |
if (gameState.keys['KeyS'] || gameState.keys['ArrowDown']) moveForward += 1; | |
if (gameState.keys['KeyA'] || gameState.keys['ArrowLeft']) moveRight -= 1; | |
if (gameState.keys['KeyD'] || gameState.keys['ArrowRight']) moveRight += 1; | |
controls.moveForward(moveForward * moveSpeed); | |
controls.moveRight(moveRight * moveSpeed); | |
camera.position.x = Math.max(-CONFIG.MAP_SIZE/2 + 1, Math.min(CONFIG.MAP_SIZE/2 - 1, camera.position.x)); | |
camera.position.z = Math.max(-CONFIG.MAP_SIZE/2 + 1, Math.min(CONFIG.MAP_SIZE/2 - 1, camera.position.z)); | |
} | |
function handlePhysics(deltaTime) { | |
gameState.playerVelocity.y -= CONFIG.GRAVITY * deltaTime; | |
camera.position.y += gameState.playerVelocity.y * deltaTime; | |
if (camera.position.y < CONFIG.CAMERA_HEIGHT) { | |
camera.position.y = CONFIG.CAMERA_HEIGHT; | |
gameState.playerVelocity.y = 0; | |
gameState.canJump = true; | |
} | |
if ((gameState.keys['Space']) && gameState.canJump) { | |
gameState.playerVelocity.y = CONFIG.PLAYER_JUMP_FORCE; | |
gameState.canJump = false; | |
} | |
} | |
function checkTreasureCollection() { | |
const playerPos = camera.position; | |
gameState.treasures.forEach(chest => { | |
if (!chest.userData.found && chest.position.distanceTo(playerPos) < CONFIG.TREASURE_REACH_DISTANCE) { | |
collectTreasure(chest); | |
} | |
}); | |
} | |
function collectTreasure(chest) { | |
if (chest.userData.found) return; // Prevent double collection | |
chest.userData.found = true; | |
gameState.score += CONFIG.TREASURE_VALUE; | |
gameState.treasuresFound++; | |
// Visual feedback: Shrink and fade out animation remains the same | |
const duration = 0.5; | |
const targetScale = 0.01; | |
let elapsed = 0; | |
const initialOpacityMap = new Map(); // Store initial opacities properly | |
chest.children.forEach(child => { | |
if (child.material) { | |
// Ensure material is unique or cloned if shared | |
if (!child.material.userData?.clonedForFade) { | |
child.material = child.material.clone(); | |
child.material.userData = { clonedForFade: true }; | |
} | |
initialOpacityMap.set(child.uuid, child.material.opacity !== undefined ? child.material.opacity : 1); | |
child.material.transparent = true; // Prepare for fading | |
} | |
}); | |
function shrinkAndFade() { | |
if (!gameActive && chest.scale.x > targetScale) { | |
requestAnimationFrame(shrinkAndFade); | |
return; | |
} | |
// Use performance.now() for more accurate delta time in animation if needed | |
elapsed += 1/60; // Approximation | |
const progress = Math.min(1, elapsed / duration); | |
const currentScale = THREE.MathUtils.lerp(chest.userData.initialScale.x, targetScale, progress); | |
chest.scale.set(currentScale, currentScale, currentScale); | |
chest.children.forEach(child => { | |
if (child.material) { | |
const initialOpacity = initialOpacityMap.get(child.uuid) ?? 1; | |
child.material.opacity = THREE.MathUtils.lerp(initialOpacity, 0, progress); | |
} | |
if(child.isPointLight) { | |
child.intensity = THREE.MathUtils.lerp(CONFIG.TREASURE_GLOW_INTENSITY, 0, progress); | |
} | |
}); | |
if (progress < 1) { | |
requestAnimationFrame(shrinkAndFade); | |
} else { | |
chest.visible = false; | |
// Restore if needed for reset, handled in resetGame now | |
console.log(`Collected treasure ${gameState.treasuresFound}/${gameState.treasuresTotal}`); | |
} | |
} | |
requestAnimationFrame(shrinkAndFade); | |
// Don't update UI here, it's handled in the main loop or after checkWinCondition | |
checkWinCondition(); | |
} | |
function formatTime(seconds) { | |
const mins = Math.floor(seconds / 60); | |
const secs = Math.floor(seconds % 60); | |
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; | |
} | |
function checkWinCondition() { | |
if (gameState.treasuresFound >= gameState.treasuresTotal) { | |
handleGameWin(); | |
} | |
} | |
function handleGameWin() { | |
if (!gameActive) return; // Prevent multiple win calls | |
gameActive = false; | |
controls.unlock(); | |
menuTitle.textContent = "YOU WIN!"; | |
menuTitle.classList.remove('game-over'); | |
menuMessage.textContent = `Congratulations! You found all ${gameState.treasuresTotal} treasures with ${formatTime(gameState.remainingTime)} left. Final Score: ${gameState.score}`; | |
playButton.textContent = "PLAY AGAIN"; | |
playButton.onclick = resetGame; | |
menuOverlay.classList.remove('hidden'); | |
console.log("Game Won!"); | |
} | |
function handleGameOver(reason) { | |
if (!gameActive && gameState.remainingTime > 0) return; // Prevent game over if paused or already over normally | |
gameActive = false; | |
controls.unlock(); // Ensure controls are unlocked | |
menuTitle.textContent = "GAME OVER"; | |
menuTitle.classList.add('game-over'); // Style game over title | |
menuMessage.textContent = `${reason}. You found ${gameState.treasuresFound}/${gameState.treasuresTotal} treasures. Score: ${gameState.score}`; | |
playButton.textContent = "TRY AGAIN"; | |
playButton.onclick = resetGame; | |
menuOverlay.classList.remove('hidden'); | |
console.log("Game Over:", reason); | |
} | |
// --- UI --- | |
function updateUI() { | |
treasureInfo.textContent = `Treasures: ${gameState.treasuresFound}/${gameState.treasuresTotal}`; | |
scoreInfo.textContent = `Score: ${gameState.score}`; | |
// Update Timer display | |
timerInfo.textContent = `Time: ${formatTime(gameState.remainingTime)}`; | |
// Add/remove warning class based on time remaining | |
if (gameState.remainingTime <= CONFIG.TIME_LOW_THRESHOLD && gameState.remainingTime > 0) { | |
timerInfo.classList.add('low-time'); | |
} else { | |
timerInfo.classList.remove('low-time'); | |
} | |
} | |
function updateMenuState() { | |
// Called when pointer unlocks | |
if (gameState.treasuresFound >= gameState.treasuresTotal) { | |
// Game Won state already handled by handleGameWin | |
} else if (gameState.remainingTime <= 0) { | |
// Game Over state already handled by handleGameOver | |
} else { | |
// Game Paused state | |
menuTitle.textContent = "PAUSED"; | |
menuTitle.classList.remove('game-over'); | |
menuMessage.textContent = "Take a break or click/press ESC to resume."; | |
playButton.textContent = "RESUME"; | |
playButton.onclick = startGame; // Resume function | |
} | |
menuOverlay.classList.remove('hidden'); // Ensure menu is visible | |
} | |
// --- Reset --- | |
function resetGame() { | |
console.log("Resetting game..."); | |
gameActive = false; // Ensure game is not active during reset | |
// Reset game state | |
gameState.score = 0; | |
gameState.treasuresFound = 0; | |
gameState.remainingTime = CONFIG.TIMER_DURATION; // Reset timer | |
gameState.playerVelocity.set(0, 0, 0); | |
gameState.canJump = false; | |
gameState.keys = {}; | |
camera.position.set(0, CONFIG.CAMERA_HEIGHT, 5); | |
camera.rotation.set(0, 0, 0); | |
// Recreate or reset treasures | |
createTreasures(); // Recreating them handles position shuffling and state reset | |
updateUI(); // Update UI to reflect reset state (timer, score, etc.) | |
// Reset menu appearance | |
menuTitle.textContent = "TREASURE HUNT"; | |
menuTitle.classList.remove('game-over'); | |
menuMessage.textContent = "Explore the misty landscape and find all the hidden treasure chests before time runs out!"; | |
playButton.textContent = "PLAY"; | |
playButton.onclick = startGame; // Set button to start a new game | |
menuOverlay.classList.remove('hidden'); // Show the main menu | |
crosshair.style.display = 'none'; // Hide crosshair | |
timerInfo.classList.remove('low-time'); // Remove timer warning style | |
// Crucially, ensure controls are unlocked if they somehow ended up locked | |
if(controls.isLocked) { | |
controls.unlock(); | |
} | |
} | |
function startGame() { | |
// This function is called by the PLAY/RESUME button | |
// If the button says PLAY AGAIN or TRY AGAIN, perform a full reset first | |
const buttonText = playButton.textContent; | |
if (buttonText === "PLAY AGAIN" || buttonText === "TRY AGAIN") { | |
resetGame(); | |
// After reset, the button text becomes "PLAY", fall through is not intended here. | |
// We want the user to click "PLAY" again after reset. | |
return; // Exit after resetting, user needs to click PLAY on the reset menu | |
} | |
// If resuming or starting fresh (after initial load or reset) | |
if (!controls.isLocked) { | |
controls.lock(); // This will trigger the 'lock' event listener | |
} | |
// The 'lock' listener now handles setting gameActive = true and hiding the menu | |
} | |
// --- Event Listeners --- | |
function addEventListeners() { | |
window.addEventListener('resize', onWindowResize, false); | |
document.addEventListener('keydown', (event) => { gameState.keys[event.code] = true; }); | |
document.addEventListener('keyup', (event) => { gameState.keys[event.code] = false; }); | |
playButton.addEventListener('click', startGame); | |
document.addEventListener('keydown', (event) => { | |
if (event.code === 'Escape') { | |
if (controls.isLocked) { | |
controls.unlock(); // This triggers the 'unlock' listener | |
} else if (!menuOverlay.classList.contains('hidden')) { | |
// If menu is visible (paused, game over, win), try to resume/start | |
startGame(); // Let startGame handle the logic (lock or reset) | |
} | |
} | |
}); | |
} | |
function onWindowResize() { | |
camera.aspect = window.innerWidth / window.innerHeight; | |
camera.updateProjectionMatrix(); | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
} | |
// --- Start the game --- | |
init(); | |
</script> | |
</body> | |
</html> |