awacke1 commited on
Commit
fce2d60
·
verified ·
1 Parent(s): fa40038

Update game.js

Browse files
Files changed (1) hide show
  1. game.js +215 -368
game.js CHANGED
@@ -1,6 +1,7 @@
1
  import * as THREE from 'three';
2
  import * as CANNON from 'cannon-es';
3
- // import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; // Optional for debugging
 
4
 
5
  // --- DOM Elements ---
6
  const sceneContainer = document.getElementById('scene-container');
@@ -12,8 +13,8 @@ const logElement = document.getElementById('log-display');
12
  const ROOM_SIZE = 10;
13
  const WALL_HEIGHT = 4;
14
  const WALL_THICKNESS = 0.5;
15
- const CAMERA_Y_OFFSET = 15; // Camera height
16
- const PLAYER_SPEED = 5; // Movement speed units/sec
17
  const PLAYER_RADIUS = 0.5;
18
  const PLAYER_HEIGHT = 1.8;
19
  const PROJECTILE_SPEED = 15;
@@ -21,38 +22,35 @@ const PROJECTILE_RADIUS = 0.2;
21
 
22
  // --- Three.js Setup ---
23
  let scene, camera, renderer;
24
- let playerMesh; // Visual representation of the player
25
- const meshesToSync = []; // Array to hold { mesh, body } pairs for sync
26
- const projectiles = []; // Active projectiles { mesh, body, lifetime }
 
27
 
28
  // --- Physics Setup ---
29
  let world;
30
- let playerBody; // Physics body for the player
31
- const physicsBodies = []; // Keep track of bodies to remove later if needed
32
- const cannonDebugRenderer = null; // Optional: Use cannon-es-debugger
33
 
34
  // --- Game State ---
35
- let gameState = {
36
- inventory: [],
37
- stats: { hp: 30, maxHp: 30, strength: 7, wisdom: 5, courage: 6 },
38
- position: { x: 0, z: 0 }, // Player's logical grid position (optional)
39
- monsters: [], // Store active monster data { id, hp, body, mesh, ... }
40
- items: [], // Store active item data { id, name, body, mesh, ... }
41
- };
42
- const keysPressed = {}; // Track currently pressed keys
43
 
44
- // --- Game Data (Keep relevant parts, add monster/item placements) ---
45
  const gameData = {
46
- // Structure: "x,y": { type, features, items?, monsters? }
47
- "0,0": { type: 'city', features: ['door_north'] },
48
- "0,1": { type: 'forest', features: ['path_north', 'door_south', 'item_potion'] }, // Add item marker
49
- "0,2": { type: 'forest', features: ['path_south', 'monster_goblin'] }, // Add monster marker
50
- // ... Add many more locations based on your design ...
 
51
  };
52
 
53
  const itemsData = {
54
  "Healing Potion": { type: "consumable", description: "Restores 10 HP.", hpRestore: 10, model: 'sphere_red' },
55
- "Key": { type: "quest", description: "Unlocks a door.", model: 'box_gold'},
56
  // ...
57
  };
58
 
@@ -64,23 +62,40 @@ const monstersData = {
64
  // --- Initialization ---
65
 
66
  function init() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  initThreeJS();
68
  initPhysics();
69
- initPlayer();
70
- generateMap(); // Generate based on gameData
71
  setupInputListeners();
72
- animate(); // Start the game loop
73
  updateUI(); // Initial UI update
74
  addLog("Welcome! Move with WASD/Arrows. Space to Attack.", "info");
 
 
 
75
  }
76
 
77
  function initThreeJS() {
 
78
  scene = new THREE.Scene();
79
  scene.background = new THREE.Color(0x111111);
80
 
81
  camera = new THREE.PerspectiveCamera(60, sceneContainer.clientWidth / sceneContainer.clientHeight, 0.1, 1000);
82
- // Start camera slightly offset, will follow player
83
- camera.position.set(0, CAMERA_Y_OFFSET, 5); // Look slightly forward initially
84
  camera.lookAt(0, 0, 0);
85
 
86
  renderer = new THREE.WebGLRenderer({ antialias: true });
@@ -89,140 +104,152 @@ function initThreeJS() {
89
  sceneContainer.appendChild(renderer.domElement);
90
 
91
  // Lighting
92
- const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
93
  scene.add(ambientLight);
94
- const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
95
- dirLight.position.set(10, 20, 5);
96
- dirLight.castShadow = true; // Enable shadows for this light
97
- scene.add(dirLight);
98
-
99
- // Configure shadow properties if needed
100
  dirLight.shadow.mapSize.width = 1024;
101
  dirLight.shadow.mapSize.height = 1024;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
 
103
  window.addEventListener('resize', onWindowResize, false);
104
  }
105
 
106
  function initPhysics() {
107
- world = new CANNON.World({
108
- gravity: new CANNON.Vec3(0, -9.82, 0) // Standard gravity
109
- });
110
- world.broadphase = new CANNON.NaiveBroadphase(); // Simple broadphase for now
111
- // world.solver.iterations = 10; // Adjust solver iterations if needed
112
 
113
- // Ground plane (physics only)
114
  const groundShape = new CANNON.Plane();
115
- const groundBody = new CANNON.Body({ mass: 0 }); // Mass 0 means static
116
  groundBody.addShape(groundShape);
117
- groundBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0); // Rotate plane to be horizontal
118
  world.addBody(groundBody);
 
 
 
 
 
 
 
 
 
 
 
 
119
  }
120
 
