File size: 20,715 Bytes
908fc8e
fa40038
 
908fc8e
 
 
 
 
fa40038
908fc8e
 
fa40038
908fc8e
 
fa40038
 
 
 
 
 
908fc8e
 
 
fa40038
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
908fc8e
 
 
fa40038
908fc8e
 
fa40038
 
908fc8e
 
 
 
fa40038
908fc8e
 
 
fa40038
908fc8e
fa40038
 
 
 
908fc8e
fa40038
 
 
908fc8e
 
 
 
fa40038
 
 
 
 
 
 
 
 
 
 
 
 
908fc8e
 
fa40038
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
908fc8e
 
fa40038
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
908fc8e
 
fa40038
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
908fc8e
 
fa40038
 
 
 
908fc8e
 
 
fa40038
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
908fc8e
 
fa40038
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
908fc8e
fa40038
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
908fc8e
 
 
fa40038
 
 
 
908fc8e
 
 
fa40038
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
908fc8e
 
fa40038
 
 
 
 
908fc8e
 
fa40038
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
908fc8e
 
fa40038
 
908fc8e
fa40038
 
908fc8e
fa40038
 
 
 
908fc8e
 
fa40038
 
 
 
908fc8e
 
fa40038
 
 
 
 
 
908fc8e
fa40038
 
 
 
908fc8e
fa40038
 
 
 
 
908fc8e
 
fa40038
 
 
 
908fc8e
 
 
fa40038
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
908fc8e
fa40038
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
908fc8e
 
fa40038
 
 
 
 
 
 
 
 
 
908fc8e
 
 
fa40038
 
 
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
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
import * as THREE from 'three';
import * as CANNON from 'cannon-es';
// import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; // Optional for debugging

// --- DOM Elements ---
const sceneContainer = document.getElementById('scene-container');
const statsElement = document.getElementById('stats-display');
const inventoryElement = document.getElementById('inventory-display');
const logElement = document.getElementById('log-display');

// --- Config ---
const ROOM_SIZE = 10;
const WALL_HEIGHT = 4;
const WALL_THICKNESS = 0.5;
const CAMERA_Y_OFFSET = 15; // Camera height
const PLAYER_SPEED = 5; // Movement speed units/sec
const PLAYER_RADIUS = 0.5;
const PLAYER_HEIGHT = 1.8;
const PROJECTILE_SPEED = 15;
const PROJECTILE_RADIUS = 0.2;

// --- Three.js Setup ---
let scene, camera, renderer;
let playerMesh; // Visual representation of the player
const meshesToSync = []; // Array to hold { mesh, body } pairs for sync
const projectiles = []; // Active projectiles { mesh, body, lifetime }

// --- Physics Setup ---
let world;
let playerBody; // Physics body for the player
const physicsBodies = []; // Keep track of bodies to remove later if needed
const cannonDebugRenderer = null; // Optional: Use cannon-es-debugger

// --- Game State ---
let gameState = {
    inventory: [],
    stats: { hp: 30, maxHp: 30, strength: 7, wisdom: 5, courage: 6 },
    position: { x: 0, z: 0 }, // Player's logical grid position (optional)
    monsters: [], // Store active monster data { id, hp, body, mesh, ... }
    items: [], // Store active item data { id, name, body, mesh, ... }
};
const keysPressed = {}; // Track currently pressed keys

// --- Game Data (Keep relevant parts, add monster/item placements) ---
const gameData = {
    // Structure: "x,y": { type, features, items?, monsters? }
    "0,0": { type: 'city', features: ['door_north'] },
    "0,1": { type: 'forest', features: ['path_north', 'door_south', 'item_potion'] }, // Add item marker
    "0,2": { type: 'forest', features: ['path_south', 'monster_goblin'] }, // Add monster marker
    // ... Add many more locations based on your design ...
};

const itemsData = {
    "Healing Potion": { type: "consumable", description: "Restores 10 HP.", hpRestore: 10, model: 'sphere_red' },
    "Key": { type: "quest", description: "Unlocks a door.", model: 'box_gold'},
    // ...
};

const monstersData = {
    "goblin": { hp: 15, attack: 4, defense: 1, speed: 2, model: 'capsule_green', xp: 5 },
    // ...
};

// --- Initialization ---

function init() {
    initThreeJS();
    initPhysics();
    initPlayer();
    generateMap(); // Generate based on gameData
    setupInputListeners();
    animate(); // Start the game loop
    updateUI(); // Initial UI update
    addLog("Welcome! Move with WASD/Arrows. Space to Attack.", "info");
}

