awacke1's picture
Update index.html
b079db3 verified
raw
history blame
19.8 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Procedural 3D Dungeon Explorer</title>
<style>
body { margin: 0; overflow: hidden; background-color: #000; color: white; font-family: monospace; }
canvas { display: block; }
#blocker { /* For Pointer Lock API */
position: absolute;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
#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; /* Don't interfere with clicks */
mix-blend-mode: difference; /* Make visible on most backgrounds */
display: none; /* Hidden until pointer lock */
}
</style>
</head>
<body>
<div id="blocker">
<div id="instructions">
<h1>Dungeon Explorer</h1>
<p>Click to Enter</p>
<p>(W, A, S, D = Move, MOUSE = Look)</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';
import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js';
// --- Config ---
const DUNGEON_WIDTH = 30; // Size of grid (cells)
const DUNGEON_HEIGHT = 30;
const CELL_SIZE = 5; // Size of one cell in world units
const WALL_HEIGHT = 4;
const PLAYER_HEIGHT = 1.6; // Camera height offset from ground
const PLAYER_RADIUS = 0.4; // For collision
const PLAYER_SPEED = 5.0; // Units per second
const GENERATION_STEPS = 1500; // How long the random walk carves
// --- Three.js Setup ---
let scene, camera, renderer;
let controls; // PointerLockControls
let clock;
// --- Player State ---
const playerVelocity = new THREE.Vector3();
const playerDirection = new THREE.Vector3(); // Forward direction based on camera
let moveForward = false, moveBackward = false, moveLeft = false, moveRight = false;
let playerOnGround = true; // Basic ground check for potential jump later
// --- World Data ---
let dungeonLayout = []; // 2D grid: 0 = wall, 1 = floor
let floorMesh = null;
let wallMesh = null;
// --- DOM Elements ---
const blocker = document.getElementById('blocker');
const instructions = document.getElementById('instructions');
const crosshair = document.getElementById('crosshair');
// --- Initialization ---
function init() {
console.log("Initializing...");
clock = new THREE.Clock();
// Scene
scene = new THREE.Scene();
scene.background = new THREE.Color(0x111111);
scene.fog = new THREE.Fog(0x111111, 5, CELL_SIZE * 5); // Fog based on cell size
// Camera (First Person)
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.y = PLAYER_HEIGHT; // Set initial height
console.log("Camera created");
// Renderer
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);
console.log("Renderer created");
// Lighting
scene.add(new THREE.AmbientLight(0x404040, 0.5)); // Dim ambient light
const flashlight = new THREE.SpotLight(0xffffff, 3, 25, Math.PI / 6, 0.3, 1); // Intensity, Distance, Angle, Penumbra, Decay
flashlight.position.set(0, 0, 0); // Position relative to camera
flashlight.target.position.set(0, 0, -1); // Target 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 = 25;
camera.add(flashlight); // Attach light to camera
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);
scene.add(controls.getObject()); // Add the camera controls group
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';
});
// Keyboard Listeners
document.addEventListener('keydown', onKeyDown);
document.addEventListener('keyup', onKeyUp);
// Resize Listener
window.addEventListener('resize', onWindowResize);
// Generate and Build Dungeon
console.log("Generating dungeon layout...");
dungeonLayout = generateDungeonLayout(DUNGEON_WIDTH, DUNGEON_HEIGHT);
console.log("Layout generated, creating meshes...");
createDungeonMeshes(dungeonLayout);
console.log("Dungeon meshes created.");
// Find starting position for player
const startPos = findStartPosition(dungeonLayout);
if (startPos) {
camera.position.x = startPos.x;
camera.position.z = startPos.z;
console.log("Player start position set:", startPos);
} else {
console.error("Could not find valid start position!");
camera.position.x = (DUNGEON_WIDTH / 2) * CELL_SIZE; // Fallback
camera.position.z = (DUNGEON_HEIGHT / 2) * CELL_SIZE;
}
console.log("Initialization Complete.");
animate(); // Start the loop
}
// --- Dungeon Generation (Random Walk) ---
function generateDungeonLayout(width, height) {
const grid = Array(height).fill(null).map(() => Array(width).fill(0)); // 0 = wall
let x = Math.floor(width / 2);
let y = Math.floor(height / 2);
let currentSteps = 0;
grid[y][x] = 1; // Start point is floor
const directions = [ [0, -1], [0, 1], [-1, 0], [1, 0] ]; // N, S, W, E
while (currentSteps < GENERATION_STEPS) {
const dir = directions[Math.floor(Math.random() * directions.length)];
const nextX = x + dir[0];
const nextY = y + dir[1];
// Check bounds (stay within grid, maybe leave border walls?)
if (nextX > 0 && nextX < width - 1 && nextY > 0 && nextY < height - 1) {
x = nextX;
y = nextY;
if (grid[y][x] === 0) {
grid[y][x] = 1; // Carve floor
currentSteps++;
}
// Allow walker to backtrack or carve adjacent cells more freely?
// Maybe carve a 2x1 area sometimes? For now, simple walk.
} else {
// Hit edge, pick new random start point on existing floor?
// Or just pick new direction? Let's just pick new direction.
// To avoid getting stuck, could randomly jump to another floor tile.
}
// Randomly change direction bias? (e.g. tend to go straight)
// Keep it simple for now.
}
return grid;
}
// --- Find Start Position ---
function findStartPosition(grid) {
// Find the first floor tile encountered, starting from center
const startY = Math.floor(grid.length / 2);
const startX = Math.floor(grid[0].length / 2);
// Simple search outwards from center
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++) {
// Check only perimeter of radius r or center itself
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) {
return { x: x * CELL_SIZE + CELL_SIZE / 2, z: y * CELL_SIZE + CELL_SIZE / 2 }; // Center of cell
}
}
}
}
}
return null; // Should not happen if generation works
}
// --- Dungeon Meshing ---
function createDungeonMeshes(grid) {
console.log("Creating merged geometries...");
const floorGeometries = [];
const wallGeometries = [];
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);
// Load Textures (Replace with actual URLs)
const textureLoader = new THREE.TextureLoader();
const floorTexture = textureLoader.load('https://threejs.org/examples/textures/hardwood2_diffuse.jpg'); // Placeholder wood floor
const wallTexture = textureLoader.load('https://threejs.org/examples/textures/brick_diffuse.jpg'); // Placeholder brick wall
floorTexture.wrapS = floorTexture.wrapT = THREE.RepeatWrapping;
wallTexture.wrapS = wallTexture.wrapT = THREE.RepeatWrapping;
// Adjust texture repeats based on CELL_SIZE if needed
// floorTexture.repeat.set(CELL_SIZE / 2, CELL_SIZE / 2);
// wallTexture.repeat.set(CELL_SIZE / 2, WALL_HEIGHT / 2);
const floorMaterial = new THREE.MeshStandardMaterial({ map: floorTexture, roughness: 0.8, metalness: 0.1 });
const wallMaterial = new THREE.MeshStandardMaterial({ map: wallTexture, roughness: 0.9, metalness: 0.0 });
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 Geometry
const floorInstance = floorGeo.clone();
floorInstance.rotateX(-Math.PI / 2);
floorInstance.translate(x * CELL_SIZE + CELL_SIZE / 2, 0, y * CELL_SIZE + CELL_SIZE / 2);
floorGeometries.push(floorInstance);
// Check neighbors for Walls
// North Wall
if (y === 0 || grid[y - 1][x] === 0) {
const wallInstance = wallGeoN.clone();
wallInstance.translate(x * CELL_SIZE + CELL_SIZE / 2, WALL_HEIGHT / 2, y * CELL_SIZE);
wallGeometries.push(wallInstance);
}
// South Wall
if (y === grid.length - 1 || grid[y + 1][x] === 0) {
const wallInstance = wallGeoS.clone();
wallInstance.translate(x * CELL_SIZE + CELL_SIZE / 2, WALL_HEIGHT / 2, y * CELL_SIZE + CELL_SIZE);
wallGeometries.push(wallInstance);
}
// West Wall
if (x === 0 || grid[y][x - 1] === 0) {
const wallInstance = wallGeoW.clone();
wallInstance.translate(x * CELL_SIZE, WALL_HEIGHT / 2, y * CELL_SIZE + CELL_SIZE / 2);
wallGeometries.push(wallInstance);
}
// East Wall
if (x === grid[y].length - 1 || grid[y][x + 1] === 0) {
const wallInstance = wallGeoE.clone();
wallInstance.translate(x * CELL_SIZE + CELL_SIZE, WALL_HEIGHT / 2, y * CELL_SIZE + CELL_SIZE / 2);
wallGeometries.push(wallInstance);
}
}
}
}
// Merge Geometries
if (floorGeometries.length > 0) {
const mergedFloorGeometry = BufferGeometryUtils.mergeGeometries(floorGeometries, false);
if (mergedFloorGeometry) {
floorMesh = new THREE.Mesh(mergedFloorGeometry, floorMaterial);
floorMesh.receiveShadow = true;
scene.add(floorMesh);
console.log("Merged floor mesh added.");
} else { console.error("Floor geometry merging failed."); }
}
if (wallGeometries.length > 0) {
const mergedWallGeometry = BufferGeometryUtils.mergeGeometries(wallGeometries, false);
if (mergedWallGeometry) {
wallMesh = new THREE.Mesh(mergedWallGeometry, wallMaterial);
wallMesh.castShadow = true;
wallMesh.receiveShadow = true; // Walls can receive shadows from other parts
scene.add(wallMesh);
console.log("Merged wall mesh added.");
} else { console.error("Wall geometry merging failed."); }
}
// Dispose of individual geometries to save memory
floorGeo.dispose();
wallGeoN.dispose();
wallGeoS.dispose();
wallGeoE.dispose();
wallGeoW.dispose();
floorGeometries.forEach(g => g.dispose());
wallGeometries.forEach(g => g.dispose());
console.log("Individual geometries disposed.");
}
// --- Player Movement & Collision ---
function handleInputAndMovement(deltaTime) {
if (!controls.isLocked) return;
const speed = PLAYER_SPEED * deltaTime;
playerVelocity.x = 0; // Reset velocity each frame (using direct position change)
playerVelocity.z = 0;
// Get camera direction
camera.getWorldDirection(playerDirection);
playerDirection.y = 0; // Ignore vertical component for movement
playerDirection.normalize();
const rightDirection = new THREE.Vector3().crossVectors(camera.up, playerDirection).normalize();
// Calculate movement based on keys
if (moveForward) playerVelocity.add(playerDirection);
if (moveBackward) playerVelocity.sub(playerDirection);
if (moveLeft) playerVelocity.sub(rightDirection); // Strafe left
if (moveRight) playerVelocity.add(rightDirection); // Strafe right
// Normalize diagonal movement speed if necessary (optional but good)
if (playerVelocity.lengthSq() > 0) {
playerVelocity.normalize().multiplyScalar(speed);
}
// --- Basic Collision Detection ---
const currentPos = controls.getObject().position;
const nextPos = currentPos.clone().add(playerVelocity); // Potential next position
// Check X movement collision
const targetGridX_X = Math.floor((nextPos.x + Math.sign(playerVelocity.x) * PLAYER_RADIUS) / CELL_SIZE);
const currentGridZ_X = Math.floor(currentPos.z / CELL_SIZE);
if (dungeonLayout[currentGridZ_X]?.[targetGridX_X] === 0) { // Hit wall in X direction
playerVelocity.x = 0; // Stop X movement
console.log("Collision X");
}
// Check Z movement collision
const currentGridX_Z = Math.floor(currentPos.x / CELL_SIZE);
const targetGridZ_Z = Math.floor((nextPos.z + Math.sign(playerVelocity.z) * PLAYER_RADIUS) / CELL_SIZE);
if (dungeonLayout[targetGridZ_Z]?.[currentGridX_Z] === 0) { // Hit wall in Z direction
playerVelocity.z = 0; // Stop Z movement
console.log("Collision Z");
}
// Apply final velocity to camera controls object
controls.moveRight(playerVelocity.x); // Use moveRight for strafing based on calculated velocity X
controls.moveForward(playerVelocity.z); // Use moveForward for Z based on calculated velocity Z
// Keep player at fixed height (no gravity/jump yet)
controls.getObject().position.y = PLAYER_HEIGHT;
}
// --- Event Handlers ---
function onKeyDown(event) {
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;
// Add jump/other actions later
}
}
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() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
// --- Animation Loop ---
function animate() {
animationFrameId = requestAnimationFrame(animate); // Store frame ID
const delta = clock.getDelta();
handleInputAndMovement(delta);
// Update other game elements (monsters, etc.) here later
renderer.render(scene, camera);
}
// --- Start ---
init();
</script>
</body>
</html>