121
- // --- Primitive Assembly Functions ---
122
- function createPlayerMesh() {
123
  const group = new THREE.Group();
124
- // Simple capsule: cylinder + sphere cap
125
- const bodyMat = new THREE.MeshLambertMaterial({ color: 0x0077ff }); // Blue player
126
  const bodyGeom = new THREE.CylinderGeometry(PLAYER_RADIUS, PLAYER_RADIUS, PLAYER_HEIGHT - (PLAYER_RADIUS * 2), 16);
127
  const body = new THREE.Mesh(bodyGeom, bodyMat);
128
- body.position.y = PLAYER_RADIUS; // Position cylinder part correctly
129
  body.castShadow = true;
130
-
131
  const headGeom = new THREE.SphereGeometry(PLAYER_RADIUS, 16, 16);
132
  const head = new THREE.Mesh(headGeom, bodyMat);
133
- head.position.y = PLAYER_HEIGHT - PLAYER_RADIUS; // Position head on top
134
  head.castShadow = true;
135
-
136
- group.add(body);
137
- group.add(head);
138
-
139
- // Add a "front" indicator (e.g., small cone)
140
  const noseGeom = new THREE.ConeGeometry(PLAYER_RADIUS * 0.3, PLAYER_RADIUS * 0.5, 8);
141
- const noseMat = new THREE.MeshBasicMaterial({ color: 0xffff00 }); // Yellow nose
142
  const nose = new THREE.Mesh(noseGeom, noseMat);
143
- nose.position.set(0, PLAYER_HEIGHT * 0.7, PLAYER_RADIUS * 0.7); // Position in front, slightly down
144
- nose.rotation.x = Math.PI / 2; // Point forward
145
  group.add(nose);
146
-
147
  return group;
148
- }
149
-
150
- function createSimpleMonsterMesh(modelType = 'capsule_green') {
151
  const group = new THREE.Group();
152
- let color = 0xff0000; // Default red
153
- let geom;
154
- let mat;
155
-
156
  if (modelType === 'capsule_green') {
157
- color = 0x00ff00; // Green
158
  mat = new THREE.MeshLambertMaterial({ color: color });
159
  const bodyGeom = new THREE.CylinderGeometry(0.4, 0.4, 1.0, 12);
160
  const headGeom = new THREE.SphereGeometry(0.4, 12, 12);
161
  const body = new THREE.Mesh(bodyGeom, mat);
162
  const head = new THREE.Mesh(headGeom, mat);
163
- body.position.y = 0.5;
164
- head.position.y = 1.0 + 0.4;
165
- group.add(body);
166
- group.add(head);
167
- } else { // Default box monster
168
- geom = new THREE.BoxGeometry(0.8, 1.2, 0.8);
169
- mat = new THREE.MeshLambertMaterial({ color: color });
170
- const mesh = new THREE.Mesh(geom, mat);
171
- mesh.position.y = 0.6;
172
- group.add(mesh);
173
- }
174
  group.traverse(child => { if (child.isMesh) child.castShadow = true; });
175
  return group;
176
  }
177
-
178
- function createSimpleItemMesh(modelType = 'sphere_red') {
179
- let geom, mat;
180
- let color = 0xffffff; // Default white
181
-
182
- if(modelType === 'sphere_red') {
183
- color = 0xff0000;
184
- geom = new THREE.SphereGeometry(0.3, 16, 16);
185
- } else if (modelType === 'box_gold') {
186
- color = 0xffd700;
187
- geom = new THREE.BoxGeometry(0.4, 0.4, 0.4);
188
- } else { // Default sphere
189
- geom = new THREE.SphereGeometry(0.3, 16, 16);
190
- }
191
  mat = new THREE.MeshStandardMaterial({ color: color, metalness: 0.3, roughness: 0.6 });
192
- const mesh = new THREE.Mesh(geom, mat);
193
- mesh.position.y = PLAYER_RADIUS; // Place at player radius height
194
- mesh.castShadow = true;
195
  return mesh;
196
  }