function initThreeJS() {
    scene = new THREE.Scene();
    scene.background = new THREE.Color(0x111111);

    camera = new THREE.PerspectiveCamera(60, sceneContainer.clientWidth / sceneContainer.clientHeight, 0.1, 1000);
    // Start camera slightly offset, will follow player
    camera.position.set(0, CAMERA_Y_OFFSET, 5); // Look slightly forward initially
    camera.lookAt(0, 0, 0);

    renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setSize(sceneContainer.clientWidth, sceneContainer.clientHeight);
    renderer.shadowMap.enabled = true;
    sceneContainer.appendChild(renderer.domElement);

    // Lighting
    const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
    scene.add(ambientLight);
    const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
    dirLight.position.set(10, 20, 5);
    dirLight.castShadow = true; // Enable shadows for this light
    scene.add(dirLight);

    // Configure shadow properties if needed
    dirLight.shadow.mapSize.width = 1024;
    dirLight.shadow.mapSize.height = 1024;

    window.addEventListener('resize', onWindowResize, false);
}

function initPhysics() {
    world = new CANNON.World({
        gravity: new CANNON.Vec3(0, -9.82, 0) // Standard gravity
    });
    world.broadphase = new CANNON.NaiveBroadphase(); // Simple broadphase for now
    // world.solver.iterations = 10; // Adjust solver iterations if needed

    // Ground plane (physics only)
    const groundShape = new CANNON.Plane();
    const groundBody = new CANNON.Body({ mass: 0 }); // Mass 0 means static
    groundBody.addShape(groundShape);
    groundBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0); // Rotate plane to be horizontal
    world.addBody(groundBody);
}

// --- Primitive Assembly Functions ---
function createPlayerMesh() {
    const group = new THREE.Group();
    // Simple capsule: cylinder + sphere cap
    const bodyMat = new THREE.MeshLambertMaterial({ color: 0x0077ff }); // Blue player
    const bodyGeom = new THREE.CylinderGeometry(PLAYER_RADIUS, PLAYER_RADIUS, PLAYER_HEIGHT - (PLAYER_RADIUS * 2), 16);
    const body = new THREE.Mesh(bodyGeom, bodyMat);
    body.position.y = PLAYER_RADIUS; // Position cylinder part correctly
    body.castShadow = true;

    const headGeom = new THREE.SphereGeometry(PLAYER_RADIUS, 16, 16);
    const head = new THREE.Mesh(headGeom, bodyMat);
    head.position.y = PLAYER_HEIGHT - PLAYER_RADIUS; // Position head on top
    head.castShadow = true;

    group.add(body);
    group.add(head);

    // Add a "front" indicator (e.g., small cone)
    const noseGeom = new THREE.ConeGeometry(PLAYER_RADIUS * 0.3, PLAYER_RADIUS * 0.5, 8);
    const noseMat = new THREE.MeshBasicMaterial({ color: 0xffff00 }); // Yellow nose
    const nose = new THREE.Mesh(noseGeom, noseMat);
    nose.position.set(0, PLAYER_HEIGHT * 0.7, PLAYER_RADIUS * 0.7); // Position in front, slightly down
    nose.rotation.x = Math.PI / 2; // Point forward
    group.add(nose);

    return group;
}

function createSimpleMonsterMesh(modelType = 'capsule_green') {
    const group = new THREE.Group();
    let color = 0xff0000; // Default red
    let geom;
    let mat;

    if (modelType === 'capsule_green') {
        color = 0x00ff00; // Green
        mat = new THREE.MeshLambertMaterial({ color: color });
        const bodyGeom = new THREE.CylinderGeometry(0.4, 0.4, 1.0, 12);
        const headGeom = new THREE.SphereGeometry(0.4, 12, 12);
        const body = new THREE.Mesh(bodyGeom, mat);
        const head = new THREE.Mesh(headGeom, mat);
        body.position.y = 0.5;
        head.position.y = 1.0 + 0.4;
        group.add(body);
        group.add(head);
    } else { // Default box monster
        geom = new THREE.BoxGeometry(0.8, 1.2, 0.8);
        mat = new THREE.MeshLambertMaterial({ color: color });
        const mesh = new THREE.Mesh(geom, mat);
        mesh.position.y = 0.6;
        group.add(mesh);
    }
    group.traverse(child => { if (child.isMesh) child.castShadow = true; });
    return group;
}

