Spaces:
Running
Running
Update index.html
Browse files- index.html +242 -241
index.html
CHANGED
@@ -3,48 +3,22 @@
|
|
3 |
<head>
|
4 |
<meta charset="UTF-8">
|
5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
-
<title>Procedural 3D Dungeon
|
7 |
<style>
|
8 |
body { margin: 0; overflow: hidden; background-color: #000; color: white; font-family: monospace; }
|
9 |
canvas { display: block; }
|
10 |
-
#blocker {
|
11 |
-
|
12 |
-
|
13 |
-
height: 100%;
|
14 |
-
background-color: rgba(0,0,0,0.5);
|
15 |
-
display: flex;
|
16 |
-
justify-content: center;
|
17 |
-
align-items: center;
|
18 |
-
cursor: pointer;
|
19 |
-
}
|
20 |
-
#instructions {
|
21 |
-
width: 50%;
|
22 |
-
text-align: center;
|
23 |
-
padding: 20px;
|
24 |
-
background: rgba(20, 20, 20, 0.8);
|
25 |
-
border-radius: 10px;
|
26 |
-
}
|
27 |
-
#crosshair {
|
28 |
-
position: absolute;
|
29 |
-
top: 50%;
|
30 |
-
left: 50%;
|
31 |
-
width: 10px;
|
32 |
-
height: 10px;
|
33 |
-
border: 1px solid white;
|
34 |
-
border-radius: 50%;
|
35 |
-
transform: translate(-50%, -50%);
|
36 |
-
pointer-events: none; /* Don't interfere with clicks */
|
37 |
-
mix-blend-mode: difference; /* Make visible on most backgrounds */
|
38 |
-
display: none; /* Hidden until pointer lock */
|
39 |
-
}
|
40 |
</style>
|
41 |
</head>
|
42 |
<body>
|
43 |
<div id="blocker">
|
44 |
<div id="instructions">
|
45 |
-
<h1>Dungeon Explorer</h1>
|
46 |
<p>Click to Enter</p>
|
47 |
<p>(W, A, S, D = Move, MOUSE = Look)</p>
|
|
|
48 |
</div>
|
49 |
</div>
|
50 |
<div id="crosshair">+</div>
|
@@ -61,348 +35,346 @@
|
|
61 |
<script type="module">
|
62 |
import * as THREE from 'three';
|
63 |
import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js';
|
64 |
-
|
|
|
|
|
|
|
65 |
|
66 |
// --- Config ---
|
67 |
-
const DUNGEON_WIDTH =
|
68 |
-
const DUNGEON_HEIGHT =
|
69 |
-
const CELL_SIZE = 5;
|
70 |
const WALL_HEIGHT = 4;
|
71 |
-
const PLAYER_HEIGHT = 1.6;
|
72 |
-
const PLAYER_RADIUS = 0.4;
|
73 |
-
const PLAYER_SPEED = 5.0;
|
74 |
-
const GENERATION_STEPS = 1500; // How long the random walk carves
|
75 |
|
76 |
// --- Three.js Setup ---
|
77 |
let scene, camera, renderer;
|
78 |
-
let controls;
|
79 |
let clock;
|
|
|
80 |
|
81 |
// --- Player State ---
|
82 |
const playerVelocity = new THREE.Vector3();
|
83 |
-
const playerDirection = new THREE.Vector3();
|
84 |
let moveForward = false, moveBackward = false, moveLeft = false, moveRight = false;
|
85 |
-
let playerOnGround = true; // Basic ground check for potential jump later
|
86 |
|
87 |
// --- World Data ---
|
88 |
-
let dungeonLayout = [];
|
89 |
-
|
90 |
-
let wallMesh = null;
|
91 |
|
92 |
// --- DOM Elements ---
|
93 |
const blocker = document.getElementById('blocker');
|
94 |
const instructions = document.getElementById('instructions');
|
95 |
const crosshair = document.getElementById('crosshair');
|
96 |
|
|
|
|
|
|
|
|
|
97 |
// --- Initialization ---
|
98 |
function init() {
|
99 |
-
console.log("Initializing
|
100 |
clock = new THREE.Clock();
|
101 |
|
102 |
-
//
|
103 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
104 |
scene.background = new THREE.Color(0x111111);
|
105 |
-
scene.fog = new THREE.Fog(0x111111,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
106 |
|
107 |
// Camera (First Person)
|
108 |
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
109 |
-
camera.position.y = PLAYER_HEIGHT;
|
110 |
-
console.log("Camera created");
|
111 |
-
|
112 |
-
// Renderer
|
113 |
-
renderer = new THREE.WebGLRenderer({ antialias: true });
|
114 |
-
renderer.setSize(window.innerWidth, window.innerHeight);
|
115 |
-
renderer.setPixelRatio(window.devicePixelRatio);
|
116 |
-
renderer.shadowMap.enabled = true;
|
117 |
-
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
118 |
-
document.body.appendChild(renderer.domElement);
|
119 |
-
console.log("Renderer created");
|
120 |
|
121 |
// Lighting
|
122 |
-
scene.add(new THREE.AmbientLight(0x404040, 0.
|
123 |
|
124 |
-
|
125 |
-
flashlight.position.set(0, 0, 0); //
|
126 |
-
flashlight.target.position.set(0, 0, -1); //
|
127 |
flashlight.castShadow = true;
|
128 |
flashlight.shadow.mapSize.width = 1024;
|
129 |
flashlight.shadow.mapSize.height = 1024;
|
130 |
flashlight.shadow.camera.near = 0.5;
|
131 |
-
flashlight.shadow.camera.far =
|
132 |
-
camera.add(flashlight);
|
133 |
camera.add(flashlight.target);
|
134 |
scene.add(camera); // Add camera (with light) to scene
|
135 |
-
console.log("Lighting setup");
|
136 |
|
137 |
// Pointer Lock Controls
|
138 |
controls = new PointerLockControls(camera, renderer.domElement);
|
139 |
-
|
|
|
140 |
|
141 |
blocker.addEventListener('click', () => { controls.lock(); });
|
142 |
-
controls.addEventListener('lock', () => {
|
143 |
-
|
144 |
-
|
145 |
-
crosshair.style.display = 'block';
|
146 |
-
});
|
147 |
-
controls.addEventListener('unlock', () => {
|
148 |
-
blocker.style.display = 'flex';
|
149 |
-
instructions.style.display = '';
|
150 |
-
crosshair.style.display = 'none';
|
151 |
-
});
|
152 |
|
153 |
// Keyboard Listeners
|
|
|
|
|
154 |
document.addEventListener('keydown', onKeyDown);
|
155 |
document.addEventListener('keyup', onKeyUp);
|
156 |
|
157 |
// Resize Listener
|
|
|
158 |
window.addEventListener('resize', onWindowResize);
|
159 |
|
160 |
-
// Generate
|
161 |
-
console.log("Generating dungeon layout...");
|
162 |
-
dungeonLayout =
|
163 |
console.log("Layout generated, creating meshes...");
|
164 |
-
|
165 |
console.log("Dungeon meshes created.");
|
166 |
|
167 |
-
//
|
168 |
const startPos = findStartPosition(dungeonLayout);
|
169 |
if (startPos) {
|
170 |
-
camera
|
171 |
-
|
172 |
-
console.log("Player start position set:", startPos);
|
173 |
} else {
|
174 |
-
console.error("Could not find valid start position!");
|
175 |
-
|
176 |
-
|
|
|
177 |
}
|
178 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
179 |
|
180 |
-
console.log("Initialization Complete
|
181 |
animate(); // Start the loop
|
182 |
}
|
183 |
|
184 |
-
// --- Dungeon Generation (
|
185 |
-
function
|
186 |
-
|
187 |
-
|
188 |
-
|
189 |
-
|
190 |
-
|
191 |
-
|
192 |
-
|
193 |
-
|
194 |
-
|
195 |
-
while (currentSteps < GENERATION_STEPS) {
|
196 |
-
const dir = directions[Math.floor(Math.random() * directions.length)];
|
197 |
-
const nextX = x + dir[0];
|
198 |
-
const nextY = y + dir[1];
|
199 |
-
|
200 |
-
// Check bounds (stay within grid, maybe leave border walls?)
|
201 |
-
if (nextX > 0 && nextX < width - 1 && nextY > 0 && nextY < height - 1) {
|
202 |
-
x = nextX;
|
203 |
-
y = nextY;
|
204 |
-
if (grid[y][x] === 0) {
|
205 |
-
grid[y][x] = 1; // Carve floor
|
206 |
-
currentSteps++;
|
207 |
}
|
208 |
-
// Allow walker to backtrack or carve adjacent cells more freely?
|
209 |
-
// Maybe carve a 2x1 area sometimes? For now, simple walk.
|
210 |
-
} else {
|
211 |
-
// Hit edge, pick new random start point on existing floor?
|
212 |
-
// Or just pick new direction? Let's just pick new direction.
|
213 |
-
// To avoid getting stuck, could randomly jump to another floor tile.
|
214 |
}
|
215 |
-
|
216 |
-
// Randomly change direction bias? (e.g. tend to go straight)
|
217 |
-
// Keep it simple for now.
|
218 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
219 |
return grid;
|
220 |
}
|
221 |
|
222 |
-
|
|
|
223 |
function findStartPosition(grid) {
|
224 |
-
// Find the first floor tile encountered, starting from center
|
225 |
const startY = Math.floor(grid.length / 2);
|
226 |
const startX = Math.floor(grid[0].length / 2);
|
227 |
-
|
228 |
-
// Simple search outwards from center
|
229 |
for (let r = 0; r < Math.max(startX, startY); r++) {
|
230 |
for (let y = startY - r; y <= startY + r; y++) {
|
231 |
for (let x = startX - r; x <= startX + r; x++) {
|
232 |
-
// Check only perimeter of radius r or center itself
|
233 |
if (Math.abs(y - startY) === r || Math.abs(x - startX) === r || r === 0) {
|
234 |
if (y >= 0 && y < grid.length && x >= 0 && x < grid[0].length && grid[y][x] === 1) {
|
235 |
-
|
|
|
236 |
}
|
237 |
}
|
238 |
}
|
239 |
}
|
240 |
}
|
241 |
-
|
|
|
242 |
}
|
243 |
|
244 |
|
245 |
-
// --- Dungeon Meshing ---
|
246 |
-
function
|
247 |
-
console.log("Creating
|
248 |
-
|
249 |
-
|
250 |
-
|
251 |
-
|
252 |
-
|
253 |
-
|
254 |
-
const wallGeoE = new THREE.BoxGeometry(0.1, WALL_HEIGHT, CELL_SIZE);
|
255 |
-
const wallGeoW = new THREE.BoxGeometry(0.1, WALL_HEIGHT, CELL_SIZE);
|
256 |
-
|
257 |
-
// Load Textures (Replace with actual URLs)
|
258 |
-
const textureLoader = new THREE.TextureLoader();
|
259 |
-
const floorTexture = textureLoader.load('https://threejs.org/examples/textures/hardwood2_diffuse.jpg'); // Placeholder wood floor
|
260 |
-
const wallTexture = textureLoader.load('https://threejs.org/examples/textures/brick_diffuse.jpg'); // Placeholder brick wall
|
261 |
-
floorTexture.wrapS = floorTexture.wrapT = THREE.RepeatWrapping;
|
262 |
-
wallTexture.wrapS = wallTexture.wrapT = THREE.RepeatWrapping;
|
263 |
-
// Adjust texture repeats based on CELL_SIZE if needed
|
264 |
-
// floorTexture.repeat.set(CELL_SIZE / 2, CELL_SIZE / 2);
|
265 |
-
// wallTexture.repeat.set(CELL_SIZE / 2, WALL_HEIGHT / 2);
|
266 |
-
|
267 |
-
const floorMaterial = new THREE.MeshStandardMaterial({ map: floorTexture, roughness: 0.8, metalness: 0.1 });
|
268 |
-
const wallMaterial = new THREE.MeshStandardMaterial({ map: wallTexture, roughness: 0.9, metalness: 0.0 });
|
269 |
|
270 |
for (let y = 0; y < grid.length; y++) {
|
271 |
for (let x = 0; x < grid[y].length; x++) {
|
272 |
if (grid[y][x] === 1) { // If it's a floor cell
|
273 |
-
// Create Floor Tile
|
274 |
-
const floorInstance =
|
275 |
-
floorInstance.
|
276 |
-
floorInstance.
|
277 |
-
|
|
|
|
|
|
|
278 |
|
279 |
// Check neighbors for Walls
|
280 |
// North Wall
|
281 |
if (y === 0 || grid[y - 1][x] === 0) {
|
282 |
-
const wallInstance =
|
283 |
-
wallInstance.
|
284 |
-
|
|
|
285 |
}
|
286 |
// South Wall
|
287 |
if (y === grid.length - 1 || grid[y + 1][x] === 0) {
|
288 |
-
|
289 |
-
wallInstance.
|
290 |
-
|
|
|
291 |
}
|
292 |
// West Wall
|
293 |
if (x === 0 || grid[y][x - 1] === 0) {
|
294 |
-
|
295 |
-
wallInstance.
|
296 |
-
|
|
|
297 |
}
|
298 |
// East Wall
|
299 |
if (x === grid[y].length - 1 || grid[y][x + 1] === 0) {
|
300 |
-
|
301 |
-
wallInstance.
|
302 |
-
|
|
|
303 |
}
|
304 |
}
|
305 |
}
|
306 |
}
|
307 |
-
|
308 |
-
//
|
309 |
-
|
310 |
-
const mergedFloorGeometry = BufferGeometryUtils.mergeGeometries(floorGeometries, false);
|
311 |
-
if (mergedFloorGeometry) {
|
312 |
-
floorMesh = new THREE.Mesh(mergedFloorGeometry, floorMaterial);
|
313 |
-
floorMesh.receiveShadow = true;
|
314 |
-
scene.add(floorMesh);
|
315 |
-
console.log("Merged floor mesh added.");
|
316 |
-
} else { console.error("Floor geometry merging failed."); }
|
317 |
-
|
318 |
-
}
|
319 |
-
if (wallGeometries.length > 0) {
|
320 |
-
const mergedWallGeometry = BufferGeometryUtils.mergeGeometries(wallGeometries, false);
|
321 |
-
if (mergedWallGeometry) {
|
322 |
-
wallMesh = new THREE.Mesh(mergedWallGeometry, wallMaterial);
|
323 |
-
wallMesh.castShadow = true;
|
324 |
-
wallMesh.receiveShadow = true; // Walls can receive shadows from other parts
|
325 |
-
scene.add(wallMesh);
|
326 |
-
console.log("Merged wall mesh added.");
|
327 |
-
} else { console.error("Wall geometry merging failed."); }
|
328 |
-
}
|
329 |
-
|
330 |
-
// Dispose of individual geometries to save memory
|
331 |
-
floorGeo.dispose();
|
332 |
-
wallGeoN.dispose();
|
333 |
-
wallGeoS.dispose();
|
334 |
-
wallGeoE.dispose();
|
335 |
-
wallGeoW.dispose();
|
336 |
-
floorGeometries.forEach(g => g.dispose());
|
337 |
-
wallGeometries.forEach(g => g.dispose());
|
338 |
-
|
339 |
-
console.log("Individual geometries disposed.");
|
340 |
}
|
341 |
|
342 |
|
343 |
-
// --- Player Movement & Collision ---
|
344 |
function handleInputAndMovement(deltaTime) {
|
345 |
-
if (!controls.isLocked) return;
|
346 |
|
347 |
const speed = PLAYER_SPEED * deltaTime;
|
348 |
-
|
|
|
349 |
playerVelocity.z = 0;
|
350 |
|
351 |
-
// Get camera direction
|
352 |
-
|
353 |
-
playerDirection.y = 0;
|
354 |
playerDirection.normalize();
|
355 |
|
356 |
-
|
|
|
|
|
357 |
|
358 |
-
//
|
359 |
if (moveForward) playerVelocity.add(playerDirection);
|
360 |
if (moveBackward) playerVelocity.sub(playerDirection);
|
361 |
-
if (moveLeft) playerVelocity.sub(rightDirection);
|
362 |
-
if (moveRight) playerVelocity.add(rightDirection);
|
363 |
|
364 |
-
// Normalize diagonal
|
365 |
if (playerVelocity.lengthSq() > 0) {
|
366 |
-
|
367 |
}
|
368 |
|
369 |
-
// --- Basic Collision Detection ---
|
370 |
const currentPos = controls.getObject().position;
|
371 |
-
|
372 |
-
|
373 |
-
|
374 |
-
|
375 |
-
|
376 |
-
|
377 |
-
|
378 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
379 |
}
|
380 |
|
381 |
-
// Check Z
|
382 |
-
|
383 |
-
|
384 |
-
|
385 |
-
playerVelocity.z
|
386 |
-
|
|
|
|
|
|
|
|
|
|
|
387 |
}
|
388 |
|
389 |
-
// Apply
|
390 |
-
|
391 |
-
|
|
|
|
|
|
|
|
|
392 |
|
393 |
// Keep player at fixed height (no gravity/jump yet)
|
394 |
-
|
|
|
|
|
|
|
395 |
}
|
396 |
|
397 |
|
398 |
// --- Event Handlers ---
|
399 |
function onKeyDown(event) {
|
|
|
400 |
switch (event.code) {
|
401 |
case 'ArrowUp': case 'KeyW': moveForward = true; break;
|
402 |
case 'ArrowLeft': case 'KeyA': moveLeft = true; break;
|
403 |
case 'ArrowDown': case 'KeyS': moveBackward = true; break;
|
404 |
case 'ArrowRight': case 'KeyD': moveRight = true; break;
|
405 |
-
//
|
|
|
406 |
}
|
407 |
}
|
408 |
|
@@ -416,26 +388,55 @@
|
|
416 |
}
|
417 |
|
418 |
function onWindowResize() {
|
|
|
|
|
419 |
camera.aspect = window.innerWidth / window.innerHeight;
|
420 |
camera.updateProjectionMatrix();
|
421 |
renderer.setSize(window.innerWidth, window.innerHeight);
|
422 |
}
|
423 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
424 |
// --- Animation Loop ---
|
425 |
function animate() {
|
426 |
-
animationFrameId = requestAnimationFrame(animate);
|
427 |
|
428 |
const delta = clock.getDelta();
|
429 |
|
430 |
-
|
431 |
-
|
432 |
-
|
|
|
|
|
433 |
|
434 |
renderer.render(scene, camera);
|
435 |
}
|
436 |
|
437 |
// --- Start ---
|
438 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
439 |
|
440 |
</script>
|
441 |
</body>
|
|
|
3 |
<head>
|
4 |
<meta charset="UTF-8">
|
5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
+
<title>DEBUG - Procedural 3D Dungeon</title>
|
7 |
<style>
|
8 |
body { margin: 0; overflow: hidden; background-color: #000; color: white; font-family: monospace; }
|
9 |
canvas { display: block; }
|
10 |
+
#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; }
|
11 |
+
#instructions { width: 50%; text-align: center; padding: 20px; background: rgba(20, 20, 20, 0.8); border-radius: 10px; }
|
12 |
+
#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; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
13 |
</style>
|
14 |
</head>
|
15 |
<body>
|
16 |
<div id="blocker">
|
17 |
<div id="instructions">
|
18 |
+
<h1>Dungeon Explorer (Debug Mode)</h1>
|
19 |
<p>Click to Enter</p>
|
20 |
<p>(W, A, S, D = Move, MOUSE = Look)</p>
|
21 |
+
<p>Check F12 Console for Errors!</p>
|
22 |
</div>
|
23 |
</div>
|
24 |
<div id="crosshair">+</div>
|
|
|
35 |
<script type="module">
|
36 |
import * as THREE from 'three';
|
37 |
import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js';
|
38 |
+
// BufferGeometryUtils not needed for this debug version
|
39 |
+
// import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js';
|
40 |
+
|
41 |
+
console.log("Script Start");
|
42 |
|
43 |
// --- Config ---
|
44 |
+
const DUNGEON_WIDTH = 10; // Smaller grid for debug
|
45 |
+
const DUNGEON_HEIGHT = 10;
|
46 |
+
const CELL_SIZE = 5;
|
47 |
const WALL_HEIGHT = 4;
|
48 |
+
const PLAYER_HEIGHT = 1.6;
|
49 |
+
const PLAYER_RADIUS = 0.4;
|
50 |
+
const PLAYER_SPEED = 5.0;
|
|
|
51 |
|
52 |
// --- Three.js Setup ---
|
53 |
let scene, camera, renderer;
|
54 |
+
let controls;
|
55 |
let clock;
|
56 |
+
let flashlight;
|
57 |
|
58 |
// --- Player State ---
|
59 |
const playerVelocity = new THREE.Vector3();
|
60 |
+
const playerDirection = new THREE.Vector3();
|
61 |
let moveForward = false, moveBackward = false, moveLeft = false, moveRight = false;
|
|
|
62 |
|
63 |
// --- World Data ---
|
64 |
+
let dungeonLayout = [];
|
65 |
+
const worldMeshes = []; // Store refs to added meshes for potential cleanup
|
|
|
66 |
|
67 |
// --- DOM Elements ---
|
68 |
const blocker = document.getElementById('blocker');
|
69 |
const instructions = document.getElementById('instructions');
|
70 |
const crosshair = document.getElementById('crosshair');
|
71 |
|
72 |
+
// --- Materials (Basic Colors) ---
|
73 |
+
const floorMaterial = new THREE.MeshLambertMaterial({ color: 0x555555 }); // Use Lambert for basic lighting check
|
74 |
+
const wallMaterial = new THREE.MeshLambertMaterial({ color: 0x884444 });
|
75 |
+
|
76 |
// --- Initialization ---
|
77 |
function init() {
|
78 |
+
console.log("--- Initializing Game ---");
|
79 |
clock = new THREE.Clock();
|
80 |
|
81 |
+
// Clear previous scene if restarting
|
82 |
+
if (scene) {
|
83 |
+
console.log("Clearing previous scene objects...");
|
84 |
+
worldMeshes.forEach(mesh => {
|
85 |
+
if(mesh.parent) scene.remove(mesh);
|
86 |
+
if(mesh.geometry) mesh.geometry.dispose();
|
87 |
+
// Only dispose material if we know it's unique per object
|
88 |
+
});
|
89 |
+
worldMeshes.length = 0; // Clear the array
|
90 |
+
} else {
|
91 |
+
scene = new THREE.Scene();
|
92 |
+
}
|
93 |
scene.background = new THREE.Color(0x111111);
|
94 |
+
scene.fog = new THREE.Fog(0x111111, 10, CELL_SIZE * 6);
|
95 |
+
|
96 |
+
// Clear previous renderer if restarting
|
97 |
+
if (renderer) {
|
98 |
+
console.log("Disposing previous renderer...");
|
99 |
+
renderer.dispose();
|
100 |
+
if (renderer.domElement.parentNode) {
|
101 |
+
renderer.domElement.parentNode.removeChild(renderer.domElement);
|
102 |
+
}
|
103 |
+
}
|
104 |
+
renderer = new THREE.WebGLRenderer({ antialias: true });
|
105 |
+
renderer.setSize(window.innerWidth, window.innerHeight);
|
106 |
+
renderer.setPixelRatio(window.devicePixelRatio);
|
107 |
+
renderer.shadowMap.enabled = true; // Keep shadows enabled
|
108 |
+
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
109 |
+
document.body.appendChild(renderer.domElement);
|
110 |
+
console.log("Renderer created/reset.");
|
111 |
+
|
112 |
|
113 |
// Camera (First Person)
|
114 |
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
115 |
+
camera.position.y = PLAYER_HEIGHT;
|
116 |
+
console.log("Camera created.");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
117 |
|
118 |
// Lighting
|
119 |
+
scene.add(new THREE.AmbientLight(0x404040, 0.8)); // Slightly brighter ambient
|
120 |
|
121 |
+
flashlight = new THREE.SpotLight(0xffffff, 3, 30, Math.PI / 5, 0.4, 1.5);
|
122 |
+
flashlight.position.set(0, 0, 0); // Relative to camera
|
123 |
+
flashlight.target.position.set(0, 0, -1); // Relative to camera
|
124 |
flashlight.castShadow = true;
|
125 |
flashlight.shadow.mapSize.width = 1024;
|
126 |
flashlight.shadow.mapSize.height = 1024;
|
127 |
flashlight.shadow.camera.near = 0.5;
|
128 |
+
flashlight.shadow.camera.far = 30;
|
129 |
+
camera.add(flashlight);
|
130 |
camera.add(flashlight.target);
|
131 |
scene.add(camera); // Add camera (with light) to scene
|
132 |
+
console.log("Lighting setup.");
|
133 |
|
134 |
// Pointer Lock Controls
|
135 |
controls = new PointerLockControls(camera, renderer.domElement);
|
136 |
+
// We don't add controls.getObject() directly to scene IF flashlight is child of camera
|
137 |
+
// scene.add(controls.getObject()); // Only if camera isn't manually added
|
138 |
|
139 |
blocker.addEventListener('click', () => { controls.lock(); });
|
140 |
+
controls.addEventListener('lock', () => { instructions.style.display = 'none'; blocker.style.display = 'none'; crosshair.style.display = 'block'; });
|
141 |
+
controls.addEventListener('unlock', () => { blocker.style.display = 'flex'; instructions.style.display = ''; crosshair.style.display = 'none'; });
|
142 |
+
console.log("Controls setup.");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
143 |
|
144 |
// Keyboard Listeners
|
145 |
+
document.removeEventListener('keydown', onKeyDown); // Remove old listeners if restarting
|
146 |
+
document.removeEventListener('keyup', onKeyUp);
|
147 |
document.addEventListener('keydown', onKeyDown);
|
148 |
document.addEventListener('keyup', onKeyUp);
|
149 |
|
150 |
// Resize Listener
|
151 |
+
window.removeEventListener('resize', onWindowResize); // Remove old
|
152 |
window.addEventListener('resize', onWindowResize);
|
153 |
|
154 |
+
// --- Generate FIXED Dungeon ---
|
155 |
+
console.log("Generating FIXED dungeon layout...");
|
156 |
+
dungeonLayout = generateFixedDungeonLayout(DUNGEON_WIDTH, DUNGEON_HEIGHT);
|
157 |
console.log("Layout generated, creating meshes...");
|
158 |
+
createDungeonMeshes_Direct(dungeonLayout); // Use direct mesh addition
|
159 |
console.log("Dungeon meshes created.");
|
160 |
|
161 |
+
// --- Set Player Start Position ---
|
162 |
const startPos = findStartPosition(dungeonLayout);
|
163 |
if (startPos) {
|
164 |
+
// Position the camera (which is the player view)
|
165 |
+
controls.getObject().position.set(startPos.x, PLAYER_HEIGHT, startPos.z);
|
166 |
+
console.log("Player start position set at:", startPos);
|
167 |
} else {
|
168 |
+
console.error("Could not find valid start position! Placing at center.");
|
169 |
+
const fallbackX = (DUNGEON_WIDTH / 2) * CELL_SIZE;
|
170 |
+
const fallbackZ = (DUNGEON_HEIGHT / 2) * CELL_SIZE;
|
171 |
+
controls.getObject().position.set(fallbackX, PLAYER_HEIGHT, fallbackZ);
|
172 |
}
|
173 |
|
174 |
+
// Add Axes Helper for orientation check
|
175 |
+
const axesHelper = new THREE.AxesHelper(CELL_SIZE);
|
176 |
+
axesHelper.position.copy(controls.getObject().position); // Place at start pos
|
177 |
+
axesHelper.position.y = 0.1;
|
178 |
+
scene.add(axesHelper);
|
179 |
+
worldMeshes.push(axesHelper); // Track for cleanup
|
180 |
+
|
181 |
|
182 |
+
console.log("--- Initialization Complete ---");
|
183 |
animate(); // Start the loop
|
184 |
}
|
185 |
|
186 |
+
// --- Dungeon Generation (FIXED LAYOUT) ---
|
187 |
+
function generateFixedDungeonLayout(width, height) {
|
188 |
+
console.log("Generating FIXED 5x5 layout for debugging...");
|
189 |
+
const grid = Array(height).fill(null).map(() => Array(width).fill(0)); // All walls
|
190 |
+
// Simple 5x5 room centered
|
191 |
+
const cx = Math.floor(width / 2);
|
192 |
+
const cy = Math.floor(height / 2);
|
193 |
+
for (let y = cy - 2; y <= cy + 2; y++) {
|
194 |
+
for (let x = cx - 2; x <= cx + 2; x++) {
|
195 |
+
if (y >= 0 && y < height && x >= 0 && x < width) {
|
196 |
+
grid[y][x] = 1; // Floor
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
197 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
198 |
}
|
|
|
|
|
|
|
199 |
}
|
200 |
+
// Add a corridor
|
201 |
+
for (let y = cy + 3; y < height -1 ; y++) {
|
202 |
+
if (grid[y]) grid[y][cx] = 1;
|
203 |
+
}
|
204 |
+
console.log(`Fixed Layout Generated (${width}x${height}). Center: ${cx},${cy}`);
|
205 |
+
// console.log("Grid:", grid.map(row => row.join('')).join('\n')); // Optional: Log grid visually
|
206 |
return grid;
|
207 |
}
|
208 |
|
209 |
+
|
210 |
+
// --- Find Start Position (Same as before) ---
|
211 |
function findStartPosition(grid) {
|
|
|
212 |
const startY = Math.floor(grid.length / 2);
|
213 |
const startX = Math.floor(grid[0].length / 2);
|
214 |
+
console.log(`Searching for start near ${startX},${startY}`);
|
|
|
215 |
for (let r = 0; r < Math.max(startX, startY); r++) {
|
216 |
for (let y = startY - r; y <= startY + r; y++) {
|
217 |
for (let x = startX - r; x <= startX + r; x++) {
|
|
|
218 |
if (Math.abs(y - startY) === r || Math.abs(x - startX) === r || r === 0) {
|
219 |
if (y >= 0 && y < grid.length && x >= 0 && x < grid[0].length && grid[y][x] === 1) {
|
220 |
+
console.log(`Found start floor at ${x},${y}`);
|
221 |
+
return { x: x * CELL_SIZE + CELL_SIZE / 2, z: y * CELL_SIZE + CELL_SIZE / 2 };
|
222 |
}
|
223 |
}
|
224 |
}
|
225 |
}
|
226 |
}
|
227 |
+
console.error("Valid start position (floor tile = 1) not found near center!");
|
228 |
+
return null; // Fallback
|
229 |
}
|
230 |
|
231 |
|
232 |
+
// --- Dungeon Meshing (DIRECT ADDITION - NO MERGING) ---
|
233 |
+
function createDungeonMeshes_Direct(grid) {
|
234 |
+
console.log("Creating meshes directly (no merging)...");
|
235 |
+
// Recreate geometries each time to avoid issues with disposed geometries if init is called again
|
236 |
+
const floorGeo = new THREE.PlaneGeometry(CELL_SIZE, CELL_SIZE);
|
237 |
+
const wallGeoN = new THREE.BoxGeometry(CELL_SIZE, WALL_HEIGHT, 0.1);
|
238 |
+
const wallGeoS = new THREE.BoxGeometry(CELL_SIZE, WALL_HEIGHT, 0.1);
|
239 |
+
const wallGeoE = new THREE.BoxGeometry(0.1, WALL_HEIGHT, CELL_SIZE);
|
240 |
+
const wallGeoW = new THREE.BoxGeometry(0.1, WALL_HEIGHT, CELL_SIZE);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
241 |
|
242 |
for (let y = 0; y < grid.length; y++) {
|
243 |
for (let x = 0; x < grid[y].length; x++) {
|
244 |
if (grid[y][x] === 1) { // If it's a floor cell
|
245 |
+
// Create Floor Tile Mesh
|
246 |
+
const floorInstance = new THREE.Mesh(floorGeo, floorMaterial); // Use shared geometry instance
|
247 |
+
floorInstance.rotation.x = -Math.PI / 2;
|
248 |
+
floorInstance.position.set(x * CELL_SIZE + CELL_SIZE / 2, 0, y * CELL_SIZE + CELL_SIZE / 2);
|
249 |
+
floorInstance.receiveShadow = true;
|
250 |
+
scene.add(floorInstance);
|
251 |
+
worldMeshes.push(floorInstance); // Track mesh
|
252 |
+
// console.log(`Added floor mesh at ${x},${y}`);
|
253 |
|
254 |
// Check neighbors for Walls
|
255 |
// North Wall
|
256 |
if (y === 0 || grid[y - 1][x] === 0) {
|
257 |
+
const wallInstance = new THREE.Mesh(wallGeoN, wallMaterial);
|
258 |
+
wallInstance.position.set(x * CELL_SIZE + CELL_SIZE / 2, WALL_HEIGHT / 2, y * CELL_SIZE);
|
259 |
+
wallInstance.castShadow = true; wallInstance.receiveShadow = true;
|
260 |
+
scene.add(wallInstance); worldMeshes.push(wallInstance);
|
261 |
}
|
262 |
// South Wall
|
263 |
if (y === grid.length - 1 || grid[y + 1][x] === 0) {
|
264 |
+
const wallInstance = new THREE.Mesh(wallGeoS, wallMaterial);
|
265 |
+
wallInstance.position.set(x * CELL_SIZE + CELL_SIZE / 2, WALL_HEIGHT / 2, y * CELL_SIZE + CELL_SIZE);
|
266 |
+
wallInstance.castShadow = true; wallInstance.receiveShadow = true;
|
267 |
+
scene.add(wallInstance); worldMeshes.push(wallInstance);
|
268 |
}
|
269 |
// West Wall
|
270 |
if (x === 0 || grid[y][x - 1] === 0) {
|
271 |
+
const wallInstance = new THREE.Mesh(wallGeoW, wallMaterial);
|
272 |
+
wallInstance.position.set(x * CELL_SIZE, WALL_HEIGHT / 2, y * CELL_SIZE + CELL_SIZE / 2);
|
273 |
+
wallInstance.castShadow = true; wallInstance.receiveShadow = true;
|
274 |
+
scene.add(wallInstance); worldMeshes.push(wallInstance);
|
275 |
}
|
276 |
// East Wall
|
277 |
if (x === grid[y].length - 1 || grid[y][x + 1] === 0) {
|
278 |
+
const wallInstance = new THREE.Mesh(wallGeoE, wallMaterial);
|
279 |
+
wallInstance.position.set(x * CELL_SIZE + CELL_SIZE, WALL_HEIGHT / 2, y * CELL_SIZE + CELL_SIZE / 2);
|
280 |
+
wallInstance.castShadow = true; wallInstance.receiveShadow = true;
|
281 |
+
scene.add(wallInstance); worldMeshes.push(wallInstance);
|
282 |
}
|
283 |
}
|
284 |
}
|
285 |
}
|
286 |
+
// Geometries are shared, no need to dispose here unless we cloned them.
|
287 |
+
// If we were cloning: floorGeo.dispose(); wallGeoN.dispose(); ...
|
288 |
+
console.log("Direct mesh creation complete.");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
289 |
}
|
290 |
|
291 |
|
292 |
+
// --- Player Movement & Collision (No Physics) ---
|
293 |
function handleInputAndMovement(deltaTime) {
|
294 |
+
if (!controls || !controls.isLocked) return;
|
295 |
|
296 |
const speed = PLAYER_SPEED * deltaTime;
|
297 |
+
// Reset velocity, we calculate total displacement based on keys
|
298 |
+
playerVelocity.x = 0;
|
299 |
playerVelocity.z = 0;
|
300 |
|
301 |
+
// Get camera direction (ignore Y)
|
302 |
+
controls.getDirection(playerDirection); // Gets normalized direction vector
|
303 |
+
playerDirection.y = 0;
|
304 |
playerDirection.normalize();
|
305 |
|
306 |
+
// Calculate right vector based on camera direction
|
307 |
+
const rightDirection = new THREE.Vector3();
|
308 |
+
rightDirection.crossVectors(camera.up, playerDirection).normalize(); // camera.up is (0,1,0)
|
309 |
|
310 |
+
// Apply movement based on keys
|
311 |
if (moveForward) playerVelocity.add(playerDirection);
|
312 |
if (moveBackward) playerVelocity.sub(playerDirection);
|
313 |
+
if (moveLeft) playerVelocity.sub(rightDirection);
|
314 |
+
if (moveRight) playerVelocity.add(rightDirection);
|
315 |
|
316 |
+
// Normalize diagonal velocity if needed and apply speed
|
317 |
if (playerVelocity.lengthSq() > 0) {
|
318 |
+
playerVelocity.normalize().multiplyScalar(speed);
|
319 |
}
|
320 |
|
321 |
+
// --- Basic Collision Detection BEFORE moving ---
|
322 |
const currentPos = controls.getObject().position;
|
323 |
+
let moveXAllowed = true;
|
324 |
+
let moveZAllowed = true;
|
325 |
+
|
326 |
+
// Check X Collision
|
327 |
+
if (playerVelocity.x !== 0) {
|
328 |
+
const nextX = currentPos.x + playerVelocity.x;
|
329 |
+
// Check slightly ahead in X direction, at feet and head level Z
|
330 |
+
const checkGridX = Math.floor((nextX + Math.sign(playerVelocity.x) * PLAYER_RADIUS) / CELL_SIZE);
|
331 |
+
const checkGridZFeet = Math.floor((currentPos.z - PLAYER_RADIUS) / CELL_SIZE);
|
332 |
+
const checkGridZHead = Math.floor((currentPos.z + PLAYER_RADIUS) / CELL_SIZE);
|
333 |
+
if ((dungeonLayout[checkGridZFeet]?.[checkGridX] === 0) || (dungeonLayout[checkGridZHead]?.[checkGridX] === 0)) {
|
334 |
+
moveXAllowed = false;
|
335 |
+
// console.log(`Collision X at grid ${checkGridX},${checkGridZFeet}/${checkGridZHead}`);
|
336 |
+
}
|
337 |
}
|
338 |
|
339 |
+
// Check Z Collision
|
340 |
+
if (playerVelocity.z !== 0) {
|
341 |
+
const nextZ = currentPos.z + playerVelocity.z;
|
342 |
+
// Check slightly ahead in Z direction, at feet and head level X
|
343 |
+
const checkGridZ = Math.floor((nextZ + Math.sign(playerVelocity.z) * PLAYER_RADIUS) / CELL_SIZE);
|
344 |
+
const checkGridXFeet = Math.floor((currentPos.x - PLAYER_RADIUS) / CELL_SIZE);
|
345 |
+
const checkGridXHead = Math.floor((currentPos.x + PLAYER_RADIUS) / CELL_SIZE);
|
346 |
+
if ((dungeonLayout[checkGridZ]?.[checkGridXFeet] === 0) || (dungeonLayout[checkGridZ]?.[checkGridXHead] === 0)) {
|
347 |
+
moveZAllowed = false;
|
348 |
+
// console.log(`Collision Z at grid ${checkGridXFeet}/${checkGridXHead},${checkGridZ}`);
|
349 |
+
}
|
350 |
}
|
351 |
|
352 |
+
// Apply movement only if allowed
|
353 |
+
if (moveXAllowed) {
|
354 |
+
controls.moveRight(playerVelocity.x); // moveRight uses internal right vector, so feed X velocity
|
355 |
+
}
|
356 |
+
if (moveZAllowed) {
|
357 |
+
controls.moveForward(playerVelocity.z); // moveForward uses internal forward vector, so feed Z velocity
|
358 |
+
}
|
359 |
|
360 |
// Keep player at fixed height (no gravity/jump yet)
|
361 |
+
controls.getObject().position.y = PLAYER_HEIGHT;
|
362 |
+
|
363 |
+
// Log position occasionally
|
364 |
+
// if (Math.random() < 0.05) console.log("Player Pos:", controls.getObject().position);
|
365 |
}
|
366 |
|
367 |
|
368 |
// --- Event Handlers ---
|
369 |
function onKeyDown(event) {
|
370 |
+
// console.log("KeyDown:", event.code); // Debug key codes
|
371 |
switch (event.code) {
|
372 |
case 'ArrowUp': case 'KeyW': moveForward = true; break;
|
373 |
case 'ArrowLeft': case 'KeyA': moveLeft = true; break;
|
374 |
case 'ArrowDown': case 'KeyS': moveBackward = true; break;
|
375 |
case 'ArrowRight': case 'KeyD': moveRight = true; break;
|
376 |
+
// QWE ZXC movement not implemented in this simplified non-physics version yet
|
377 |
+
// Jump/F/Space not implemented yet
|
378 |
}
|
379 |
}
|
380 |
|
|
|
388 |
}
|
389 |
|
390 |
function onWindowResize() {
|
391 |
+
if (!camera || !renderer) return;
|
392 |
+
console.log("Resizing...");
|
393 |
camera.aspect = window.innerWidth / window.innerHeight;
|
394 |
camera.updateProjectionMatrix();
|
395 |
renderer.setSize(window.innerWidth, window.innerHeight);
|
396 |
}
|
397 |
|
398 |
+
// --- UI Update Functions (Simplified) ---
|
399 |
+
function updateUI() {
|
400 |
+
// Display basic position for debugging
|
401 |
+
if (controls) {
|
402 |
+
const pos = controls.getObject().position;
|
403 |
+
statsElement.innerHTML = `<span>Pos: ${pos.x.toFixed(1)}, ${pos.y.toFixed(1)}, ${pos.z.toFixed(1)}</span>`;
|
404 |
+
}
|
405 |
+
// Inventory display can be added later
|
406 |
+
inventoryElement.innerHTML = '<em>Inventory N/A</em>';
|
407 |
+
}
|
408 |
+
function addLog(message, type = "info") {
|
409 |
+
const p = document.createElement('p');
|
410 |
+
p.classList.add(type); // Add class for styling
|
411 |
+
p.textContent = `[${new Date().toLocaleTimeString([], { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' })}] ${message}`; // Add timestamp
|
412 |
+
logElement.appendChild(p);
|
413 |
+
logElement.scrollTop = logElement.scrollHeight; // Auto-scroll
|
414 |
+
}
|
415 |
+
|
416 |
+
|
417 |
// --- Animation Loop ---
|
418 |
function animate() {
|
419 |
+
animationFrameId = requestAnimationFrame(animate);
|
420 |
|
421 |
const delta = clock.getDelta();
|
422 |
|
423 |
+
// Update movement only if controls are locked
|
424 |
+
if (controls && controls.isLocked === true) {
|
425 |
+
handleInputAndMovement(delta);
|
426 |
+
updateUI(); // Update UI less frequently if needed
|
427 |
+
}
|
428 |
|
429 |
renderer.render(scene, camera);
|
430 |
}
|
431 |
|
432 |
// --- Start ---
|
433 |
+
console.log("Attempting to initialize...");
|
434 |
+
try {
|
435 |
+
init();
|
436 |
+
} catch(err) {
|
437 |
+
console.error("Initialization failed:", err);
|
438 |
+
alert("Error during initialization. Check the console (F12).");
|
439 |
+
}
|
440 |
|
441 |
</script>
|
442 |
</body>
|