197
-
198
- function createProjectileMesh() {
199
  const geom = new THREE.SphereGeometry(PROJECTILE_RADIUS, 8, 8);
200
- const mat = new THREE.MeshBasicMaterial({ color: 0xffff00 }); // Bright yellow
201
  const mesh = new THREE.Mesh(geom, mat);
202
  return mesh;
203
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
204
 
205
  // --- Player Setup ---
206
  function initPlayer() {
207
- // Visuals
 
208
  playerMesh = createPlayerMesh();
209
- playerMesh.position.y = PLAYER_HEIGHT / 2; // Adjust initial position based on model
 
210
  scene.add(playerMesh);
 
211
 
212
- // Physics
213
- // Using a capsule shape approximation (sphere + cylinder + sphere) is complex in Cannon.
214
- // Let's use a simpler Sphere or Box for now. Sphere is often better for rolling/movement.
215
  const playerShape = new CANNON.Sphere(PLAYER_RADIUS);
216
  playerBody = new CANNON.Body({
217
- mass: 5, // Give player some mass
218
  shape: playerShape,
219
- position: new CANNON.Vec3(0, PLAYER_HEIGHT / 2, 0), // Start at origin, slightly above ground
220
- linearDamping: 0.9, // Add damping to prevent sliding forever
221
- angularDamping: 0.9, // Prevent spinning wildly
222
- fixedRotation: true, // Prevent player body from tipping over (optional but good for top-down)
223
  });
224
- playerBody.addEventListener("collide", handlePlayerCollision); // Add collision listener
 
 
 
225
  world.addBody(playerBody);
 
226
 
227
  // Add to sync list
228
  meshesToSync.push({ mesh: playerMesh, body: playerBody });
@@ -230,291 +257,111 @@ function initPlayer() {
230
 
231
  // --- Map Generation ---
232
  function generateMap() {
233
- const wallMaterial = new CANNON.Material("wallMaterial"); // For physics interactions
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
 
235
  for (const coordString in gameData) {
236
  const [xStr, yStr] = coordString.split(',');
237
  const x = parseInt(xStr);
238
- const y = parseInt(yStr); // Represents Z in 3D
239
  const data = gameData[coordString];
 
240
 
241
- // Create Floor Mesh (visual only, physics ground plane handles floor collision)
242
- const floorMesh = createFloor(data.type, x, y);
243
- scene.add(floorMesh); // Add floor directly to scene, not mapGroup if physics handles it
 
 
 
 
 
244
 
245
  // Create Wall Meshes and Physics Bodies
246
  const features = data.features || [];
247
- const wallPositions = [
248
- { dir: 'north', xOffset: 0, zOffset: -0.5, geom: geometries.wall },
249
- { dir: 'south', xOffset: 0, zOffset: 0.5, geom: geometries.wall },
250
- { dir: 'east', xOffset: 0.5, zOffset: 0, geom: geometries.wall_side },
251
- { dir: 'west', xOffset: -0.5, zOffset: 0, geom: geometries.wall_side },
252
  ];
253
 
254
- wallPositions.forEach(wp => {
255
- // Check if a feature indicates an opening in this direction
256
- const doorFeature = `door_${wp.dir}`;
257
- const pathFeature = `path_${wp.dir}`; // Consider paths as openings too
258
  if (!features.includes(doorFeature) && !features.includes(pathFeature)) {
259
- // Add Visual Wall Mesh
260
- const wallMesh = new THREE.Mesh(wp.geom, materials.wall);
261
- wallMesh.position.set(
262
- x * ROOM_SIZE + wp.xOffset * ROOM_SIZE,
263
- WALL_HEIGHT / 2,
264
- y * ROOM_SIZE + wp.zOffset * ROOM_SIZE
265
- );
266
  wallMesh.castShadow = true;
267
  wallMesh.receiveShadow = true;
268
- scene.add(wallMesh); // Add walls directly to scene
269
-
270
- // Add Physics Wall Body
271
- const wallShape = new CANNON.Box(new CANNON.Vec3(
272
- wp.geom.parameters.width / 2,
273
- wp.geom.parameters.height / 2,
274
- wp.geom.parameters.depth / 2
275
- ));
276
- const wallBody = new CANNON.Body({
277
- mass: 0, // Static
278
- shape: wallShape,
279
- position: new CANNON.Vec3(wallMesh.position.x, wallMesh.position.y, wallMesh.position.z),
280
- material: wallMaterial // Assign physics material
281
- });
282
  world.addBody(wallBody);
283
- physicsBodies.push(wallBody); // Keep track if needed for removal
284
  }
285
  });
286
 
287
- // Spawn Items
288
- if (data.items) {
289
- data.items.forEach(itemName => spawnItem(itemName, x, y));
290
- }
291
- // Spawn Monsters
292
- if (data.monsters) {
293
- data.monsters.forEach(monsterType => spawnMonster(monsterType, x, y));
294
- }
295
-
296
- // Add other features visually (rivers, etc. - physics interaction TBD)
297
- features.forEach(feature => {
298
- if (feature === 'river') {
299
- const riverMesh = createFeature(feature, x, y);
 
 
 
 
 
 
 
300
  if (riverMesh) scene.add(riverMesh);
301
  }
302
- // Handle other features
303
- });
304
  }
 
305
  }
306
 
307
- function spawnItem(itemName, gridX, gridY) {
308
  const itemData = itemsData[itemName];
309
- if (!itemData) {
310
- console.warn(`Item data not found for ${itemName}`);
311
- return;
312
- }
313
-
314
- const x = gridX * ROOM_SIZE + (Math.random() - 0.5) * (ROOM_SIZE * 0.5); // Randomize position within cell
315
- const z = gridY * ROOM_SIZE + (Math.random() - 0.5) * (ROOM_SIZE * 0.5);
316
- const y = PLAYER_RADIUS; // Place at reachable height
317
-
318
- // Visual Mesh
319
  const mesh = createSimpleItemMesh(itemData.model);
320
  mesh.position.set(x, y, z);
321
- mesh.userData = { type: 'item', name: itemName }; // Store game data on mesh
322
  scene.add(mesh);
323
-
324
- // Physics Body (Static Sensor)
325
- const shape = new CANNON.Sphere(0.4); // Slightly larger than visual for easier pickup
326
- const body = new CANNON.Body({
327
- mass: 0,
328
- isTrigger: true, // Sensor - detects collision but doesn't cause physical reaction
329
- shape: shape,
330
- position: new CANNON.Vec3(x, y, z)
331
- });
332
- body.userData = { type: 'item', name: itemName, mesh }; // Link body back to mesh
333
  world.addBody(body);
334
-
335
  gameState.items.push({ id: body.id, name: itemName, body: body, mesh: mesh });
336
  physicsBodies.push(body);
337
- }
338
-
339
- function spawnMonster(monsterType, gridX, gridY) {
340
- const monsterData = monstersData[monsterType];
341
- if (!monsterData) {
342
- console.warn(`Monster data not found for ${monsterType}`);
343
- return;
344
- }
345
-
346
- const x = gridX * ROOM_SIZE + (Math.random() - 0.5) * (ROOM_SIZE * 0.3); // Randomize position
347
- const z = gridY * ROOM_SIZE + (Math.random() - 0.5) * (ROOM_SIZE * 0.3);
348
- const y = PLAYER_HEIGHT / 2; // Start at roughly player height
349
-
350
- // Visual Mesh
351
- const mesh = createSimpleMonsterMesh(monsterData.model);
352
- mesh.position.set(x, y, z);
353
- mesh.userData = { type: 'monster', monsterType: monsterType };
354
- scene.add(mesh);
355
-
356
- // Physics Body (Dynamic)
357
- // Using a simple sphere collider for monsters for now
358
- const shape = new CANNON.Sphere(PLAYER_RADIUS * 0.8); // Slightly smaller than player
359
- const body = new CANNON.Body({
360
- mass: 10, // Give mass
361
- shape: shape,
362
- position: new CANNON.Vec3(x, y, z),
363
- linearDamping: 0.8,
364
- angularDamping: 0.9,
365
- fixedRotation: true, // Prevent tipping
366
- });
367
- body.userData = { type: 'monster', monsterType: monsterType, mesh: mesh, hp: monsterData.hp }; // Store HP on body userData
368
- world.addBody(body);
369
-
370
- gameState.monsters.push({ id: body.id, type: monsterType, hp: monsterData.hp, body: body, mesh: mesh });
371
- meshesToSync.push({ mesh: mesh, body: body }); // Add monster to sync list
372
- physicsBodies.push(body);
373
- }
374
-
375
-
376
- // --- Input Handling ---
377
- function setupInputListeners() {
378
- window.addEventListener('keydown', (event) => {
379
- keysPressed[event.key.toLowerCase()] = true;
380
- keysPressed[event.code] = true; // Also store by code (e.g., Space)
381
- });
382
- window.addEventListener('keyup', (event) => {
383
- keysPressed[event.key.toLowerCase()] = false;
384
- keysPressed[event.code] = false;
385
- });
386
- }
387
-
388
- function handleInput(deltaTime) {
389
- if (!playerBody) return;
390
-
391
- const moveDirection = new CANNON.Vec3(0, 0, 0);
392
- const moveSpeed = PLAYER_SPEED;
393
-
394
- if (keysPressed['w'] || keysPressed['arrowup']) {
395
- moveDirection.z = -1;
396
- } else if (keysPressed['s'] || keysPressed['arrowdown']) {
397
- moveDirection.z = 1;
398
- }
399
-
400
- if (keysPressed['a'] || keysPressed['arrowleft']) {
401
- moveDirection.x = -1;
402
- } else if (keysPressed['d'] || keysPressed['arrowright']) {
403
- moveDirection.x = 1;
404
- }
405
-
406
- // Normalize diagonal movement and apply speed
407
- if (moveDirection.lengthSquared() > 0) { // Only normalize if there's movement
408
- moveDirection.normalize();
409
- // Apply velocity directly - better for responsive character control than forces
410
- playerBody.velocity.x = moveDirection.x * moveSpeed;
411
- playerBody.velocity.z = moveDirection.z * moveSpeed;
412
-
413
- // Make player mesh face movement direction (optional)
414
- const angle = Math.atan2(moveDirection.x, moveDirection.z);
415
- // Smooth rotation? Lerp quaternion later. For now, direct set.
416
- playerMesh.quaternion.setFromAxisAngle(new THREE.Vector3(0, 1, 0), angle);
417
-
418
- } else {
419
- // If no movement keys pressed, gradually stop (handled by linearDamping)
420
- // Or set velocity to zero for instant stop:
421
- // playerBody.velocity.x = 0;
422
- // playerBody.velocity.z = 0;
423
- }
424
-
425
- // Handle 'Fire' (Space bar)
426
- if (keysPressed['space']) {
427
- fireProjectile();
428
- keysPressed['space'] = false; // Prevent holding space for continuous fire (debounce)
429
- }
430
- }
431
-
432
- // --- Combat ---
433
- function fireProjectile() {
434
- if (!playerBody || !playerMesh) return;
435
- addLog("Pew!", "combat"); // Simple log
436
-
437
- // Create Mesh
438
- const projectileMesh = createProjectileMesh();
439
- // Create Physics Body
440
- const projectileShape = new CANNON.Sphere(PROJECTILE_RADIUS);
441
- const projectileBody = new CANNON.Body({
442
- mass: 0.1, // Very light
443
- shape: projectileShape,
444
- linearDamping: 0.01, // Minimal damping
445
- angularDamping: 0.01,
446
- });
447
- projectileBody.addEventListener("collide", handleProjectileCollision);
448
-
449
- // Calculate initial position and velocity
450
- // Start slightly in front of player, based on player's rotation
451
- const offsetDistance = PLAYER_RADIUS + PROJECTILE_RADIUS + 0.1; // Start just outside player radius
452
- const direction = new THREE.Vector3(0, 0, -1); // Base direction (forward Z)
453
- direction.applyQuaternion(playerMesh.quaternion); // Rotate based on player mesh orientation
454
-
455
- const startPos = new CANNON.Vec3().copy(playerBody.position).vadd(
456
- new CANNON.Vec3(direction.x, 0, direction.z).scale(offsetDistance) // Offset horizontally
457
- );
458
- startPos.y = PLAYER_HEIGHT * 0.7; // Fire from "head" height approx
459
-
460
- projectileBody.position.copy(startPos);
461
- projectileMesh.position.copy(startPos); // Sync initial mesh position
462
-
463
- // Set velocity in the direction the player is facing
464
- projectileBody.velocity = new CANNON.Vec3(direction.x, 0, direction.z).scale(PROJECTILE_SPEED);
465
-
466
- // Add to scene and world
467
- scene.add(projectileMesh);
468
- world.addBody(projectileBody);
469
-
470
- // Add to sync list and active projectiles list
471
- const projectileData = { mesh: projectileMesh, body: projectileBody, lifetime: 3.0 }; // 3 second lifetime
472
- meshesToSync.push(projectileData);
473
- projectiles.push(projectileData);
474
- physicsBodies.push(projectileBody);
475
-
476
- // Link body and mesh
477
- projectileBody.userData = { type: 'projectile', mesh: projectileMesh, data: projectileData };
478
- projectileMesh.userData = { type: 'projectile', body: projectileBody, data: projectileData };
479
- }
480
-
481
- // --- Collision Handling ---
482
- function handlePlayerCollision(event) {
483
- const otherBody = event.body; // The body the player collided with
484
- if (!otherBody.userData) return;
485
-
486
- // Player <-> Item Collision
487
- if (otherBody.userData.type === 'item') {
488
- const itemName = otherBody.userData.name;
489
- const itemIndex = gameState.items.findIndex(item => item.id === otherBody.id);
490
-
491
- if (itemIndex > -1 && !gameState.inventory.includes(itemName)) {
492
- gameState.inventory.push(itemName);
493
- addLog(`Picked up ${itemName}!`, "pickup");
494
- updateInventoryDisplay(); // Update UI immediately
495
-
496
- // Remove item from world
497
- scene.remove(otherBody.userData.mesh);
498
- world.removeBody(otherBody);
499
- meshesToSync = meshesToSync.filter(sync => sync.body.id !== otherBody.id);
500
- physicsBodies = physicsBodies.filter(body => body.id !== otherBody.id);
501
- gameState.items.splice(itemIndex, 1);
502
- }
503
- }
504
-
505
- // Player <-> Monster Collision (Simple damage placeholder)
506
- else if (otherBody.userData.type === 'monster') {
507
- // Example: Player takes damage on touch
508
- // Implement cooldown later
509
- gameState.stats.hp -= 1; // Monster touch damage
510
- addLog(`Touched by ${otherBody.userData.monsterType}! HP: ${gameState.stats.hp}`, "combat");
511
- updateStatsDisplay();
512
- if (gameState.stats.hp <= 0) {
513
- gameOver("Defeated by a monster!");
514
- }
515
- }
516
- }
517
-
518
- function handleProjectileCollision(event) {
519
- const projectileBody = event.target; // The projectile body itself
520
- const otherBody = event.body;
 
1
  import * as THREE from 'three';
2
  import * as CANNON from 'cannon-es';
3
+ // Uncomment for physics debugging:
4
+ // import CannonDebugger from 'cannon-es-debugger';
5
 
6
  // --- DOM Elements ---
7
  const sceneContainer = document.getElementById('scene-container');
 
13
  const ROOM_SIZE = 10;
14
  const WALL_HEIGHT = 4;
15
  const WALL_THICKNESS = 0.5;
16
+ const CAMERA_Y_OFFSET = 20; // Increased camera height for better overview
17
+ const PLAYER_SPEED = 5;
18
  const PLAYER_RADIUS = 0.5;
19
  const PLAYER_HEIGHT = 1.8;
20
  const PROJECTILE_SPEED = 15;
 
22
 
23
  // --- Three.js Setup ---
24
  let scene, camera, renderer;
25
+ let playerMesh;
26
+ const meshesToSync = [];
27
+ const projectiles = [];
28
+ let axesHelper; // For debugging orientation
29
 
30
  // --- Physics Setup ---
31
  let world;
32
+ let playerBody;
33
+ const physicsBodies = [];
34
+ let cannonDebugger = null; // Optional physics debugger instance
35
 
36
  // --- Game State ---
37
+ let gameState = {}; // Will be initialized in init()
38
+ const keysPressed = {};
39
+ let gameLoopActive = false; // Control the game loop
 
 
 
 
 
40
 
41
+ // --- Game Data (Ensure starting point 0,0 exists!) ---
42
  const gameData = {
43
+ "0,0": { type: 'city', features: ['door_north', 'item_Key'], name: "City Square"}, // Added item
44
+ "0,1": { type: 'forest', features: ['path_north', 'door_south', 'monster_goblin'], name: "Forest Path" }, // Added monster
45
+ "0,2": { type: 'forest', features: ['path_south'], name: "Deep Forest"},
46
+ "1,1": { type: 'forest', features: ['river', 'path_west'], name: "River Bend" },
47
+ "-1,1": { type: 'ruins', features: ['path_east', 'item_Healing Potion'], name: "Old Ruins"}, // Added item
48
+ // ... Add many more locations ...
49
  };
50
 
51
  const itemsData = {
52
  "Healing Potion": { type: "consumable", description: "Restores 10 HP.", hpRestore: 10, model: 'sphere_red' },
53
+ "Key": { type: "quest", description: "A rusty key.", model: 'box_gold'},
54
  // ...
55
  };
56
 
 
62
  // --- Initialization ---
63
 
64
  function init() {
65
+ console.log("Initializing Game...");
66
+ // Reset state cleanly
67
+ gameState = {
68
+ inventory: [],
69
+ stats: { hp: 30, maxHp: 30, strength: 7, wisdom: 5, courage: 6 },
70
+ position: { x: 0, z: 0 },
71
+ monsters: [],
72
+ items: [],
73
+ };
74
+ // Clear existing sync/physics lists if restarting
75
+ meshesToSync.length = 0;
76
+ projectiles.length = 0;
77
+ physicsBodies.length = 0;
78
+
79
+
80
  initThreeJS();
81
  initPhysics();
82
+ initPlayer(); // Must be after physics
83
+ generateMap(); // Generate map based on gameData
84
  setupInputListeners();
 
85
  updateUI(); // Initial UI update
86
  addLog("Welcome! Move with WASD/Arrows. Space to Attack.", "info");
87
+ console.log("Initialization Complete.");
88
+ gameLoopActive = true; // Start the loop AFTER setup
89
+ animate(); // Start the game loop
90
  }
91
 
92
  function initThreeJS() {
93
+ console.log("Initializing Three.js...");
94
  scene = new THREE.Scene();
95
  scene.background = new THREE.Color(0x111111);
96
 
97
  camera = new THREE.PerspectiveCamera(60, sceneContainer.clientWidth / sceneContainer.clientHeight, 0.1, 1000);
98
+ camera.position.set(0, CAMERA_Y_OFFSET, 5); // Start above origin
 
99
  camera.lookAt(0, 0, 0);
100
 
101
  renderer = new THREE.WebGLRenderer({ antialias: true });
 
104
  sceneContainer.appendChild(renderer.domElement);
105
 
106
  // Lighting
107
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
108
  scene.add(ambientLight);
109
+ const dirLight = new THREE.DirectionalLight(0xffffff, 1.0); // Brighter directional
110
+ dirLight.position.set(15, 25, 10); // Higher angle
111
+ dirLight.castShadow = true;
 
 
 
112
  dirLight.shadow.mapSize.width = 1024;
113
  dirLight.shadow.mapSize.height = 1024;
114
+ // Optional: Improve shadow quality/range
115
+ dirLight.shadow.camera.near = 0.5;
116
+ dirLight.shadow.camera.far = 50;
117
+ dirLight.shadow.camera.left = -ROOM_SIZE * 2;
118
+ dirLight.shadow.camera.right = ROOM_SIZE * 2;
119
+ dirLight.shadow.camera.top = ROOM_SIZE * 2;
120
+ dirLight.shadow.camera.bottom = -ROOM_SIZE * 2;
121
+
122
+ scene.add(dirLight);
123
+ // scene.add( new THREE.CameraHelper( dirLight.shadow.camera ) ); // Debug shadow camera
124
+
125
+ // Axes Helper
126
+ axesHelper = new THREE.AxesHelper(ROOM_SIZE / 2); // Size relative to room
127
+ axesHelper.position.set(0, 0.1, 0); // Slightly above ground
128
+ scene.add(axesHelper);
129
+ console.log("Three.js Initialized.");
130
 
131
  window.addEventListener('resize', onWindowResize, false);
132
  }
133
 
134
  function initPhysics() {
135
+ console.log("Initializing Cannon-es...");
136
+ world = new CANNON.World({ gravity: new CANNON.Vec3(0, -9.82, 0) });
137
+ world.broadphase = new CANNON.SAPBroadphase(world); // Generally better than Naive
138
+ world.allowSleep = true; // Allow bodies to sleep to save performance
 
139
 
140
+ // Ground plane
141
  const groundShape = new CANNON.Plane();
142
+ const groundBody = new CANNON.Body({ mass: 0, material: new CANNON.Material("ground") });
143
  groundBody.addShape(groundShape);
144
+ groundBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0);
145
  world.addBody(groundBody);
146
+ console.log("Physics World Initialized.");
147
+
148
+ // Optional: Physics Debugger
149
+ // try {
150
+ // cannonDebugger = new CannonDebugger(scene, world, {
151
+ // color: 0x00ff00, // Optional: specify color
152
+ // scale: 1.0, // Optional: scale
153
+ // });
154
+ // console.log("Cannon-es Debugger Initialized.");
155
+ // } catch (e) {
156
+ // console.error("Failed to initialize CannonDebugger. Did you include it?", e);
157
+ // }
158
  }
159
 
160
+ // --- Primitive Creation Functions (Keep as before) ---
161
+ function createPlayerMesh() { /* ... same as before ... */
162
  const group = new THREE.Group();
163
+ const bodyMat = new THREE.MeshLambertMaterial({ color: 0x0077ff });
 
164
  const bodyGeom = new THREE.CylinderGeometry(PLAYER_RADIUS, PLAYER_RADIUS, PLAYER_HEIGHT - (PLAYER_RADIUS * 2), 16);
165
  const body = new THREE.Mesh(bodyGeom, bodyMat);
166
+ body.position.y = PLAYER_RADIUS;
167
  body.castShadow = true;
 
168
  const headGeom = new THREE.SphereGeometry(PLAYER_RADIUS, 16, 16);
169
  const head = new THREE.Mesh(headGeom, bodyMat);
170
+ head.position.y = PLAYER_HEIGHT - PLAYER_RADIUS;
171
  head.castShadow = true;
172
+ group.add(body); group.add(head);
 
 
 
 
173
  const noseGeom = new THREE.ConeGeometry(PLAYER_RADIUS * 0.3, PLAYER_RADIUS * 0.5, 8);
174
+ const noseMat = new THREE.MeshBasicMaterial({ color: 0xffff00 });
175
  const nose = new THREE.Mesh(noseGeom, noseMat);
176
+ nose.position.set(0, PLAYER_HEIGHT * 0.7, -PLAYER_RADIUS * 0.7); // Point along negative Z initially
177
+ nose.rotation.x = Math.PI / 2 + Math.PI; // Adjust rotation to point forward (-Z)
178
  group.add(nose);
 
179
  return group;
180
+ }
181
+ function createSimpleMonsterMesh(modelType = 'capsule_green') { /* ... same as before ... */
 
182
  const group = new THREE.Group();
183
+ let color = 0xff0000;
184
+ let geom; let mat;
 
 
185
  if (modelType === 'capsule_green') {
186
+ color = 0x00ff00;
187
  mat = new THREE.MeshLambertMaterial({ color: color });
188
  const bodyGeom = new THREE.CylinderGeometry(0.4, 0.4, 1.0, 12);
189
  const headGeom = new THREE.SphereGeometry(0.4, 12, 12);
190
  const body = new THREE.Mesh(bodyGeom, mat);
191
  const head = new THREE.Mesh(headGeom, mat);
192
+ body.position.y = 0.5; head.position.y = 1.0 + 0.4;
193
+ group.add(body); group.add(head);
194
+ } else { 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); }
 
 
 
 
 
 
 
 
195
  group.traverse(child => { if (child.isMesh) child.castShadow = true; });
196
  return group;
197
  }
198
+ function createSimpleItemMesh(modelType = 'sphere_red') { /* ... same as before ... */
199
+ let geom, mat; let color = 0xffffff;
200
+ if(modelType === 'sphere_red') { color = 0xff0000; geom = new THREE.SphereGeometry(0.3, 16, 16); }
201
+ else if (modelType === 'box_gold') { color = 0xffd700; geom = new THREE.BoxGeometry(0.4, 0.4, 0.4); }
202
+ else { geom = new THREE.SphereGeometry(0.3, 16, 16); }
 
 
 
 
 
 
 
 
 
203
  mat = new THREE.MeshStandardMaterial({ color: color, metalness: 0.3, roughness: 0.6 });
204
+ const mesh = new THREE.Mesh(geom, mat); mesh.position.y = PLAYER_RADIUS; mesh.castShadow = true;
 
 
205
  return mesh;
206
  }
207
+ function createProjectileMesh() { /* ... same as before ... */
 
208
  const geom = new THREE.SphereGeometry(PROJECTILE_RADIUS, 8, 8);
209
+ const mat = new THREE.MeshBasicMaterial({ color: 0xffff00 });
210
  const mesh = new THREE.Mesh(geom, mat);
211
  return mesh;
212
  }
213
+ // Shared Geometries / Materials for walls/floors
214
+ const floorGeometry = new THREE.PlaneGeometry(ROOM_SIZE, ROOM_SIZE);
215
+ const wallN SGeometry = new THREE.BoxGeometry(ROOM_SIZE, WALL_HEIGHT, WALL_THICKNESS);
216
+ const wallEWGeometry = new THREE.BoxGeometry(WALL_THICKNESS, WALL_HEIGHT, ROOM_SIZE);
217
+ const floorMaterials = {
218
+ city: new THREE.MeshLambertMaterial({ color: 0xdeb887 }),
219
+ forest: new THREE.MeshLambertMaterial({ color: 0x228B22 }),
220
+ cave: new THREE.MeshLambertMaterial({ color: 0x696969 }),
221
+ ruins: new THREE.MeshLambertMaterial({ color: 0x778899 }),
222
+ plains: new THREE.MeshLambertMaterial({ color: 0x90EE90 }),
223
+ default: new THREE.MeshLambertMaterial({ color: 0xaaaaaa })
224
+ };
225
+ const wallMaterial = new THREE.MeshLambertMaterial({ color: 0x888888 });
226
 
227
  // --- Player Setup ---
228
  function initPlayer() {
229
+ console.log("Initializing Player...");
230
+ // Visual Mesh
231
  playerMesh = createPlayerMesh();
232
+ // Start slightly above ground to avoid initial collision issues
233
+ playerMesh.position.set(0, PLAYER_HEIGHT, 0); // Adjust initial Y based on model pivot
234
  scene.add(playerMesh);
235
+ console.log("Player mesh added to scene.");
236
 
237
+ // Physics Body
 
 
238
  const playerShape = new CANNON.Sphere(PLAYER_RADIUS);
239
  playerBody = new CANNON.Body({
240
+ mass: 70, // Player mass in kg (approx)
241
  shape: playerShape,
242
+ position: new CANNON.Vec3(0, PLAYER_HEIGHT, 0), // Match mesh Y
243
+ linearDamping: 0.95, // High damping for tight control
244
+ angularDamping: 1.0, // Prevent any spinning
245
+ material: new CANNON.Material("player") // Assign physics material
246
  });
247
+ // playerBody.fixedRotation = true; // Might not be needed with angular damping 1.0
248
+ playerBody.allowSleep = false; // Player should always be active
249
+
250
+ playerBody.addEventListener("collide", handlePlayerCollision);
251
  world.addBody(playerBody);
252
+ console.log("Player physics body added.");
253
 
254
  // Add to sync list
255
  meshesToSync.push({ mesh: playerMesh, body: playerBody });
 
257
 
258
  // --- Map Generation ---
259
  function generateMap() {
260
+ console.log("Generating Map...");
261
+ const staticMaterial = new CANNON.Material("static"); // Physics material for walls/floor
262
+
263
+ // Add physics ground plane (redundant with initPhysics but ensures material)
264
+ const groundShape = new CANNON.Plane();
265
+ const groundBody = new CANNON.Body({ mass: 0, material: staticMaterial });
266
+ groundBody.addShape(groundShape);
267
+ groundBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0);
268
+ world.addBody(groundBody);
269
+ physicsBodies.push(groundBody);
270
+
271
+ // Add visual grid helper (optional)
272
+ // const gridHelper = new THREE.GridHelper(ROOM_SIZE * 5, 5, 0x444444, 0x444444); // Size, Divisions
273
+ // scene.add(gridHelper);
274
+
275
 
276
  for (const coordString in gameData) {
277
  const [xStr, yStr] = coordString.split(',');
278
  const x = parseInt(xStr);
279
+ const z = parseInt(yStr); // Map Y is World Z
280
  const data = gameData[coordString];
281
+ const type = data.type || 'default';
282
 
283
+ // Create Floor Mesh
284
+ const floorMat = floorMaterials[type] || floorMaterials.default;
285
+ const floorMesh = new THREE.Mesh(floorGeometry, floorMat);
286
+ floorMesh.rotation.x = -Math.PI / 2;
287
+ floorMesh.position.set(x * ROOM_SIZE, 0, z * ROOM_SIZE);
288
+ floorMesh.receiveShadow = true;
289
+ scene.add(floorMesh);
290
+ // console.log(`Added floor at <span class="math-inline">\{x\},</span>{z}`);
291
 
292
  // Create Wall Meshes and Physics Bodies
293
  const features = data.features || [];
294
+ const wallDefs = [ // [direction, geometry, xOffset, zOffset]
295
+ ['north', wallNSGeometry, 0, -0.5],
296
+ ['south', wallNSGeometry, 0, 0.5],
297
+ ['east', wallEWGeometry, 0.5, 0],
298
+ ['west', wallEWGeometry, -0.5, 0],
299
  ];
300
 
301
+ wallDefs.forEach(([dir, geom, xOff, zOff]) => {
302
+ const doorFeature = `door_${dir}`;
303
+ const pathFeature = `path_${dir}`; // Consider paths as openings
 
304
  if (!features.includes(doorFeature) && !features.includes(pathFeature)) {
305
+ const wallX = x * ROOM_SIZE + xOff * ROOM_SIZE;
306
+ const wallZ = z * ROOM_SIZE + zOff * ROOM_SIZE;
307
+ const wallY = WALL_HEIGHT / 2;
308
+
309
+ // Visual Wall
310
+ const wallMesh = new THREE.Mesh(geom, wallMaterial);
311
+ wallMesh.position.set(wallX, wallY, wallZ);
312
  wallMesh.castShadow = true;
313
  wallMesh.receiveShadow = true;
314
+ scene.add(wallMesh);
315
+
316
+ // Physics Wall
317
+ const wallShape = new CANNON.Box(new CANNON.Vec3(geom.parameters.width / 2, geom.parameters.height / 2, geom.parameters.depth / 2));
318
+ const wallBody = new CANNON.Body({ mass: 0, shape: wallShape, position: new CANNON.Vec3(wallX, wallY, wallZ), material: staticMaterial });
 
 
 
 
 
 
 
 
 
319
  world.addBody(wallBody);
320
+ physicsBodies.push(wallBody);
321
  }
322
  });
323
 
324
+ // Spawn Items based on features like 'item_Key'
325
+ features.forEach(feature => {
326
+ if (feature.startsWith('item_')) {
327
+ const itemName = feature.substring(5).replace('_', ' '); // e.g., item_Healing_Potion -> Healing Potion
328
+ if(itemsData[itemName]) {
329
+ spawnItem(itemName, x, z);
330
+ } else {
331
+ console.warn(`Item feature found but no data for: ${itemName}`);
332
+ }
333
+ } else if (feature.startsWith('monster_')) {
334
+ const monsterType = feature.substring(8); // e.g., monster_goblin -> goblin
335
+ if(monstersData[monsterType]) {
336
+ spawnMonster(monsterType, x, z);
337
+ } else {
338
+ console.warn(`Monster feature found but no data for: ${monsterType}`);
339
+ }
340
+ }
341
+ // Handle other visual features (rivers etc.) - currently no physics
342
+ else if (feature === 'river') {
343
+ const riverMesh = createFeature(feature, x, z);
344
  if (riverMesh) scene.add(riverMesh);
345
  }
346
+ });
 
347
  }
348
+ console.log("Map Generation Complete.");
349
  }
350
 
351
+ function spawnItem(itemName, gridX, gridZ) { /* ... Mostly same as before ... */
352
  const itemData = itemsData[itemName];
353
+ if (!itemData) return;
354
+ const x = gridX * ROOM_SIZE + (Math.random() - 0.5) * (ROOM_SIZE * 0.4);
355
+ const z = gridZ * ROOM_SIZE + (Math.random() - 0.5) * (ROOM_SIZE * 0.4);
356
+ const y = PLAYER_RADIUS;
 
 
 
 
 
 
357
  const mesh = createSimpleItemMesh(itemData.model);
358
  mesh.position.set(x, y, z);
359
+ mesh.userData = { type: 'item', name: itemName };
360
  scene.add(mesh);
361
+ const shape = new CANNON.Sphere(PLAYER_RADIUS); // Make pickup radius same as player
362
+ const body = new CANNON.Body({ mass: 0, isTrigger: true, shape: shape, position: new CANNON.Vec3(x, y, z)});
363
+ body.userData = { type: 'item', name: itemName, mesh: mesh }; // Link back
 
 
 
 
 
 
 
364
  world.addBody(body);
 
365
  gameState.items.push({ id: body.id, name: itemName, body: body, mesh: mesh });
366
  physicsBodies.push(body);
367
+ console.log(`Spawned item ${