File size: 21,722 Bytes
2c77579
 
 
 
 
960a9eb
2c77579
b079db3
 
960a9eb
 
 
b079db3
 
 
 
 
960a9eb
b079db3
 
960a9eb
b079db3
 
 
2c77579
b079db3
 
 
 
 
 
 
 
 
 
 
 
960a9eb
 
 
 
b079db3
 
960a9eb
 
 
b079db3
960a9eb
 
 
b079db3
 
 
960a9eb
b079db3
960a9eb
b079db3
 
 
960a9eb
b079db3
 
 
960a9eb
 
b079db3
 
 
 
 
 
960a9eb
 
 
 
b079db3
 
960a9eb
b079db3
 
960a9eb
 
 
 
 
 
 
 
 
 
 
 
b079db3
960a9eb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b079db3
 
 
960a9eb
 
b079db3
 
960a9eb
b079db3
960a9eb
 
 
b079db3
 
 
 
960a9eb
 
b079db3
 
960a9eb
b079db3
 
 
960a9eb
 
b079db3
 
960a9eb
 
 
b079db3
 
960a9eb
 
b079db3
 
 
 
960a9eb
b079db3
 
960a9eb
 
 
b079db3
960a9eb
b079db3
 
960a9eb
b079db3
 
960a9eb
 
 
b079db3
960a9eb
 
 
 
b079db3
 
960a9eb
 
 
 
 
 
 
b079db3
960a9eb
b079db3
2c77579
 
960a9eb
 
 
 
 
 
 
 
 
 
 
b079db3
 
 
960a9eb
 
 
 
 
 
b079db3
2c77579
 
960a9eb
 
b079db3
 
 
960a9eb
b079db3
 
 
 
 
960a9eb
 
b079db3
 
 
 
 
960a9eb
 
2c77579
 
b079db3
960a9eb
 
 
 
 
 
 
 
 
b079db3
 
 
 
960a9eb
 
 
 
 
 
 
 
b079db3
 
 
 
960a9eb
 
 
 
b079db3
 
 
960a9eb
 
 
 
b079db3
 
 
960a9eb
 
 
 
b079db3
 
 
960a9eb
 
 
 
b079db3
 
 
 
960a9eb
 
 
2c77579
b079db3
 
960a9eb
b079db3
960a9eb
b079db3
 
960a9eb
 
b079db3
 
960a9eb
 
 
b079db3
 
960a9eb
 
 
b079db3
960a9eb
b079db3
 
960a9eb
 
b079db3
960a9eb
b079db3
960a9eb
b079db3
 
960a9eb
b079db3
960a9eb
 
 
 
 
 
 
 
 
 
 
 
 
 
b079db3
 
960a9eb
 
 
 
 
 
 
 
 
 
 
b079db3
 
960a9eb
 
 
 
 
 
 
b079db3
 
960a9eb
 
 
 
2c77579
 
b079db3
 
 
960a9eb
b079db3
 
 
 
 
960a9eb
 
b079db3
2c77579
 
b079db3
 
 
 
 
 
 
2c77579
b079db3
 
960a9eb
 
b079db3
 
 
2c77579
 
960a9eb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b079db3
 
960a9eb
49d65ee
b079db3
2c77579
960a9eb
 
 
 
 
b079db3
 
2c77579
 
b079db3
960a9eb
 
 
 
 
 
 
2c77579
b079db3
2c77579
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
<!DOCTYPE html>
<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>