function createSimpleItemMesh(modelType = 'sphere_red') {
     let geom, mat;
     let color = 0xffffff; // Default white

     if(modelType === 'sphere_red') {
         color = 0xff0000;
         geom = new THREE.SphereGeometry(0.3, 16, 16);
     } else if (modelType === 'box_gold') {
          color = 0xffd700;
          geom = new THREE.BoxGeometry(0.4, 0.4, 0.4);
     } else { // Default sphere
          geom = new THREE.SphereGeometry(0.3, 16, 16);
     }
     mat = new THREE.MeshStandardMaterial({ color: color, metalness: 0.3, roughness: 0.6 });
     const mesh = new THREE.Mesh(geom, mat);
     mesh.position.y = PLAYER_RADIUS; // Place at player radius height
     mesh.castShadow = true;
     return mesh;
}

function createProjectileMesh() {
    const geom = new THREE.SphereGeometry(PROJECTILE_RADIUS, 8, 8);
    const mat = new THREE.MeshBasicMaterial({ color: 0xffff00 }); // Bright yellow
    const mesh = new THREE.Mesh(geom, mat);
    return mesh;
}

// --- Player Setup ---
function initPlayer() {
    // Visuals
    playerMesh = createPlayerMesh();
    playerMesh.position.y = PLAYER_HEIGHT / 2; // Adjust initial position based on model
    scene.add(playerMesh);

    // Physics
    // Using a capsule shape approximation (sphere + cylinder + sphere) is complex in Cannon.
    // Let's use a simpler Sphere or Box for now. Sphere is often better for rolling/movement.
    const playerShape = new CANNON.Sphere(PLAYER_RADIUS);
    playerBody = new CANNON.Body({
        mass: 5, // Give player some mass
        shape: playerShape,
        position: new CANNON.Vec3(0, PLAYER_HEIGHT / 2, 0), // Start at origin, slightly above ground
        linearDamping: 0.9, // Add damping to prevent sliding forever
        angularDamping: 0.9, // Prevent spinning wildly
        fixedRotation: true, // Prevent player body from tipping over (optional but good for top-down)
    });
     playerBody.addEventListener("collide", handlePlayerCollision); // Add collision listener
    world.addBody(playerBody);

    // Add to sync list
    meshesToSync.push({ mesh: playerMesh, body: playerBody });
}

// --- Map Generation ---
function generateMap() {
    const wallMaterial = new CANNON.Material("wallMaterial"); // For physics interactions

    for (const coordString in gameData) {
        const [xStr, yStr] = coordString.split(',');
        const x = parseInt(xStr);
        const y = parseInt(yStr); // Represents Z in 3D
        const data = gameData[coordString];

        // Create Floor Mesh (visual only, physics ground plane handles floor collision)
        const floorMesh = createFloor(data.type, x, y);
        scene.add(floorMesh); // Add floor directly to scene, not mapGroup if physics handles it

        // Create Wall Meshes and Physics Bodies
        const features = data.features || [];
        const wallPositions = [
            { dir: 'north', xOffset: 0, zOffset: -0.5, geom: geometries.wall },
            { dir: 'south', xOffset: 0, zOffset: 0.5, geom: geometries.wall },
            { dir: 'east', xOffset: 0.5, zOffset: 0, geom: geometries.wall_side },
            { dir: 'west', xOffset: -0.5, zOffset: 0, geom: geometries.wall_side },
        ];

        wallPositions.forEach(wp => {
            // Check if a feature indicates an opening in this direction
            const doorFeature = `door_${wp.dir}`;
            const pathFeature = `path_${wp.dir}`; // Consider paths as openings too
            if (!features.includes(doorFeature) && !features.includes(pathFeature)) {
                // Add Visual Wall Mesh
                const wallMesh = new THREE.Mesh(wp.geom, materials.wall);
                wallMesh.position.set(
                    x * ROOM_SIZE + wp.xOffset * ROOM_SIZE,
                    WALL_HEIGHT / 2,
                    y * ROOM_SIZE + wp.zOffset * ROOM_SIZE
                );
                wallMesh.castShadow = true;
                wallMesh.receiveShadow = true;
                scene.add(wallMesh); // Add walls directly to scene

                // Add Physics Wall Body
                const wallShape = new CANNON.Box(new CANNON.Vec3(
                    wp.geom.parameters.width / 2,
                    wp.geom.parameters.height / 2,
                    wp.geom.parameters.depth / 2
                ));
                const wallBody = new CANNON.Body({
                    mass: 0, // Static
                    shape: wallShape,
                    position: new CANNON.Vec3(wallMesh.position.x, wallMesh.position.y, wallMesh.position.z),
                    material: wallMaterial // Assign physics material
                });
                world.addBody(wallBody);
                physicsBodies.push(wallBody); // Keep track if needed for removal
            }
        });

        // Spawn Items
        if (data.items) {
            data.items.forEach(itemName => spawnItem(itemName, x, y));
        }
        // Spawn Monsters
         if (data.monsters) {
            data.monsters.forEach(monsterType => spawnMonster(monsterType, x, y));
         }

         // Add other features visually (rivers, etc. - physics interaction TBD)
         features.forEach(feature => {
             if (feature === 'river') {
                 const riverMesh = createFeature(feature, x, y);
                 if (riverMesh) scene.add(riverMesh);
             }
             // Handle other features
         });
    }
}

