Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>DEBUG - Procedural 3D Dungeon</title> | |
<style> | |
body { margin: 0; overflow: hidden; background-color: #000; color: white; font-family: monospace; } | |
canvas { display: block; } | |
#blocker { position: absolute; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5); display: flex; justify-content: center; align-items: center; cursor: pointer; z-index: 10; } | |
#instructions { width: 50%; text-align: center; padding: 20px; background: rgba(20, 20, 20, 0.8); border-radius: 10px; } | |
#crosshair { position: absolute; top: 50%; left: 50%; width: 10px; height: 10px; border: 1px solid white; border-radius: 50%; transform: translate(-50%, -50%); pointer-events: none; mix-blend-mode: difference; display: none; z-index: 11; } | |
</style> | |
</head> | |
<body> | |
<div id="blocker"> | |
<div id="instructions"> | |
<h1>Dungeon Explorer (Debug Mode)</h1> | |
<p>Click to Enter</p> | |
<p>(W, A, S, D = Move, MOUSE = Look)</p> | |
<p>Check F12 Console for Errors!</p> | |
</div> | |
</div> | |
<div id="crosshair">+</div> | |
<script type="importmap"> | |
{ | |
"imports": { | |
"three": "https://unpkg.com/[email protected]/build/three.module.js", | |
"three/addons/": "https://unpkg.com/[email protected]/examples/jsm/" | |
} | |
} | |
</script> | |
<script type="module"> | |
import * as THREE from 'three'; | |
import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js'; | |
// BufferGeometryUtils not needed for this debug version | |
// import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js'; | |
console.log("Script Start"); | |
// --- Config --- | |
const DUNGEON_WIDTH = 10; // Smaller grid for debug | |
const DUNGEON_HEIGHT = 10; | |
const CELL_SIZE = 5; | |
const WALL_HEIGHT = 4; | |
const PLAYER_HEIGHT = 1.6; | |
const PLAYER_RADIUS = 0.4; | |
const PLAYER_SPEED = 5.0; | |
// --- Three.js Setup --- | |
let scene, camera, renderer; | |
let controls; | |
let clock; | |
let flashlight; | |
// --- Player State --- | |
const playerVelocity = new THREE.Vector3(); | |
const playerDirection = new THREE.Vector3(); | |
let moveForward = false, moveBackward = false, moveLeft = false, moveRight = false; | |
// --- World Data --- | |
let dungeonLayout = []; | |
const worldMeshes = []; // Store refs to added meshes for potential cleanup | |
// --- DOM Elements --- | |
const blocker = document.getElementById('blocker'); | |
const instructions = document.getElementById('instructions'); | |
const crosshair = document.getElementById('crosshair'); | |
// --- Materials (Basic Colors) --- | |
const floorMaterial = new THREE.MeshLambertMaterial({ color: 0x555555 }); // Use Lambert for basic lighting check | |
const wallMaterial = new THREE.MeshLambertMaterial({ color: 0x884444 }); | |
// --- Initialization --- | |
function init() { | |
console.log("--- Initializing Game ---"); | |
clock = new THREE.Clock(); | |
// Clear previous scene if restarting | |
if (scene) { | |
console.log("Clearing previous scene objects..."); | |
worldMeshes.forEach(mesh => { | |
if(mesh.parent) scene.remove(mesh); | |
if(mesh.geometry) mesh.geometry.dispose(); | |
// Only dispose material if we know it's unique per object | |
}); | |
worldMeshes.length = 0; // Clear the array | |
} else { | |
scene = new THREE.Scene(); | |
} | |
scene.background = new THREE.Color(0x111111); | |
scene.fog = new THREE.Fog(0x111111, 10, CELL_SIZE * 6); | |
// Clear previous renderer if restarting | |
if (renderer) { | |
console.log("Disposing previous renderer..."); | |
renderer.dispose(); | |
if (renderer.domElement.parentNode) { | |
renderer.domElement.parentNode.removeChild(renderer.domElement); | |
} | |
} | |
renderer = new THREE.WebGLRenderer({ antialias: true }); | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
renderer.setPixelRatio(window.devicePixelRatio); | |
renderer.shadowMap.enabled = true; // Keep shadows enabled | |
renderer.shadowMap.type = THREE.PCFSoftShadowMap; | |
document.body.appendChild(renderer.domElement); | |
console.log("Renderer created/reset."); | |
// Camera (First Person) | |
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); | |
camera.position.y = PLAYER_HEIGHT; | |
console.log("Camera created."); | |
// Lighting | |
scene.add(new THREE.AmbientLight(0x404040, 0.8)); // Slightly brighter ambient | |
flashlight = new THREE.SpotLight(0xffffff, 3, 30, Math.PI / 5, 0.4, 1.5); | |
flashlight.position.set(0, 0, 0); // Relative to camera | |
flashlight.target.position.set(0, 0, -1); // Relative to camera | |
flashlight.castShadow = true; | |
flashlight.shadow.mapSize.width = 1024; | |
flashlight.shadow.mapSize.height = 1024; | |
flashlight.shadow.camera.near = 0.5; | |
flashlight.shadow.camera.far = 30; | |
camera.add(flashlight); | |
camera.add(flashlight.target); | |
scene.add(camera); // Add camera (with light) to scene | |
console.log("Lighting setup."); | |
// Pointer Lock Controls | |
controls = new PointerLockControls(camera, renderer.domElement); | |
// We don't add controls.getObject() directly to scene IF flashlight is child of camera | |
// scene.add(controls.getObject()); // Only if camera isn't manually added | |
blocker.addEventListener('click', () => { controls.lock(); }); | |
controls.addEventListener('lock', () => { instructions.style.display = 'none'; blocker.style.display = 'none'; crosshair.style.display = 'block'; }); | |
controls.addEventListener('unlock', () => { blocker.style.display = 'flex'; instructions.style.display = ''; crosshair.style.display = 'none'; }); | |
console.log("Controls setup."); | |
// Keyboard Listeners | |
document.removeEventListener('keydown', onKeyDown); // Remove old listeners if restarting | |
document.removeEventListener('keyup', onKeyUp); | |
document.addEventListener('keydown', onKeyDown); | |
document.addEventListener('keyup', onKeyUp); | |
// Resize Listener | |
window.removeEventListener('resize', onWindowResize); // Remove old | |
window.addEventListener('resize', onWindowResize); | |
// --- Generate FIXED Dungeon --- | |
console.log("Generating FIXED dungeon layout..."); | |
dungeonLayout = generateFixedDungeonLayout(DUNGEON_WIDTH, DUNGEON_HEIGHT); | |
console.log("Layout generated, creating meshes..."); | |
createDungeonMeshes_Direct(dungeonLayout); // Use direct mesh addition | |
console.log("Dungeon meshes created."); | |
// --- Set Player Start Position --- | |
const startPos = findStartPosition(dungeonLayout); | |
if (startPos) { | |
// Position the camera (which is the player view) | |
controls.getObject().position.set(startPos.x, PLAYER_HEIGHT, startPos.z); | |
console.log("Player start position set at:", startPos); | |
} else { | |
console.error("Could not find valid start position! Placing at center."); | |
const fallbackX = (DUNGEON_WIDTH / 2) * CELL_SIZE; | |
const fallbackZ = (DUNGEON_HEIGHT / 2) * CELL_SIZE; | |
controls.getObject().position.set(fallbackX, PLAYER_HEIGHT, fallbackZ); | |
} | |
// Add Axes Helper for orientation check | |
const axesHelper = new THREE.AxesHelper(CELL_SIZE); | |
axesHelper.position.copy(controls.getObject().position); // Place at start pos | |
axesHelper.position.y = 0.1; | |
scene.add(axesHelper); | |
worldMeshes.push(axesHelper); // Track for cleanup | |
console.log("--- Initialization Complete ---"); | |
animate(); // Start the loop | |
} | |
// --- Dungeon Generation (FIXED LAYOUT) --- | |
function generateFixedDungeonLayout(width, height) { | |
console.log("Generating FIXED 5x5 layout for debugging..."); | |
const grid = Array(height).fill(null).map(() => Array(width).fill(0)); // All walls | |
// Simple 5x5 room centered | |
const cx = Math.floor(width / 2); | |
const cy = Math.floor(height / 2); | |
for (let y = cy - 2; y <= cy + 2; y++) { | |
for (let x = cx - 2; x <= cx + 2; x++) { | |
if (y >= 0 && y < height && x >= 0 && x < width) { | |
grid[y][x] = 1; // Floor | |
} | |
} | |
} | |
// Add a corridor | |
for (let y = cy + 3; y < height -1 ; y++) { | |
if (grid[y]) grid[y][cx] = 1; | |
} | |
console.log(`Fixed Layout Generated (${width}x${height}). Center: ${cx},${cy}`); | |
// console.log("Grid:", grid.map(row => row.join('')).join('\n')); // Optional: Log grid visually | |
return grid; | |
} | |
// --- Find Start Position (Same as before) --- | |
function findStartPosition(grid) { | |
const startY = Math.floor(grid.length / 2); | |
const startX = Math.floor(grid[0].length / 2); | |
console.log(`Searching for start near ${startX},${startY}`); | |
for (let r = 0; r < Math.max(startX, startY); r++) { | |
for (let y = startY - r; y <= startY + r; y++) { | |
for (let x = startX - r; x <= startX + r; x++) { | |
if (Math.abs(y - startY) === r || Math.abs(x - startX) === r || r === 0) { | |
if (y >= 0 && y < grid.length && x >= 0 && x < grid[0].length && grid[y][x] === 1) { | |
console.log(`Found start floor at ${x},${y}`); | |
return { x: x * CELL_SIZE + CELL_SIZE / 2, z: y * CELL_SIZE + CELL_SIZE / 2 }; | |
} | |
} | |
} | |
} | |
} | |
console.error("Valid start position (floor tile = 1) not found near center!"); | |
return null; // Fallback | |
} | |
// --- Dungeon Meshing (DIRECT ADDITION - NO MERGING) --- | |
function createDungeonMeshes_Direct(grid) { | |
console.log("Creating meshes directly (no merging)..."); | |
// Recreate geometries each time to avoid issues with disposed geometries if init is called again | |
const floorGeo = new THREE.PlaneGeometry(CELL_SIZE, CELL_SIZE); | |
const wallGeoN = new THREE.BoxGeometry(CELL_SIZE, WALL_HEIGHT, 0.1); | |
const wallGeoS = new THREE.BoxGeometry(CELL_SIZE, WALL_HEIGHT, 0.1); | |
const wallGeoE = new THREE.BoxGeometry(0.1, WALL_HEIGHT, CELL_SIZE); | |
const wallGeoW = new THREE.BoxGeometry(0.1, WALL_HEIGHT, CELL_SIZE); | |
for (let y = 0; y < grid.length; y++) { | |
for (let x = 0; x < grid[y].length; x++) { | |
if (grid[y][x] === 1) { // If it's a floor cell | |
// Create Floor Tile Mesh | |
const floorInstance = new THREE.Mesh(floorGeo, floorMaterial); // Use shared geometry instance | |
floorInstance.rotation.x = -Math.PI / 2; | |
floorInstance.position.set(x * CELL_SIZE + CELL_SIZE / 2, 0, y * CELL_SIZE + CELL_SIZE / 2); | |
floorInstance.receiveShadow = true; | |
scene.add(floorInstance); | |
worldMeshes.push(floorInstance); // Track mesh | |
// console.log(`Added floor mesh at ${x},${y}`); | |
// Check neighbors for Walls | |
// North Wall | |
if (y === 0 || grid[y - 1][x] === 0) { | |
const wallInstance = new THREE.Mesh(wallGeoN, wallMaterial); | |
wallInstance.position.set(x * CELL_SIZE + CELL_SIZE / 2, WALL_HEIGHT / 2, y * CELL_SIZE); | |
wallInstance.castShadow = true; wallInstance.receiveShadow = true; | |
scene.add(wallInstance); worldMeshes.push(wallInstance); | |
} | |
// South Wall | |
if (y === grid.length - 1 || grid[y + 1][x] === 0) { | |
const wallInstance = new THREE.Mesh(wallGeoS, wallMaterial); | |
wallInstance.position.set(x * CELL_SIZE + CELL_SIZE / 2, WALL_HEIGHT / 2, y * CELL_SIZE + CELL_SIZE); | |
wallInstance.castShadow = true; wallInstance.receiveShadow = true; | |
scene.add(wallInstance); worldMeshes.push(wallInstance); | |
} | |
// West Wall | |
if (x === 0 || grid[y][x - 1] === 0) { | |
const wallInstance = new THREE.Mesh(wallGeoW, wallMaterial); | |
wallInstance.position.set(x * CELL_SIZE, WALL_HEIGHT / 2, y * CELL_SIZE + CELL_SIZE / 2); | |
wallInstance.castShadow = true; wallInstance.receiveShadow = true; | |
scene.add(wallInstance); worldMeshes.push(wallInstance); | |
} | |
// East Wall | |
if (x === grid[y].length - 1 || grid[y][x + 1] === 0) { | |
const wallInstance = new THREE.Mesh(wallGeoE, wallMaterial); | |
wallInstance.position.set(x * CELL_SIZE + CELL_SIZE, WALL_HEIGHT / 2, y * CELL_SIZE + CELL_SIZE / 2); | |
wallInstance.castShadow = true; wallInstance.receiveShadow = true; | |
scene.add(wallInstance); worldMeshes.push(wallInstance); | |
} | |
} | |
} | |
} | |
// Geometries are shared, no need to dispose here unless we cloned them. | |
// If we were cloning: floorGeo.dispose(); wallGeoN.dispose(); ... | |
console.log("Direct mesh creation complete."); | |
} | |
// --- Player Movement & Collision (No Physics) --- | |
function handleInputAndMovement(deltaTime) { | |
if (!controls || !controls.isLocked) return; | |
const speed = PLAYER_SPEED * deltaTime; | |
// Reset velocity, we calculate total displacement based on keys | |
playerVelocity.x = 0; | |
playerVelocity.z = 0; | |
// Get camera direction (ignore Y) | |
controls.getDirection(playerDirection); // Gets normalized direction vector | |
playerDirection.y = 0; | |
playerDirection.normalize(); | |
// Calculate right vector based on camera direction | |
const rightDirection = new THREE.Vector3(); | |
rightDirection.crossVectors(camera.up, playerDirection).normalize(); // camera.up is (0,1,0) | |
// Apply movement based on keys | |
if (moveForward) playerVelocity.add(playerDirection); | |
if (moveBackward) playerVelocity.sub(playerDirection); | |
if (moveLeft) playerVelocity.sub(rightDirection); | |
if (moveRight) playerVelocity.add(rightDirection); | |
// Normalize diagonal velocity if needed and apply speed | |
if (playerVelocity.lengthSq() > 0) { | |
playerVelocity.normalize().multiplyScalar(speed); | |
} | |
// --- Basic Collision Detection BEFORE moving --- | |
const currentPos = controls.getObject().position; | |
let moveXAllowed = true; | |
let moveZAllowed = true; | |
// Check X Collision | |
if (playerVelocity.x !== 0) { | |
const nextX = currentPos.x + playerVelocity.x; | |
// Check slightly ahead in X direction, at feet and head level Z | |
const checkGridX = Math.floor((nextX + Math.sign(playerVelocity.x) * PLAYER_RADIUS) / CELL_SIZE); | |
const checkGridZFeet = Math.floor((currentPos.z - PLAYER_RADIUS) / CELL_SIZE); | |
const checkGridZHead = Math.floor((currentPos.z + PLAYER_RADIUS) / CELL_SIZE); | |
if ((dungeonLayout[checkGridZFeet]?.[checkGridX] === 0) || (dungeonLayout[checkGridZHead]?.[checkGridX] === 0)) { | |
moveXAllowed = false; | |
// console.log(`Collision X at grid ${checkGridX},${checkGridZFeet}/${checkGridZHead}`); | |
} | |
} | |
// Check Z Collision | |
if (playerVelocity.z !== 0) { | |
const nextZ = currentPos.z + playerVelocity.z; | |
// Check slightly ahead in Z direction, at feet and head level X | |
const checkGridZ = Math.floor((nextZ + Math.sign(playerVelocity.z) * PLAYER_RADIUS) / CELL_SIZE); | |
const checkGridXFeet = Math.floor((currentPos.x - PLAYER_RADIUS) / CELL_SIZE); | |
const checkGridXHead = Math.floor((currentPos.x + PLAYER_RADIUS) / CELL_SIZE); | |
if ((dungeonLayout[checkGridZ]?.[checkGridXFeet] === 0) || (dungeonLayout[checkGridZ]?.[checkGridXHead] === 0)) { | |
moveZAllowed = false; | |
// console.log(`Collision Z at grid ${checkGridXFeet}/${checkGridXHead},${checkGridZ}`); | |
} | |
} | |
// Apply movement only if allowed | |
if (moveXAllowed) { | |
controls.moveRight(playerVelocity.x); // moveRight uses internal right vector, so feed X velocity | |
} | |
if (moveZAllowed) { | |
controls.moveForward(playerVelocity.z); // moveForward uses internal forward vector, so feed Z velocity | |
} | |
// Keep player at fixed height (no gravity/jump yet) | |
controls.getObject().position.y = PLAYER_HEIGHT; | |
// Log position occasionally | |
// if (Math.random() < 0.05) console.log("Player Pos:", controls.getObject().position); | |
} | |
// --- Event Handlers --- | |
function onKeyDown(event) { | |
// console.log("KeyDown:", event.code); // Debug key codes | |
switch (event.code) { | |
case 'ArrowUp': case 'KeyW': moveForward = true; break; | |
case 'ArrowLeft': case 'KeyA': moveLeft = true; break; | |
case 'ArrowDown': case 'KeyS': moveBackward = true; break; | |
case 'ArrowRight': case 'KeyD': moveRight = true; break; | |
// QWE ZXC movement not implemented in this simplified non-physics version yet | |
// Jump/F/Space not implemented yet | |
} | |
} | |
function onKeyUp(event) { | |
switch (event.code) { | |
case 'ArrowUp': case 'KeyW': moveForward = false; break; | |
case 'ArrowLeft': case 'KeyA': moveLeft = false; break; | |
case 'ArrowDown': case 'KeyS': moveBackward = false; break; | |
case 'ArrowRight': case 'KeyD': moveRight = false; break; | |
} | |
} | |
function onWindowResize() { | |
if (!camera || !renderer) return; | |
console.log("Resizing..."); | |
camera.aspect = window.innerWidth / window.innerHeight; | |
camera.updateProjectionMatrix(); | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
} | |
// --- UI Update Functions (Simplified) --- | |
function updateUI() { | |
// Display basic position for debugging | |
if (controls) { | |
const pos = controls.getObject().position; | |
statsElement.innerHTML = `<span>Pos: ${pos.x.toFixed(1)}, ${pos.y.toFixed(1)}, ${pos.z.toFixed(1)}</span>`; | |
} | |
// Inventory display can be added later | |
inventoryElement.innerHTML = '<em>Inventory N/A</em>'; | |
} | |
function addLog(message, type = "info") { | |
const p = document.createElement('p'); | |
p.classList.add(type); // Add class for styling | |
p.textContent = `[${new Date().toLocaleTimeString([], { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' })}] ${message}`; // Add timestamp | |
logElement.appendChild(p); | |
logElement.scrollTop = logElement.scrollHeight; // Auto-scroll | |
} | |
// --- Animation Loop --- | |
function animate() { | |
animationFrameId = requestAnimationFrame(animate); | |
const delta = clock.getDelta(); | |
// Update movement only if controls are locked | |
if (controls && controls.isLocked === true) { | |
handleInputAndMovement(delta); | |
updateUI(); // Update UI less frequently if needed | |
} | |
renderer.render(scene, camera); | |
} | |
// --- Start --- | |
console.log("Attempting to initialize..."); | |
try { | |
init(); | |
} catch(err) { | |
console.error("Initialization failed:", err); | |
alert("Error during initialization. Check the console (F12)."); | |
} | |
</script> | |
</body> | |
</html> |