treasurehunt / index.html
akhaliq's picture
akhaliq HF Staff
Update index.html
304e5f1 verified
<!DOCTYPE html>
<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>