function spawnItem(itemName, gridX, gridY) {
    const itemData = itemsData[itemName];
    if (!itemData) {
        console.warn(`Item data not found for ${itemName}`);
        return;
    }

    const x = gridX * ROOM_SIZE + (Math.random() - 0.5) * (ROOM_SIZE * 0.5); // Randomize position within cell
    const z = gridY * ROOM_SIZE + (Math.random() - 0.5) * (ROOM_SIZE * 0.5);
    const y = PLAYER_RADIUS; // Place at reachable height

    // Visual Mesh
    const mesh = createSimpleItemMesh(itemData.model);
    mesh.position.set(x, y, z);
    mesh.userData = { type: 'item', name: itemName }; // Store game data on mesh
    scene.add(mesh);

    // Physics Body (Static Sensor)
    const shape = new CANNON.Sphere(0.4); // Slightly larger than visual for easier pickup
    const body = new CANNON.Body({
        mass: 0,
        isTrigger: true, // Sensor - detects collision but doesn't cause physical reaction
        shape: shape,
        position: new CANNON.Vec3(x, y, z)
    });
    body.userData = { type: 'item', name: itemName, mesh }; // Link body back to mesh
    world.addBody(body);

    gameState.items.push({ id: body.id, name: itemName, body: body, mesh: mesh });
    physicsBodies.push(body);
}

function spawnMonster(monsterType, gridX, gridY) {
     const monsterData = monstersData[monsterType];
     if (!monsterData) {
         console.warn(`Monster data not found for ${monsterType}`);
         return;
     }

     const x = gridX * ROOM_SIZE + (Math.random() - 0.5) * (ROOM_SIZE * 0.3); // Randomize position
     const z = gridY * ROOM_SIZE + (Math.random() - 0.5) * (ROOM_SIZE * 0.3);
     const y = PLAYER_HEIGHT / 2; // Start at roughly player height

     // Visual Mesh
     const mesh = createSimpleMonsterMesh(monsterData.model);
     mesh.position.set(x, y, z);
     mesh.userData = { type: 'monster', monsterType: monsterType };
     scene.add(mesh);

     // Physics Body (Dynamic)
     // Using a simple sphere collider for monsters for now
     const shape = new CANNON.Sphere(PLAYER_RADIUS * 0.8); // Slightly smaller than player
     const body = new CANNON.Body({
         mass: 10, // Give mass
         shape: shape,
         position: new CANNON.Vec3(x, y, z),
         linearDamping: 0.8,
         angularDamping: 0.9,
         fixedRotation: true, // Prevent tipping
     });
     body.userData = { type: 'monster', monsterType: monsterType, mesh: mesh, hp: monsterData.hp }; // Store HP on body userData
     world.addBody(body);

     gameState.monsters.push({ id: body.id, type: monsterType, hp: monsterData.hp, body: body, mesh: mesh });
     meshesToSync.push({ mesh: mesh, body: body }); // Add monster to sync list
     physicsBodies.push(body);
 }


// --- Input Handling ---
function setupInputListeners() {
    window.addEventListener('keydown', (event) => {
        keysPressed[event.key.toLowerCase()] = true;
         keysPressed[event.code] = true; // Also store by code (e.g., Space)
    });
    window.addEventListener('keyup', (event) => {
        keysPressed[event.key.toLowerCase()] = false;
         keysPressed[event.code] = false;
    });
}

