awacke1 commited on
Commit
1e5f49f
·
verified ·
1 Parent(s): f22aa2f

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +163 -398
index.html CHANGED
@@ -3,12 +3,11 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Procedural World Grid Combat</title>
7
  <style>
8
- /* Base styles from previous version */
9
  body { font-family: 'Courier New', monospace; background-color: #111; color: #eee; margin: 0; padding: 0; overflow: hidden; display: flex; flex-direction: column; height: 100vh; }
10
  #game-container { display: flex; flex-grow: 1; overflow: hidden; }
11
- #scene-container { flex-grow: 3; position: relative; border-right: 2px solid #444; min-width: 250px; background-color: #000; height: 100%; box-sizing: border-box; overflow: hidden; cursor: crosshair; }
12
  #ui-container { flex-grow: 2; padding: 25px; overflow-y: auto; background-color: #2b2b2b; min-width: 320px; height: 100%; box-sizing: border-box; display: flex; flex-direction: column; }
13
  #scene-container canvas { display: block; }
14
  #story-title { color: #f0c060; margin: 0 0 15px 0; padding-bottom: 10px; border-bottom: 1px solid #555; font-size: 1.6em; text-shadow: 1px 1px 1px #000; }
@@ -18,44 +17,29 @@
18
  #stats-display span, #inventory-display .item-tag { display: inline-block; background-color: #484848; padding: 3px 9px; border-radius: 15px; margin: 0 8px 5px 0; border: 1px solid #6a6a6a; white-space: nowrap; box-shadow: inset 0 1px 2px rgba(0,0,0,0.3); }
19
  #stats-display strong, #inventory-display strong { color: #ccc; margin-right: 6px; }
20
  #inventory-display em { color: #888; font-style: normal; }
21
- .item-quest { background-color: #666030; border-color: #999048;}
22
- .item-weapon { background-color: #663030; border-color: #994848;}
23
- .item-armor { background-color: #306630; border-color: #489948;}
24
- .item-consumable { background-color: #664430; border-color: #996648;}
25
- .item-unknown { background-color: #555; border-color: #777;}
26
  #choices-container { margin-top: auto; padding-top: 20px; border-top: 1px solid #555; }
27
  #choices-container h3 { margin-top: 0; margin-bottom: 12px; color: #ccc; font-size: 1.1em; }
28
  #choices { display: flex; flex-direction: column; gap: 12px; }
29
  .choice-button { display: block; width: 100%; padding: 12px 15px; margin-bottom: 0; background-color: #555; color: #eee; border: 1px solid #777; border-radius: 4px; cursor: pointer; text-align: left; font-family: 'Courier New', monospace; font-size: 1.05em; transition: background-color 0.2s, border-color 0.2s, box-shadow 0.1s; box-sizing: border-box; }
30
  .choice-button:hover:not(:disabled) { background-color: #e0b050; color: #111; border-color: #c89040; box-shadow: 0 0 5px rgba(255, 200, 100, 0.5); }
31
  .choice-button:disabled { background-color: #404040; color: #777; cursor: not-allowed; border-color: #555; opacity: 0.7; }
32
- .message { padding: 8px 12px; margin-bottom: 1em; border-left-width: 3px; border-left-style: solid; font-size: 0.95em; background-color: rgba(255, 255, 255, 0.05); }
33
- .message-success { color: #8f8; border-left-color: #4a4; }
34
  .message-failure { color: #f88; border-left-color: #a44; }
35
- .message-info { color: #aaa; border-left-color: #666; }
36
- .message-item { color: #8bf; border-left-color: #46a; }
37
- .message-combat { color: #f98; border-left-color: #c64; font-weight: bold;}
38
- #action-info { position: absolute; bottom: 10px; left: 10px; background-color: rgba(0,0,0,0.7); color: #ffcc66; padding: 5px 10px; border-radius: 3px; font-size: 0.9em; display: block; z-index: 10;} /* Always visible */
39
- #combat-ui { margin-top: 15px; padding-top: 15px; border-top: 1px dashed #666; }
40
- .combat-button { background-color: #a33; border-color: #c66; color: #fff; font-weight: bold; text-align: center;}
41
- .combat-button:hover:not(:disabled) { background-color: #d44; border-color: #f88;}
42
  </style>
43
  </head>
44
  <body>
45
  <div id="game-container">
46
- <div id="scene-container">
47
- <div id="action-info">Mode: Explore</div>
48
- </div>
49
  <div id="ui-container">
50
- <h2 id="story-title">World Initializing...</h2>
51
- <div id="story-content"><p>Establishing reality...</p></div>
52
  <div id="stats-inventory-container">
53
- <div id="stats-display"></div>
54
- <div id="inventory-display"></div>
55
  </div>
56
  <div id="choices-container">
57
  <h3>What will you do?</h3>
58
- <div id="choices"></div>
59
  </div>
60
  </div>
61
  </div>
@@ -70,8 +54,7 @@
70
  <script type="module">
71
  import * as THREE from 'three';
72
  import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
73
- import { FontLoader } from 'three/addons/loaders/FontLoader.js';
74
- import { TextGeometry } from 'three/addons/geometries/TextGeometry.js';
75
 
76
  const sceneContainer = document.getElementById('scene-container');
77
  const storyTitleElement = document.getElementById('story-title');
@@ -79,46 +62,25 @@
79
  const choicesElement = document.getElementById('choices');
80
  const statsElement = document.getElementById('stats-display');
81
  const inventoryElement = document.getElementById('inventory-display');
82
- const actionInfoElement = document.getElementById('action-info');
83
 
84
- let scene, camera, renderer, clock, controls, raycaster, mouse;
85
  let worldGroup = null;
86
- let locationGroups = {}; // { zoneId: { group: THREE.Group, data: zoneData } }
87
  let currentMessage = "";
88
- let currentLights = [];
89
- let threeFont = null; // To store loaded font
90
 
91
  const MAT = {
92
- stone: new THREE.MeshStandardMaterial({ color: 0x777788, roughness: 0.85, metalness: 0.1 }),
93
- wood: new THREE.MeshStandardMaterial({ color: 0x9F6633, roughness: 0.75, metalness: 0 }),
94
- leaf: new THREE.MeshStandardMaterial({ color: 0x3E9B4E, roughness: 0.6, metalness: 0, side: THREE.DoubleSide }),
95
- ground: new THREE.MeshStandardMaterial({ color: 0x556B2F, roughness: 0.95, metalness: 0 }),
96
- metal: new THREE.MeshStandardMaterial({ color: 0xaaaaaa, metalness: 0.85, roughness: 0.35 }),
97
  dirt: new THREE.MeshStandardMaterial({ color: 0x8B5E3C, roughness: 0.9 }),
98
  grass: new THREE.MeshStandardMaterial({ color: 0x4CB781, roughness: 0.85 }),
99
- water: new THREE.MeshStandardMaterial({ color: 0x4682B4, roughness: 0.3, metalness: 0.2, transparent: true, opacity: 0.85 }),
100
- error: new THREE.MeshStandardMaterial({ color: 0xff3300, roughness: 0.5, emissive: 0x551100 }),
101
- gameOver: new THREE.MeshStandardMaterial({ color: 0xaa0000, roughness: 0.6, metalness: 0.2, emissive: 0x220000 }),
102
  simple: new THREE.MeshStandardMaterial({ color: 0xaaaaaa, roughness: 0.8 }),
103
- pickupHighlight: new THREE.MeshBasicMaterial({ color: 0xffff00, wireframe: true, depthTest: false }),
104
- dice: new THREE.MeshStandardMaterial({ color: 0xeeeeff, roughness: 0.2, metalness: 0.1 }),
105
- text: new THREE.MeshBasicMaterial({ color: 0xffddaa }),
106
- };
107
-
108
- const itemsData = {
109
- "Rusty Sword": {type:"weapon", description:"Old but sharp.", baseDamage: 3},
110
- "Health Potion": {type:"consumable", description:"Restores 10 HP.", effect: { hpGain: 10 }},
111
- "Goblin Ear": {type:"quest", description:"A gruesome trophy."},
112
- "Cave Crystal": {type:"unknown", description:"A faintly glowing crystal shard."}
113
- };
114
-
115
- const enemyData = {
116
- 'goblin': { name: "Goblin", hp: 8, defense: 11, attackDamage: 2, xp: 15, drops: ["Goblin Ear", "Health Potion"] },
117
- 'spider': { name: "Giant Spider", hp: 12, defense: 13, attackDamage: 3, xp: 25, drops: ["Cave Crystal"] }
118
  };
119
 
120
- // --- Game State Variable ---
121
- let gameState = {}; // Initialized later
122
 
123
  // --- Core Functions ---
124
 
@@ -126,15 +88,13 @@
126
  scene = new THREE.Scene();
127
  scene.background = new THREE.Color(0x1a1a1a);
128
  clock = new THREE.Clock();
129
- raycaster = new THREE.Raycaster();
130
- mouse = new THREE.Vector2();
131
  worldGroup = new THREE.Group();
132
  scene.add(worldGroup);
133
 
134
  const width = sceneContainer.clientWidth || 1;
135
  const height = sceneContainer.clientHeight || 1;
136
  camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 1000);
137
- camera.position.set(0, 5, 10);
138
 
139
  renderer = new THREE.WebGLRenderer({ antialias: true });
140
  renderer.setSize(width, height);
@@ -151,26 +111,11 @@
151
  controls.maxPolarAngle = Math.PI / 2 - 0.05;
152
 
153
  window.addEventListener('resize', onWindowResize, false);
154
- renderer.domElement.addEventListener('mousemove', onMouseMove, false);
155
- renderer.domElement.addEventListener('click', onMouseClick, false);
156
  setTimeout(onWindowResize, 50);
157
  animate();
158
  }
159
 
160
- function loadFontAndStart() {
161
- const loader = new FontLoader();
162
- loader.load('https://unpkg.com/[email protected]/examples/fonts/helvetiker_regular.typeface.json', function (font) {
163
- threeFont = font;
164
- console.log("Font loaded.");
165
- startGame(); // Start game only after font is loaded
166
- }, undefined, function (error) {
167
- console.error('Font loading failed:', error);
168
- storyTitleElement.textContent = "Font Load Error";
169
- storyContentElement.innerHTML = `<p style="color:red;">Could not load required font.</p>`;
170
- // Optionally try to start without font features or show a permanent error
171
- });
172
- }
173
-
174
  function onWindowResize() {
175
  if (!renderer || !camera || !sceneContainer) return;
176
  const width = sceneContainer.clientWidth || 1;
@@ -180,25 +125,9 @@
180
  renderer.setSize(width, height);
181
  }
182
 
183
- function onMouseMove( event ) {
184
- const rect = renderer.domElement.getBoundingClientRect();
185
- mouse.x = ( (event.clientX - rect.left) / rect.width ) * 2 - 1;
186
- mouse.y = - ( (event.clientY - rect.top) / rect.height ) * 2 + 1;
187
- }
188
-
189
- function onMouseClick( event ) {
190
- // Click interaction is now primarily for picking up items
191
- pickupItem();
192
- }
193
-
194
  function animate() {
195
  requestAnimationFrame(animate);
196
- const delta = clock.getDelta();
197
- const time = clock.getElapsedTime();
198
-
199
- controls.update();
200
- worldGroup.traverse(obj => { if (obj.userData.update) obj.userData.update(time, delta); });
201
-
202
  if (renderer && scene && camera) renderer.render(scene, camera);
203
  }
204
 
@@ -216,55 +145,35 @@
216
  const ground = new THREE.Mesh(geo, material);
217
  ground.rotation.x = -Math.PI / 2; ground.position.y = 0;
218
  ground.receiveShadow = true; ground.castShadow = false;
219
- ground.userData.isGround = true;
220
  return ground;
221
  }
222
 
223
  function setupLighting(type = 'default') {
224
- currentLights.forEach(light => {
225
- if (light) scene.remove(light);
226
- });
227
- currentLights = [];
228
 
229
- let ambientIntensity = 0.4;
230
- let dirIntensity = 0.9;
231
  let dirColor = 0xffffff;
232
  let dirPosition = new THREE.Vector3(10, 15, 8);
233
 
234
- if (type === 'forest') {
235
- ambientIntensity = 0.3; dirIntensity = 0.7; dirColor = 0xccffcc; dirPosition = new THREE.Vector3(5, 10, 5);
236
- } else if (type === 'cave') {
237
- ambientIntensity = 0.1; dirIntensity = 0;
238
- const ptLight = new THREE.PointLight(0xffaa55, 1.5, 12, 1);
239
- ptLight.position.set(0, 1.5, 1);
240
- ptLight.castShadow = true;
241
- ptLight.shadow.mapSize.set(512, 512);
242
- scene.add(ptLight); currentLights.push(ptLight);
243
- } else if (type === 'ruins') {
244
- ambientIntensity = 0.25; dirIntensity = 0.6; dirColor = 0xaaaaff; dirPosition = new THREE.Vector3(-8, 12, -5);
245
- } else if (type === 'gameover') {
246
- ambientIntensity = 0.1; dirIntensity = 0.4; dirColor = 0xff6666;
247
- }
248
 
249
  const ambientLight = new THREE.AmbientLight(0xffffff, ambientIntensity);
250
- scene.add(ambientLight);
251
- currentLights.push(ambientLight);
252
 
253
  if (dirIntensity > 0) {
254
  const directionalLight = new THREE.DirectionalLight(dirColor, dirIntensity);
255
  directionalLight.position.copy(dirPosition);
256
  directionalLight.castShadow = true;
257
  directionalLight.shadow.mapSize.set(1024, 1024);
258
- directionalLight.shadow.camera.near = 0.5;
259
- directionalLight.shadow.camera.far = 50;
260
- const shadowBounds = 20;
261
- directionalLight.shadow.camera.left = -shadowBounds;
262
- directionalLight.shadow.camera.right = shadowBounds;
263
- directionalLight.shadow.camera.top = shadowBounds;
264
- directionalLight.shadow.camera.bottom = -shadowBounds;
265
  directionalLight.shadow.bias = -0.0005;
266
- scene.add(directionalLight);
267
- currentLights.push(directionalLight);
268
  }
269
  }
270
 
@@ -272,13 +181,18 @@
272
  function createFieldZone(zoneId) {
273
  const group = new THREE.Group();
274
  group.add(createGround(MAT.grass, 30));
275
- // Add some rocks or flowers
276
  const rockGeo = new THREE.IcosahedronGeometry(0.5 + Math.random()*0.5, 0);
277
  for(let i=0; i<5; i++) {
278
  group.add(createMesh(rockGeo, MAT.stone, {x: (Math.random()-0.5)*25, y:0.3, z: (Math.random()-0.5)*25}));
279
  }
280
  group.visible = false;
281
- return { group, lighting: 'default', entryText: "You stand in an open field.", zoneId: zoneId };
 
 
 
 
 
 
282
  }
283
 
284
  function createForestZone(zoneId) {
@@ -286,7 +200,7 @@
286
  group.add(createGround(MAT.ground, 30));
287
  const trunkGeo = new THREE.CylinderGeometry(0.2, 0.3, 4, 8);
288
  const leafGeo = new THREE.SphereGeometry(1.5, 8, 6);
289
- for(let i=0; i<25; i++) { // Denser forest
290
  const x = (Math.random() - 0.5) * 28;
291
  const z = (Math.random() - 0.5) * 28;
292
  if(Math.sqrt(x*x+z*z) < 1) continue;
@@ -299,7 +213,15 @@
299
  group.add(tree);
300
  }
301
  group.visible = false;
302
- return { group, lighting: 'forest', entryText: "You are surrounded by trees.", zoneId: zoneId };
 
 
 
 
 
 
 
 
303
  }
304
 
305
  function createCaveZone(zoneId) {
@@ -314,8 +236,21 @@
314
  const st = createMesh(coneGeo, MAT.stone, {x: (Math.random()-0.5)*16, y: 6 + Math.random()*2, z: (Math.random()-0.5)*16}, {x:Math.PI});
315
  group.add(st);
316
  }
 
 
 
 
 
 
 
317
  group.visible = false;
318
- return { group, lighting: 'cave', entryText: "It's dark and damp in here.", zoneId: zoneId };
 
 
 
 
 
 
319
  }
320
 
321
  function createRuinsZone(zoneId) {
@@ -327,49 +262,49 @@
327
  {x: (Math.random()-0.5)*20, y:1, z: (Math.random()-0.5)*20},
328
  {y: Math.random() * Math.PI}
329
  );
330
- wall.scale.y = 0.5 + Math.random() * 0.8; // Vary height
331
- wall.rotation.x = (Math.random()-0.5)*0.1; // Tilt slightly
332
  wall.rotation.z = (Math.random()-0.5)*0.1;
333
  group.add(wall);
334
  }
335
  group.visible = false;
336
- return { group, lighting: 'ruins', entryText: "Crumbling walls stand sentinel.", zoneId: zoneId };
 
 
 
 
 
 
 
 
337
  }
338
 
339
  // --- Map Grid & Zone Definitions ---
340
  const MAP_ROWS = 3;
341
  const MAP_COLS = 4;
342
- const zoneData = {}; // Will be populated: { "zone_0_0": { group, lighting, entryText, zoneId }, ... }
343
 
344
  function getZoneId(row, col) { return `zone_${row}_${col}`; }
345
 
346
- function populateZoneData() {
347
  for (let r = 0; r < MAP_ROWS; r++) {
348
  for (let c = 0; c < MAP_COLS; c++) {
349
  const zoneId = getZoneId(r, c);
350
- let zoneType;
351
- // Simple pattern for variety, expand this logic
352
- if (r === 0) zoneType = 'forest';
353
- else if (r === 1 && c % 2 === 0) zoneType = 'field';
354
- else if (r === 1 && c % 2 !== 0) zoneType = 'ruins';
355
- else zoneType = 'cave';
356
-
357
  let creatorFunc;
358
- switch(zoneType) {
359
- case 'forest': creatorFunc = createForestZone; break;
360
- case 'cave': creatorFunc = createCaveZone; break;
361
- case 'ruins': creatorFunc = createRuinsZone; break;
362
- case 'field':
363
- default: creatorFunc = createFieldZone; break;
364
- }
365
- locationData[zoneId] = { creator: () => creatorFunc(zoneId) }; // Store creator with ID
366
  }
367
  }
368
- console.log("Zone data populated:", Object.keys(locationData));
369
  }
370
 
371
  function getZoneNeighbors(zoneId) {
372
  const parts = zoneId.split('_');
 
373
  const r = parseInt(parts[1]);
374
  const c = parseInt(parts[2]);
375
  const neighbors = {};
@@ -380,110 +315,106 @@
380
  return neighbors;
381
  }
382
 
383
- // --- Game State Initialization ---
384
  function startGame() {
385
  const defaultChar = {
386
  name: "Player",
387
- stats: { hp: 20, maxHp: 20, xp: 0, strength: 10, dexterity: 10 }, // Added stats
388
  inventory: []
389
  };
390
  gameState = {
391
  currentZoneId: null,
392
- character: JSON.parse(JSON.stringify(defaultChar)),
393
- combat: null // { active: true, enemyId: 'goblin', enemyHp: 8 }
394
  };
395
- populateZoneData(); // Create zone definitions
396
  console.log("Starting new game:", gameState);
397
  transitionToZone(getZoneId(1, 1)); // Start in a central zone
398
  }
399
 
400
- // --- Zone Transition ---
401
  function transitionToZone(newZoneId) {
402
- console.log(`Transitioning from ${gameState.currentZoneId} to ${newZoneId}`);
403
  currentMessage = "";
404
- gameState.combat = null; // End combat on zone transition
405
 
406
- if (gameState.currentZoneId && locationGroups[gameState.currentZoneId]) {
407
- locationGroups[gameState.currentZoneId].group.visible = false;
 
408
  }
409
 
410
  let zoneInfo;
411
- if (locationGroups[newZoneId]) {
412
- zoneInfo = locationGroups[newZoneId];
 
413
  zoneInfo.group.visible = true;
414
  } else {
415
- if (locationData[newZoneId] && locationData[newZoneId].creator) {
416
- zoneInfo = locationData[newZoneId].creator(); // Call the creator function
417
- locationGroups[newZoneId] = zoneInfo; // Store { group, lighting, entryText, zoneId }
 
418
  worldGroup.add(zoneInfo.group);
419
  zoneInfo.group.visible = true;
420
- } else {
421
- console.error(`Zone data or creator missing for ID: ${newLocationId}, attempting fallback`);
422
- const fallbackId = getZoneId(1, 1); // Fallback to center
423
- if (!locationGroups[fallbackId]) {
424
- locationGroups[fallbackId] = locationData[fallbackId].creator();
425
- worldGroup.add(locationGroups[fallbackId].group);
426
- }
427
- zoneInfo = locationGroups[fallbackId];
428
- zoneInfo.group.visible = true;
429
  newZoneId = fallbackId;
430
- currentMessage += `<p class="message message-failure">Error: Couldn't load target zone, returned to center.</p>`;
431
- }
432
  }
433
 
434
  gameState.currentZoneId = newZoneId;
435
  setupLighting(zoneInfo.lighting || 'default');
436
 
437
- // Basic camera reset, could be customized per zone later
438
- camera.position.set(0, 5, 10);
439
  controls.target.set(0, 1, 0);
440
  controls.update();
441
 
442
  renderCurrentPageUI();
443
  }
444
 
445
- // --- UI Rendering ---
446
  function renderCurrentPageUI() {
447
- const zoneInfo = locationGroups[gameState.currentZoneId];
448
- const zoneId = gameState.currentZoneId;
449
 
450
  if (!zoneInfo) {
451
- console.error(`No zone info for ${zoneId}`);
452
- storyTitleElement.textContent = "Lost";
453
- storyContentElement.innerHTML = currentMessage + "<p>You are in an undefined space.</p>";
454
- choicesElement.innerHTML = `<button class="choice-button" onclick="handleTransition({ transitionTo: getZoneId(1, 1) })">Return to Center</button>`;
455
  updateStatsDisplay();
456
  updateInventoryDisplay();
457
- updateActionInfo();
458
  return;
459
  }
460
 
461
- storyTitleElement.textContent = zoneId.replace('_', ' ').replace('zone ', 'Zone '); // Simple title
462
  storyContentElement.innerHTML = currentMessage + (zoneInfo.entryText ? `<p>${zoneInfo.entryText}</p>` : '');
463
  choicesElement.innerHTML = '';
464
 
465
  // Add Navigation Options
466
- const neighbors = getZoneNeighbors(zoneId);
467
- if (neighbors.north) choicesElement.innerHTML += `<button class="choice-button" onclick="handleTransition({ transitionTo: '${neighbors.north}' })">Go North</button>`;
468
- if (neighbors.south) choicesElement.innerHTML += `<button class="choice-button" onclick="handleTransition({ transitionTo: '${neighbors.south}' })">Go South</button>`;
469
- if (neighbors.east) choicesElement.innerHTML += `<button class="choice-button" onclick="handleTransition({ transitionTo: '${neighbors.east}' })">Go East</button>`;
470
- if (neighbors.west) choicesElement.innerHTML += `<button class="choice-button" onclick="handleTransition({ transitionTo: '${neighbors.west}' })">Go West</button>`;
471
-
472
- // Add Zone Specific Options / Combat Trigger Example
473
- if (zoneId === getZoneId(0, 1)) { // Example: Forest zone triggers combat
474
- choicesElement.innerHTML += `<button class="choice-button combat-button" onclick="startCombat('goblin')">Investigate Rustling</button>`;
475
- }
476
- if (zoneId === getZoneId(2, 2)) { // Example: Cave zone triggers combat
477
- choicesElement.innerHTML += `<button class="choice-button combat-button" onclick="startCombat('spider')">Disturb Webs</button>`;
478
  }
479
 
480
- // Add Combat UI if active
481
- if (gameState.combat?.active) {
482
- choicesElement.innerHTML += `
483
- <div id="combat-ui">
484
- <p class="message message-combat">Combat vs ${gameState.combat.enemyName}! (Enemy HP: ${gameState.combat.enemyHp})</p>
485
- <button class="choice-button combat-button" onclick="handleCombatAction('attack')">Roll to Attack!</button>
486
- </div>`;
 
 
487
  }
488
 
489
  updateStatsDisplay();
@@ -491,17 +422,10 @@
491
  updateActionInfo();
492
  }
493
 
494
- // Make functions globally accessible for inline onclick handlers
495
- window.handleTransition = (option) => { if (option.transitionTo) transitionToZone(option.transitionTo); };
496
- window.startCombat = startCombat;
497
- window.handleCombatAction = handleCombatAction;
498
- window.getZoneId = getZoneId; // Make helper accessible if needed
499
-
500
  function updateStatsDisplay() {
501
  const { hp, maxHp, xp } = gameState.character.stats;
502
  const hpColor = hp / maxHp < 0.3 ? '#f88' : (hp / maxHp < 0.6 ? '#fd5' : '#8f8');
503
  statsElement.innerHTML = `<strong>Stats:</strong> <span style="color:${hpColor}">HP: ${hp}/${maxHp}</span> <span>XP: ${xp}</span>`;
504
- // Could add Str/Dex here too if desired
505
  }
506
 
507
  function updateInventoryDisplay() {
@@ -516,208 +440,49 @@
516
  });
517
  }
518
  inventoryElement.innerHTML = invHtml;
519
- // Removed placement click handler
520
  }
521
 
522
  function updateActionInfo() {
523
- actionInfoElement.textContent = gameState.combat?.active ? `Mode: Combat` : `Mode: Explore (Click items)`;
524
- actionInfoElement.style.display = 'block';
525
  }
526
 
527
- // --- Combat & Item Functions ---
 
 
 
528
 
 
529
  function startCombat(enemyTypeId) {
530
- const enemyBase = enemyData[enemyTypeId];
531
- if (!enemyBase) {
532
- console.error("Unknown enemy type:", enemyTypeId);
533
- currentMessage = `<p class="message message-failure">Error: Unknown enemy encountered!</p>`;
534
- renderCurrentPageUI();
535
- return;
536
- }
537
- gameState.combat = {
538
- active: true,
539
- enemyId: enemyTypeId,
540
- enemyName: enemyBase.name,
541
- enemyHp: enemyBase.hp,
542
- enemyDefense: enemyBase.defense,
543
- enemyDamage: enemyBase.attackDamage,
544
- enemyXp: enemyBase.xp,
545
- enemyDrops: enemyBase.drops || []
546
- };
547
- currentMessage = `<p class="message message-combat">A wild ${enemyBase.name} appears!</p>`;
548
- renderCurrentPageUI();
549
  }
550
-
551
  function handleCombatAction(action) {
552
- if (!gameState.combat?.active) return;
553
-
554
- if (action === 'attack') {
555
- // Player attacks
556
- const roll = Math.floor(Math.random() * 20) + 1;
557
- const attackBonus = Math.floor((gameState.character.stats.strength - 10) / 2); // Simple STR mod
558
- const totalAttack = roll + attackBonus;
559
- const hit = totalAttack >= gameState.combat.enemyDefense;
560
-
561
- displayDiceRoll(roll, hit); // Show the dice roll visually
562
-
563
- if (hit) {
564
- const baseDamage = itemsData[gameState.character.inventory.find(i => itemsData[i]?.type === 'weapon')]?.baseDamage || 1; // Basic weapon damage or 1
565
- const damageRoll = Math.max(1, Math.floor(Math.random() * baseDamage) + 1); // Roll 1dX
566
- gameState.combat.enemyHp -= damageRoll;
567
- currentMessage = `<p class="message message-success">You hit the ${gameState.combat.enemyName} for ${damageRoll} damage! (Rolled ${totalAttack} vs DC ${gameState.combat.enemyDefense})</p>`;
568
- } else {
569
- currentMessage = `<p class="message message-failure">You missed the ${gameState.combat.enemyName}. (Rolled ${totalAttack} vs DC ${gameState.combat.enemyDefense})</p>`;
570
- }
571
-
572
- if (gameState.combat.enemyHp <= 0) {
573
- // Enemy defeated
574
- currentMessage += `<p class="message message-success"><b>You defeated the ${gameState.combat.enemyName}!</b></p>`;
575
- gameState.character.stats.xp += gameState.combat.enemyXp;
576
- currentMessage += `<p class="message message-info"><em>Gained ${gameState.combat.enemyXp} XP.</em></p>`;
577
- // Handle drops
578
- if (gameState.combat.enemyDrops.length > 0) {
579
- const droppedItemName = gameState.combat.enemyDrops[Math.floor(Math.random() * gameState.combat.enemyDrops.length)];
580
- dropItemInZone(droppedItemName, new THREE.Vector3(Math.random()*2-1, 0, Math.random()*2-1)); // Drop near center
581
- currentMessage += `<p class="message message-item"><em>The ${gameState.combat.enemyName} dropped a ${droppedItemName}!</em></p>`;
582
- }
583
- gameState.combat = null; // End combat
584
- renderCurrentPageUI();
585
- } else {
586
- // Enemy attacks
587
- const enemyAttackRoll = Math.floor(Math.random() * 20) + 1 + 2; // Simple enemy attack bonus
588
- const playerDefense = 10 + Math.floor((gameState.character.stats.dexterity - 10) / 2); // Simple AC
589
- if (enemyAttackRoll >= playerDefense) {
590
- const enemyDamageDealt = Math.max(1, Math.floor(Math.random() * gameState.combat.enemyDamage) + 1);
591
- gameState.character.stats.hp -= enemyDamageDealt;
592
- currentMessage += `<p class="message message-failure">The ${gameState.combat.enemyName} hits you for ${enemyDamageDealt} damage!</p>`;
593
- if (gameState.character.stats.hp <= 0) {
594
- currentMessage += `<p class="message message-failure"><b>You have been defeated!</b></p>`;
595
- gameState.combat = null; // End combat
596
- transitionToZone(getZoneId(1,1)); // Go back to start/center on death? Or game over page
597
- // TODO: Implement proper game over state
598
- }
599
- } else {
600
- currentMessage += `<p class="message message-info">The ${gameState.combat.enemyName} misses you.</p>`;
601
- }
602
- renderCurrentPageUI(); // Update UI after both attacks (if player survived)
603
- }
604
- }
605
- }
606
-
607
- function displayDiceRoll(result, success) {
608
- if (!threeFont) {
609
- console.warn("Font not loaded, cannot display dice roll text.");
610
- return;
611
  }
612
-
613
- const textGeo = new TextGeometry(result.toString(), {
614
- font: threeFont,
615
- size: 0.8,
616
- height: 0.1,
617
- curveSegments: 4,
618
- });
619
- textGeo.computeBoundingBox();
620
- const textWidth = textGeo.boundingBox.max.x - textGeo.boundingBox.min.x;
621
- const textMat = MAT.text.clone();
622
- textMat.color.setHex(success ? 0x88ff88 : 0xff8888); // Green for success, red for fail
623
-
624
- const textMesh = new THREE.Mesh(textGeo, textMat);
625
- // Position above center, facing camera
626
- const distance = 5;
627
- const textPos = camera.position.clone().add(camera.getWorldDirection(new THREE.Vector3()).multiplyScalar(distance));
628
- textMesh.position.copy(textPos);
629
- textMesh.position.y += 1; // Slightly higher
630
- textMesh.position.x -= textWidth / 2; // Center horizontally approx
631
- textMesh.quaternion.copy(camera.quaternion); // Face camera
632
-
633
- scene.add(textMesh);
634
-
635
- // Fade out and remove
636
- let opacity = 1;
637
- const fadeDuration = 1.5; // seconds
638
- const fadeStartTime = clock.getElapsedTime();
639
-
640
- textMesh.userData.update = (time) => {
641
- const elapsed = time - fadeStartTime;
642
- if (elapsed >= fadeDuration) {
643
- scene.remove(textMesh);
644
- delete textMesh.userData.update;
645
- } else {
646
- opacity = 1.0 - (elapsed / fadeDuration);
647
- // textMat.opacity = opacity; // Requires material.transparent = true; Might look odd for basic mesh
648
- textMesh.scale.setScalar(1 + (elapsed / fadeDuration)); // Grow slightly as it fades
649
- textMesh.position.y += 0.01; // Float up
650
- }
651
- };
652
- }
653
-
654
- function dropItemInZone(itemName, positionOffset = new THREE.Vector3(0, 0, 0)) {
655
- const currentGroup = locationGroups[gameState.currentZoneId]?.group;
656
- if (!currentGroup || !itemsData[itemName]) return;
657
-
658
- const itemDef = itemsData[itemName];
659
- const itemGeo = new THREE.BoxGeometry(0.4, 0.4, 0.4); // Simple box for dropped item
660
- const itemMat = MAT.simple.clone();
661
- if(itemDef.type === 'weapon') itemMat.color.setHex(0xcc6666);
662
- else if(itemDef.type === 'consumable') itemMat.color.setHex(0xcc9966);
663
- else if(itemDef.type === 'quest') itemMat.color.setHex(0xcccc66);
664
- else itemMat.color.setHex(0xaaaaee);
665
-
666
- const dropPos = new THREE.Vector3(positionOffset.x, 0.2, positionOffset.z); // Drop slightly above ground near offset
667
- const droppedMesh = createMesh(itemGeo, itemMat, dropPos);
668
- droppedMesh.userData = { isPickupable: true, itemName: itemName, description: `Dropped ${itemName}` };
669
- currentGroup.add(droppedMesh);
670
- console.log(`Dropped ${itemName} in zone ${gameState.currentZoneId}`);
671
  }
 
 
 
672
 
673
 
674
- function pickupItem() {
675
- if (gameState.combat?.active) return; // No pickup during combat
676
-
677
- raycaster.setFromCamera(mouse, camera);
678
- const currentGroup = locationGroups[gameState.currentZoneId]?.group;
679
- if (!currentGroup) return;
680
-
681
- const pickupables = [];
682
- currentGroup.traverseVisible(child => {
683
- if (child.userData.isPickupable) {
684
- pickupables.push(child);
685
- }
686
- });
687
-
688
- const intersects = raycaster.intersectObjects(pickupables, false);
689
-
690
- if (intersects.length > 0) {
691
- const clickedObject = intersects[0].object;
692
- const itemName = clickedObject.userData.itemName;
693
-
694
- if (itemName && itemsData[itemName]) {
695
- console.log(`Picked up: ${itemName}`);
696
- currentMessage = `<p class="message message-item"><em>Picked up: ${itemName}</em></p>`;
697
-
698
- if (!gameState.character.inventory.includes(itemName)) {
699
- gameState.character.inventory.push(itemName);
700
- } else {
701
- // Handle stacking or just ignore if already have? For now, ignore.
702
- currentMessage += `<p class="message message-info"><em>(You already have one/can't carry more)</em></p>`;
703
- }
704
-
705
- clickedObject.visible = false;
706
- clickedObject.userData.isPickupable = false; // Mark as picked up
707
-
708
- renderCurrentPageUI(); // Update inventory and clear message
709
- }
710
- }
711
- }
712
-
713
  // --- Initialization ---
714
  document.addEventListener('DOMContentLoaded', () => {
715
- console.log("DOM Ready - Initializing Persistent World Adventure.");
716
  try {
717
  initThreeJS();
718
  if (!scene || !camera || !renderer) throw new Error("Three.js failed to initialize.");
719
- loadFontAndStart(); // Load font, then start game
720
- console.log("Initialization sequence started.");
721
  } catch (error) {
722
  console.error("Initialization failed:", error);
723
  storyTitleElement.textContent = "Initialization Error";
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>World Grid Test</title>
7
  <style>
 
8
  body { font-family: 'Courier New', monospace; background-color: #111; color: #eee; margin: 0; padding: 0; overflow: hidden; display: flex; flex-direction: column; height: 100vh; }
9
  #game-container { display: flex; flex-grow: 1; overflow: hidden; }
10
+ #scene-container { flex-grow: 3; position: relative; border-right: 2px solid #444; min-width: 250px; background-color: #000; height: 100%; box-sizing: border-box; overflow: hidden; cursor: default; }
11
  #ui-container { flex-grow: 2; padding: 25px; overflow-y: auto; background-color: #2b2b2b; min-width: 320px; height: 100%; box-sizing: border-box; display: flex; flex-direction: column; }
12
  #scene-container canvas { display: block; }
13
  #story-title { color: #f0c060; margin: 0 0 15px 0; padding-bottom: 10px; border-bottom: 1px solid #555; font-size: 1.6em; text-shadow: 1px 1px 1px #000; }
 
17
  #stats-display span, #inventory-display .item-tag { display: inline-block; background-color: #484848; padding: 3px 9px; border-radius: 15px; margin: 0 8px 5px 0; border: 1px solid #6a6a6a; white-space: nowrap; box-shadow: inset 0 1px 2px rgba(0,0,0,0.3); }
18
  #stats-display strong, #inventory-display strong { color: #ccc; margin-right: 6px; }
19
  #inventory-display em { color: #888; font-style: normal; }
 
 
 
 
 
20
  #choices-container { margin-top: auto; padding-top: 20px; border-top: 1px solid #555; }
21
  #choices-container h3 { margin-top: 0; margin-bottom: 12px; color: #ccc; font-size: 1.1em; }
22
  #choices { display: flex; flex-direction: column; gap: 12px; }
23
  .choice-button { display: block; width: 100%; padding: 12px 15px; margin-bottom: 0; background-color: #555; color: #eee; border: 1px solid #777; border-radius: 4px; cursor: pointer; text-align: left; font-family: 'Courier New', monospace; font-size: 1.05em; transition: background-color 0.2s, border-color 0.2s, box-shadow 0.1s; box-sizing: border-box; }
24
  .choice-button:hover:not(:disabled) { background-color: #e0b050; color: #111; border-color: #c89040; box-shadow: 0 0 5px rgba(255, 200, 100, 0.5); }
25
  .choice-button:disabled { background-color: #404040; color: #777; cursor: not-allowed; border-color: #555; opacity: 0.7; }
26
+ .message { padding: 8px 12px; margin-bottom: 1em; border-left-width: 3px; border-left-style: solid; font-size: 0.95em; background-color: rgba(255, 255, 255, 0.05); border-color: #666; color: #aaa;}
 
27
  .message-failure { color: #f88; border-left-color: #a44; }
 
 
 
 
 
 
 
28
  </style>
29
  </head>
30
  <body>
31
  <div id="game-container">
32
+ <div id="scene-container"></div>
 
 
33
  <div id="ui-container">
34
+ <h2 id="story-title">Initializing...</h2>
35
+ <div id="story-content"><p>Loading assets...</p></div>
36
  <div id="stats-inventory-container">
37
+ <div id="stats-display">HP: ?/? | XP: ?</div>
38
+ <div id="inventory-display">Inventory: Empty</div>
39
  </div>
40
  <div id="choices-container">
41
  <h3>What will you do?</h3>
42
+ <div id="choices">Loading...</div>
43
  </div>
44
  </div>
45
  </div>
 
54
  <script type="module">
55
  import * as THREE from 'three';
56
  import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
57
+ // Font loading removed for simplicity for now
 
58
 
59
  const sceneContainer = document.getElementById('scene-container');
60
  const storyTitleElement = document.getElementById('story-title');
 
62
  const choicesElement = document.getElementById('choices');
63
  const statsElement = document.getElementById('stats-display');
64
  const inventoryElement = document.getElementById('inventory-display');
 
65
 
66
+ let scene, camera, renderer, clock, controls;
67
  let worldGroup = null;
68
+ let zoneGroups = {}; // Stores the THREE.Group for loaded zones { zoneId: group }
69
  let currentMessage = "";
 
 
70
 
71
  const MAT = {
72
+ stone: new THREE.MeshStandardMaterial({ color: 0x777788, roughness: 0.85 }),
73
+ wood: new THREE.MeshStandardMaterial({ color: 0x9F6633, roughness: 0.75 }),
74
+ leaf: new THREE.MeshStandardMaterial({ color: 0x3E9B4E, roughness: 0.6, side: THREE.DoubleSide }),
75
+ ground: new THREE.MeshStandardMaterial({ color: 0x556B2F, roughness: 0.95 }),
 
76
  dirt: new THREE.MeshStandardMaterial({ color: 0x8B5E3C, roughness: 0.9 }),
77
  grass: new THREE.MeshStandardMaterial({ color: 0x4CB781, roughness: 0.85 }),
78
+ water: new THREE.MeshStandardMaterial({ color: 0x4682B4, roughness: 0.3, transparent: true, opacity: 0.85 }),
 
 
79
  simple: new THREE.MeshStandardMaterial({ color: 0xaaaaaa, roughness: 0.8 }),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  };
81
 
82
+ // --- Game State ---
83
+ let gameState = {}; // Initialized in startGame
84
 
85
  // --- Core Functions ---
86
 
 
88
  scene = new THREE.Scene();
89
  scene.background = new THREE.Color(0x1a1a1a);
90
  clock = new THREE.Clock();
 
 
91
  worldGroup = new THREE.Group();
92
  scene.add(worldGroup);
93
 
94
  const width = sceneContainer.clientWidth || 1;
95
  const height = sceneContainer.clientHeight || 1;
96
  camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 1000);
97
+ camera.position.set(0, 6, 12); // Slightly higher view
98
 
99
  renderer = new THREE.WebGLRenderer({ antialias: true });
100
  renderer.setSize(width, height);
 
111
  controls.maxPolarAngle = Math.PI / 2 - 0.05;
112
 
113
  window.addEventListener('resize', onWindowResize, false);
114
+ // Removed mouse listeners for now
 
115
  setTimeout(onWindowResize, 50);
116
  animate();
117
  }
118
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  function onWindowResize() {
120
  if (!renderer || !camera || !sceneContainer) return;
121
  const width = sceneContainer.clientWidth || 1;
 
125
  renderer.setSize(width, height);
126
  }
127
 
 
 
 
 
 
 
 
 
 
 
 
128
  function animate() {
129
  requestAnimationFrame(animate);
130
+ controls.update(); // Need to update controls
 
 
 
 
 
131
  if (renderer && scene && camera) renderer.render(scene, camera);
132
  }
133
 
 
145
  const ground = new THREE.Mesh(geo, material);
146
  ground.rotation.x = -Math.PI / 2; ground.position.y = 0;
147
  ground.receiveShadow = true; ground.castShadow = false;
 
148
  return ground;
149
  }
150
 
151
  function setupLighting(type = 'default') {
152
+ worldGroup.children.filter(c => c.isLight).forEach(light => worldGroup.remove(light)); // Remove old lights from worldGroup
 
 
 
153
 
154
+ let ambientIntensity = 0.5;
155
+ let dirIntensity = 1.0;
156
  let dirColor = 0xffffff;
157
  let dirPosition = new THREE.Vector3(10, 15, 8);
158
 
159
+ if (type === 'forest') { ambientIntensity = 0.4; dirIntensity = 0.8; dirColor = 0xccffcc; dirPosition = new THREE.Vector3(5, 10, 5); }
160
+ if (type === 'cave') { ambientIntensity = 0.2; dirIntensity = 0; } // Rely on point light added in zone creator
161
+ if (type === 'ruins') { ambientIntensity = 0.4; dirIntensity = 0.7; dirColor = 0xaaaaff; dirPosition = new THREE.Vector3(-8, 12, -5); }
 
 
 
 
 
 
 
 
 
 
 
162
 
163
  const ambientLight = new THREE.AmbientLight(0xffffff, ambientIntensity);
164
+ worldGroup.add(ambientLight); // Add lights to worldGroup
 
165
 
166
  if (dirIntensity > 0) {
167
  const directionalLight = new THREE.DirectionalLight(dirColor, dirIntensity);
168
  directionalLight.position.copy(dirPosition);
169
  directionalLight.castShadow = true;
170
  directionalLight.shadow.mapSize.set(1024, 1024);
171
+ directionalLight.shadow.camera.near = 0.5; directionalLight.shadow.camera.far = 50;
172
+ const sb = 20; // shadow bounds
173
+ directionalLight.shadow.camera.left = -sb; directionalLight.shadow.camera.right = sb;
174
+ directionalLight.shadow.camera.top = sb; directionalLight.shadow.camera.bottom = -sb;
 
 
 
175
  directionalLight.shadow.bias = -0.0005;
176
+ worldGroup.add(directionalLight);
 
177
  }
178
  }
179
 
 
181
  function createFieldZone(zoneId) {
182
  const group = new THREE.Group();
183
  group.add(createGround(MAT.grass, 30));
 
184
  const rockGeo = new THREE.IcosahedronGeometry(0.5 + Math.random()*0.5, 0);
185
  for(let i=0; i<5; i++) {
186
  group.add(createMesh(rockGeo, MAT.stone, {x: (Math.random()-0.5)*25, y:0.3, z: (Math.random()-0.5)*25}));
187
  }
188
  group.visible = false;
189
+ return {
190
+ group,
191
+ lighting: 'default',
192
+ title: "Open Field",
193
+ entryText: "You stand in an open, grassy field.",
194
+ options: [] // Neighbors added dynamically
195
+ };
196
  }
197
 
198
  function createForestZone(zoneId) {
 
200
  group.add(createGround(MAT.ground, 30));
201
  const trunkGeo = new THREE.CylinderGeometry(0.2, 0.3, 4, 8);
202
  const leafGeo = new THREE.SphereGeometry(1.5, 8, 6);
203
+ for(let i=0; i<25; i++) {
204
  const x = (Math.random() - 0.5) * 28;
205
  const z = (Math.random() - 0.5) * 28;
206
  if(Math.sqrt(x*x+z*z) < 1) continue;
 
213
  group.add(tree);
214
  }
215
  group.visible = false;
216
+ return {
217
+ group,
218
+ lighting: 'forest',
219
+ title: "Dark Forest",
220
+ entryText: "Sunlight is sparse beneath the thick canopy.",
221
+ options: [
222
+ { text: "Search for tracks (TBC)", action: "search"} // Placeholder for future skill check
223
+ ]
224
+ };
225
  }
226
 
227
  function createCaveZone(zoneId) {
 
236
  const st = createMesh(coneGeo, MAT.stone, {x: (Math.random()-0.5)*16, y: 6 + Math.random()*2, z: (Math.random()-0.5)*16}, {x:Math.PI});
237
  group.add(st);
238
  }
239
+ // Add point light specific to cave
240
+ const ptLight = new THREE.PointLight(0xffaa55, 1.5, 12, 1);
241
+ ptLight.position.set(0, 3, 0);
242
+ ptLight.castShadow = true;
243
+ ptLight.shadow.mapSize.set(512, 512);
244
+ group.add(ptLight);
245
+
246
  group.visible = false;
247
+ return {
248
+ group,
249
+ lighting: 'cave',
250
+ title: "Dim Cave",
251
+ entryText: "It's dark and damp in here. Water drips.",
252
+ options: []
253
+ };
254
  }
255
 
256
  function createRuinsZone(zoneId) {
 
262
  {x: (Math.random()-0.5)*20, y:1, z: (Math.random()-0.5)*20},
263
  {y: Math.random() * Math.PI}
264
  );
265
+ wall.scale.y = 0.5 + Math.random() * 0.8;
266
+ wall.rotation.x = (Math.random()-0.5)*0.1;
267
  wall.rotation.z = (Math.random()-0.5)*0.1;
268
  group.add(wall);
269
  }
270
  group.visible = false;
271
+ return {
272
+ group,
273
+ lighting: 'ruins',
274
+ title: "Crumbling Ruins",
275
+ entryText: "The wind whistles through broken walls.",
276
+ options: [
277
+ { text: "Examine rubble (TBC)", action: "examine_rubble"}
278
+ ]
279
+ };
280
  }
281
 
282
  // --- Map Grid & Zone Definitions ---
283
  const MAP_ROWS = 3;
284
  const MAP_COLS = 4;
285
+ const zoneCreators = {}; // { "zone_0_0": createFunction, ... }
286
 
287
  function getZoneId(row, col) { return `zone_${row}_${col}`; }
288
 
289
+ function populateZoneCreators() {
290
  for (let r = 0; r < MAP_ROWS; r++) {
291
  for (let c = 0; c < MAP_COLS; c++) {
292
  const zoneId = getZoneId(r, c);
 
 
 
 
 
 
 
293
  let creatorFunc;
294
+ // Simple pattern for variety
295
+ if (r === 0) creatorFunc = createForestZone;
296
+ else if (r === 1 && c % 2 === 0) creatorFunc = createFieldZone;
297
+ else if (r === 1 && c % 2 !== 0) creatorFunc = createRuinsZone;
298
+ else creatorFunc = createCaveZone;
299
+ zoneCreators[zoneId] = () => creatorFunc(zoneId); // Store function that calls creator with ID
 
 
300
  }
301
  }
302
+ console.log("Zone creators populated.");
303
  }
304
 
305
  function getZoneNeighbors(zoneId) {
306
  const parts = zoneId.split('_');
307
+ if (parts.length !== 3) return {};
308
  const r = parseInt(parts[1]);
309
  const c = parseInt(parts[2]);
310
  const neighbors = {};
 
315
  return neighbors;
316
  }
317
 
 
318
  function startGame() {
319
  const defaultChar = {
320
  name: "Player",
321
+ stats: { hp: 20, maxHp: 20, xp: 0 },
322
  inventory: []
323
  };
324
  gameState = {
325
  currentZoneId: null,
326
+ character: JSON.parse(JSON.stringify(defaultChar))
 
327
  };
328
+ populateZoneCreators();
329
  console.log("Starting new game:", gameState);
330
  transitionToZone(getZoneId(1, 1)); // Start in a central zone
331
  }
332
 
 
333
  function transitionToZone(newZoneId) {
334
+ console.log(`Transitioning to ${newZoneId}`);
335
  currentMessage = "";
 
336
 
337
+ // Hide current zone if exists
338
+ if (gameState.currentZoneId && zoneGroups[gameState.currentZoneId]) {
339
+ zoneGroups[gameState.currentZoneId].visible = false;
340
  }
341
 
342
  let zoneInfo;
343
+ // Load or unhide new zone
344
+ if (zoneGroups[newZoneId]) {
345
+ zoneInfo = zoneGroups[newZoneId];
346
  zoneInfo.group.visible = true;
347
  } else {
348
+ const creator = zoneCreators[newZoneId];
349
+ if (creator) {
350
+ zoneInfo = creator(); // Create the zone { group, lighting, title, entryText, options }
351
+ zoneGroups[newZoneId] = zoneInfo; // Store the created zone data
352
  worldGroup.add(zoneInfo.group);
353
  zoneInfo.group.visible = true;
354
+ } else {
355
+ console.error(`No creator found for zone ID: ${newZoneId}, going to default`);
356
+ const fallbackId = getZoneId(1, 1);
357
+ if (!zoneGroups[fallbackId]) { // Ensure fallback exists
358
+ zoneGroups[fallbackId] = zoneCreators[fallbackId]();
359
+ worldGroup.add(zoneGroups[fallbackId].group);
360
+ }
361
+ zoneInfo = zoneGroups[fallbackId];
362
+ zoneInfo.group.visible = true;
363
  newZoneId = fallbackId;
364
+ currentMessage = `<p class="message message-failure">Error: Invalid zone transition.</p>`;
365
+ }
366
  }
367
 
368
  gameState.currentZoneId = newZoneId;
369
  setupLighting(zoneInfo.lighting || 'default');
370
 
371
+ // Basic camera reset
372
+ camera.position.set(0, 6, 12);
373
  controls.target.set(0, 1, 0);
374
  controls.update();
375
 
376
  renderCurrentPageUI();
377
  }
378
 
379
+
380
  function renderCurrentPageUI() {
381
+ const zoneInfo = zoneGroups[gameState.currentZoneId];
 
382
 
383
  if (!zoneInfo) {
384
+ console.error(`No zone info loaded for ${gameState.currentZoneId}`);
385
+ storyTitleElement.textContent = "Error";
386
+ storyContentElement.innerHTML = currentMessage + "<p>Cannot render current zone.</p>";
387
+ choicesElement.innerHTML = `<button class="choice-button" onclick="transitionToZone('${getZoneId(1, 1)}')">Return to Start</button>`;
388
  updateStatsDisplay();
389
  updateInventoryDisplay();
 
390
  return;
391
  }
392
 
393
+ storyTitleElement.textContent = zoneInfo.title || "Unknown Zone";
394
  storyContentElement.innerHTML = currentMessage + (zoneInfo.entryText ? `<p>${zoneInfo.entryText}</p>` : '');
395
  choicesElement.innerHTML = '';
396
 
397
  // Add Navigation Options
398
+ const neighbors = getZoneNeighbors(gameState.currentZoneId);
399
+ const directions = {'north': 'North', 'south': 'South', 'east': 'East', 'west': 'West'};
400
+ for(const dir in neighbors) {
401
+ const neighborId = neighbors[dir];
402
+ const button = document.createElement('button');
403
+ button.classList.add('choice-button');
404
+ button.textContent = `Go ${directions[dir]}`;
405
+ button.onclick = () => transitionToZone(neighborId);
406
+ choicesElement.appendChild(button);
 
 
 
407
  }
408
 
409
+ // Add Zone Specific Options
410
+ if (zoneInfo.options && zoneInfo.options.length > 0) {
411
+ zoneInfo.options.forEach(option => {
412
+ const button = document.createElement('button');
413
+ button.classList.add('choice-button');
414
+ button.textContent = option.text;
415
+ // Add onclick based on option.action or other properties later
416
+ choicesElement.appendChild(button);
417
+ });
418
  }
419
 
420
  updateStatsDisplay();
 
422
  updateActionInfo();
423
  }
424
 
 
 
 
 
 
 
425
  function updateStatsDisplay() {
426
  const { hp, maxHp, xp } = gameState.character.stats;
427
  const hpColor = hp / maxHp < 0.3 ? '#f88' : (hp / maxHp < 0.6 ? '#fd5' : '#8f8');
428
  statsElement.innerHTML = `<strong>Stats:</strong> <span style="color:${hpColor}">HP: ${hp}/${maxHp}</span> <span>XP: ${xp}</span>`;
 
429
  }
430
 
431
  function updateInventoryDisplay() {
 
440
  });
441
  }
442
  inventoryElement.innerHTML = invHtml;
 
443
  }
444
 
445
  function updateActionInfo() {
446
+ actionInfoElement.textContent = `Zone: ${gameState.currentZoneId || 'None'}`;
 
447
  }
448
 
449
+ // --- Pickup/Placement (Removed for now) ---
450
+ function pickupItem() { console.log("Pickup disabled in this version."); }
451
+ function placeItem() { console.log("Placement disabled in this version."); }
452
+ function togglePlacementMode() { console.log("Placement disabled in this version.");}
453
 
454
+ // --- Combat/Checks (Placeholders) ---
455
  function startCombat(enemyTypeId) {
456
+ console.log("Combat Started (Placeholder):", enemyTypeId);
457
+ currentMessage = `<p class="message message-combat">Placeholder: Encountered a ${enemyTypeId}!</p>`;
458
+ renderCurrentPageUI(); // Re-render to show message
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
459
  }
 
460
  function handleCombatAction(action) {
461
+ console.log("Combat Action (Placeholder):", action);
462
+ currentMessage = `<p class="message message-info">Placeholder: Combat action '${action}' executed.</p>`;
463
+ // Add basic HP loss for testing
464
+ gameState.character.stats.hp -= 1;
465
+ gameState.character.stats.hp = Math.max(0, gameState.character.stats.hp);
466
+ if(gameState.character.stats.hp === 0) {
467
+ currentMessage += `<p class="message message-failure">You were defeated in placeholder combat!</p>`;
468
+ // Reset HP for now instead of game over
469
+ gameState.character.stats.hp = gameState.character.stats.maxHp;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
470
  }
471
+ renderCurrentPageUI();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
472
  }
473
+ // Make placeholders globally accessible if needed by inline HTML later
474
+ window.startCombat = startCombat;
475
+ window.handleCombatAction = handleCombatAction;
476
 
477
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
478
  // --- Initialization ---
479
  document.addEventListener('DOMContentLoaded', () => {
480
+ console.log("DOM Ready - Initializing World Grid Test.");
481
  try {
482
  initThreeJS();
483
  if (!scene || !camera || !renderer) throw new Error("Three.js failed to initialize.");
484
+ startGame(); // Font not needed for this simplified version
485
+ console.log("Game world initialized and started.");
486
  } catch (error) {
487
  console.error("Initialization failed:", error);
488
  storyTitleElement.textContent = "Initialization Error";