function handleInput(deltaTime) {
    if (!playerBody) return;

    const moveDirection = new CANNON.Vec3(0, 0, 0);
    const moveSpeed = PLAYER_SPEED;

    if (keysPressed['w'] || keysPressed['arrowup']) {
        moveDirection.z = -1;
    } else if (keysPressed['s'] || keysPressed['arrowdown']) {
        moveDirection.z = 1;
    }

    if (keysPressed['a'] || keysPressed['arrowleft']) {
        moveDirection.x = -1;
    } else if (keysPressed['d'] || keysPressed['arrowright']) {
        moveDirection.x = 1;
    }

    // Normalize diagonal movement and apply speed
    if (moveDirection.lengthSquared() > 0) { // Only normalize if there's movement
        moveDirection.normalize();
        // Apply velocity directly - better for responsive character control than forces
        playerBody.velocity.x = moveDirection.x * moveSpeed;
        playerBody.velocity.z = moveDirection.z * moveSpeed;

        // Make player mesh face movement direction (optional)
         const angle = Math.atan2(moveDirection.x, moveDirection.z);
         // Smooth rotation? Lerp quaternion later. For now, direct set.
         playerMesh.quaternion.setFromAxisAngle(new THREE.Vector3(0, 1, 0), angle);

    } else {
        // If no movement keys pressed, gradually stop (handled by linearDamping)
        // Or set velocity to zero for instant stop:
        // playerBody.velocity.x = 0;
        // playerBody.velocity.z = 0;
    }

    // Handle 'Fire' (Space bar)
    if (keysPressed['space']) {
        fireProjectile();
        keysPressed['space'] = false; // Prevent holding space for continuous fire (debounce)
    }
}

// --- Combat ---
function fireProjectile() {
    if (!playerBody || !playerMesh) return;
    addLog("Pew!", "combat"); // Simple log

    // Create Mesh
    const projectileMesh = createProjectileMesh();
    // Create Physics Body
    const projectileShape = new CANNON.Sphere(PROJECTILE_RADIUS);
    const projectileBody = new CANNON.Body({
        mass: 0.1, // Very light
        shape: projectileShape,
        linearDamping: 0.01, // Minimal damping
        angularDamping: 0.01,
    });
    projectileBody.addEventListener("collide", handleProjectileCollision);

    // Calculate initial position and velocity
    // Start slightly in front of player, based on player's rotation
    const offsetDistance = PLAYER_RADIUS + PROJECTILE_RADIUS + 0.1; // Start just outside player radius
    const direction = new THREE.Vector3(0, 0, -1); // Base direction (forward Z)
    direction.applyQuaternion(playerMesh.quaternion); // Rotate based on player mesh orientation

    const startPos = new CANNON.Vec3().copy(playerBody.position).vadd(
        new CANNON.Vec3(direction.x, 0, direction.z).scale(offsetDistance) // Offset horizontally
    );
    startPos.y = PLAYER_HEIGHT * 0.7; // Fire from "head" height approx

    projectileBody.position.copy(startPos);
    projectileMesh.position.copy(startPos); // Sync initial mesh position

    // Set velocity in the direction the player is facing
    projectileBody.velocity = new CANNON.Vec3(direction.x, 0, direction.z).scale(PROJECTILE_SPEED);

    // Add to scene and world
    scene.add(projectileMesh);
    world.addBody(projectileBody);

    // Add to sync list and active projectiles list
    const projectileData = { mesh: projectileMesh, body: projectileBody, lifetime: 3.0 }; // 3 second lifetime
    meshesToSync.push(projectileData);
    projectiles.push(projectileData);
    physicsBodies.push(projectileBody);

    // Link body and mesh
    projectileBody.userData = { type: 'projectile', mesh: projectileMesh, data: projectileData };
    projectileMesh.userData = { type: 'projectile', body: projectileBody, data: projectileData };
}

// --- Collision Handling ---
function handlePlayerCollision(event) {
    const otherBody = event.body; // The body the player collided with
    if (!otherBody.userData) return;

    // Player <-> Item Collision
    if (otherBody.userData.type === 'item') {
        const itemName = otherBody.userData.name;
        const itemIndex = gameState.items.findIndex(item => item.id === otherBody.id);

        if (itemIndex > -1 && !gameState.inventory.includes(itemName)) {
            gameState.inventory.push(itemName);
            addLog(`Picked up ${itemName}!`, "pickup");
            updateInventoryDisplay(); // Update UI immediately

            // Remove item from world
            scene.remove(otherBody.userData.mesh);
            world.removeBody(otherBody);
            meshesToSync = meshesToSync.filter(sync => sync.body.id !== otherBody.id);
            physicsBodies = physicsBodies.filter(body => body.id !== otherBody.id);
            gameState.items.splice(itemIndex, 1);
        }
    }

    // Player <-> Monster Collision (Simple damage placeholder)
    else if (otherBody.userData.type === 'monster') {
        // Example: Player takes damage on touch
        // Implement cooldown later
        gameState.stats.hp -= 1; // Monster touch damage
        addLog(`Touched by ${otherBody.userData.monsterType}! HP: ${gameState.stats.hp}`, "combat");
        updateStatsDisplay();
         if (gameState.stats.hp <= 0) {
             gameOver("Defeated by a monster!");
         }
    }
}

function handleProjectileCollision(event) {
    const projectileBody = event.target; // The projectile body itself
    const otherBody = event.body;