awacke1 commited on
Commit
062181d
·
verified ·
1 Parent(s): 17b162a

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +242 -764
index.html CHANGED
@@ -3,41 +3,29 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Enhanced CYOA - 3D Interaction & Skills</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: 20px; overflow-y: auto; background-color: #2b2b2b; min-width: 350px; 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; }
14
- #story-content { margin-bottom: 20px; line-height: 1.7; flex-grow: 1; font-size: 1.1em; min-height: 100px; /* Ensure space */}
15
- #stats-inventory-container { margin-bottom: 15px; padding: 15px; border: 1px solid #444; border-radius: 4px; background-color: #333; font-size: 0.95em; }
16
- #stats-display, #inventory-display, #history-log { margin-bottom: 10px; line-height: 1.8; }
17
- #history-log { max-height: 150px; overflow-y: auto; border-top: 1px dashed #555; padding-top: 10px; font-size: 0.9em; color: #aaa; }
18
- #history-log strong { color: #ccc; }
19
- #history-log div { margin-bottom: 4px; }
20
  #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); }
21
- #stats-display strong, #inventory-display strong { color: #bbb; margin-right: 6px; }
22
  #inventory-display em { color: #888; font-style: normal; }
23
- .item-quest { background-color: #666030; border-color: #999048;}
24
- .item-weapon { background-color: #663030; border-color: #994848;}
25
- .item-armor { background-color: #306630; border-color: #489948;}
26
- .item-consumable { background-color: #664430; border-color: #996648;}
27
- .item-unknown { background-color: #555; border-color: #777;}
28
- #choices-container { margin-top: auto; padding-top: 15px; border-top: 1px solid #555; }
29
  #choices-container h3 { margin-top: 0; margin-bottom: 12px; color: #ccc; font-size: 1.1em; }
30
- #choices { display: flex; flex-direction: column; gap: 10px; }
31
- .choice-button { display: block; width: 100%; padding: 10px 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.0em; transition: background-color 0.2s, border-color 0.2s, box-shadow 0.1s; box-sizing: border-box; }
32
  .choice-button:hover:not(:disabled) { background-color: #e0b050; color: #111; border-color: #c89040; box-shadow: 0 0 5px rgba(255, 200, 100, 0.5); }
33
- .choice-button:disabled { background-color: #404040; color: #777; cursor: not-allowed; border-color: #555; opacity: 0.7; }
34
  .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;}
35
- .message-success { color: #8f8; border-left-color: #4a4; }
36
  .message-failure { color: #f88; border-left-color: #a44; }
37
- .message-info { color: #8af; border-left-color: #46a; }
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;}
39
- /* Placement Preview Style */
40
- .placement-preview { opacity: 0.6; /* Make it semi-transparent */ }
41
  </style>
42
  </head>
43
  <body>
@@ -49,9 +37,8 @@
49
  <h2 id="story-title">Initializing...</h2>
50
  <div id="story-content"><p>Loading assets...</p></div>
51
  <div id="stats-inventory-container">
52
- <div id="stats-display">HP: ?/? | XP: ? | STR: ? | DEX: ? | INT: ?</div>
53
  <div id="inventory-display">Inventory: Empty</div>
54
- <div id="history-log"><strong>History:</strong><div>Game Started.</div></div>
55
  </div>
56
  <div id="choices-container">
57
  <h3>What will you do?</h3>
@@ -62,148 +49,112 @@
62
 
63
  <script type="importmap">
64
  { "imports": {
65
- "three": "https://unpkg.com/[email protected]/build/three.module.js",
66
- "three/addons/": "https://unpkg.com/[email protected]/examples/jsm/"
67
  }}
68
  </script>
69
 
70
  <script type="module">
71
  import * as THREE from 'three';
72
- import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
 
 
73
 
74
- // --- DOM Elements ---
75
  const sceneContainer = document.getElementById('scene-container');
76
  const storyTitleElement = document.getElementById('story-title');
77
  const storyContentElement = document.getElementById('story-content');
78
  const choicesElement = document.getElementById('choices');
79
  const statsElement = document.getElementById('stats-display');
80
  const inventoryElement = document.getElementById('inventory-display');
81
- const historyElement = document.getElementById('history-log');
82
  const actionInfoElement = document.getElementById('action-info');
83
 
84
- // --- Three.js Variables ---
85
- let scene, camera, renderer, clock, controls, raycaster, mouse;
86
- let worldGroup = null;
87
- let zoneGroups = {}; // Stores { group, lighting, title, entryText, options, zoneId, items: [], interactables: [] }
88
- let currentLights = [];
89
- let playerAvatar = null;
90
 
91
- // --- Game State Variables ---
92
- let gameState = {};
 
93
  let currentMessage = "";
94
- let placementMode = false;
95
- let itemToPlace = null; // The name (key) of the item being placed
96
- let previewMesh = null;
97
-
98
- // --- Constants ---
99
- const MAP_ROWS = 5; // Increased for more zones
100
- const MAP_COLS = 4;
101
- const PLAYER_HEIGHT = 1.0; // For avatar y-position
102
 
103
- // --- Materials ---
104
- const MAT = {
105
  stone: new THREE.MeshStandardMaterial({ color: 0x777788, roughness: 0.85 }),
106
  wood: new THREE.MeshStandardMaterial({ color: 0x9F6633, roughness: 0.75 }),
107
  leaf: new THREE.MeshStandardMaterial({ color: 0x3E9B4E, roughness: 0.6, side: THREE.DoubleSide }),
108
  ground: new THREE.MeshStandardMaterial({ color: 0x556B2F, roughness: 0.95 }),
109
  dirt: new THREE.MeshStandardMaterial({ color: 0x8B5E3C, roughness: 0.9 }),
110
  grass: new THREE.MeshStandardMaterial({ color: 0x4CB781, roughness: 0.85 }),
111
- water: new THREE.MeshStandardMaterial({ color: 0x4682B4, roughness: 0.3, transparent: true, opacity: 0.85 }),
112
- sand: new THREE.MeshStandardMaterial({ color: 0xC2B280, roughness: 0.9 }),
113
- ice: new THREE.MeshStandardMaterial({ color: 0xadd8e6, roughness: 0.2, transparent: true, opacity: 0.9 }),
114
- portal: new THREE.MeshStandardMaterial({ color: 0x9933ff, emissive: 0x6600cc, roughness: 0.4, side: THREE.DoubleSide }),
115
- itemGeneric: new THREE.MeshStandardMaterial({ color: 0xffcc00, roughness: 0.5 }), // For pickup items
116
- preview: new THREE.MeshStandardMaterial({ color: 0x00ff00, transparent: true, opacity: 0.5, wireframe: true }), // For placement
117
- simple: new THREE.MeshStandardMaterial({ color: 0xaaaaaa, roughness: 0.8 }),
118
  };
119
 
120
- // --- Item Data ---
 
121
  const itemsData = {
122
- "Rusty Sword": {type:"weapon", description:"Old but sharp.", baseDamage: 3, meshGeo: new THREE.BoxGeometry(0.1, 0.8, 0.05), meshMat: MAT.simple.clone().set({color:0x999999})},
123
- "Health Potion": {type:"consumable", description:"Restores 10 HP.", effect: { hpGain: 10 }, meshGeo: new THREE.SphereGeometry(0.15, 8, 6), meshMat: MAT.simple.clone().set({color:0xff4444})},
124
- "Goblin Ear": {type:"quest", description:"A gruesome trophy.", meshGeo: new THREE.PlaneGeometry(0.2, 0.3), meshMat: MAT.simple.clone().set({color:0x55aa55})},
125
- "Cave Crystal": {type:"unknown", description:"A faintly glowing crystal shard.", meshGeo: new THREE.IcosahedronGeometry(0.2, 0), meshMat: MAT.simple.clone().set({color:0xaaaaff, emissive: 0x5555cc})},
126
- "Ancient Key": {type:"quest", description:"A large, ornate key.", meshGeo: new THREE.BoxGeometry(0.05, 0.3, 0.05), meshMat: MAT.simple.clone().set({color:0xccaa00})},
127
  };
128
 
129
- // --- Zone Creation ---
130
- const zoneCreators = {}; // Populated later
 
131
 
132
  function initThreeJS() {
133
- scene = new THREE.Scene();
134
- scene.background = new THREE.Color(0x1a1a1a);
135
- clock = new THREE.Clock();
136
- raycaster = new THREE.Raycaster();
137
- mouse = new THREE.Vector2();
138
- worldGroup = new THREE.Group();
139
- scene.add(worldGroup);
140
-
141
- // Add Player Avatar
142
- const avatarGeo = new THREE.CapsuleGeometry(0.3, PLAYER_HEIGHT - 0.6, 4, 10);
143
- playerAvatar = new THREE.Mesh(avatarGeo, new THREE.MeshStandardMaterial({ color: 0xeeeeff, roughness: 0.7 }));
144
- playerAvatar.position.y = PLAYER_HEIGHT / 2; // Center it vertically
145
- playerAvatar.castShadow = true;
146
- // Don't add to worldGroup directly, add to current zone's group later if needed or keep separate
147
- scene.add(playerAvatar); // Keep it at scene root for now
148
-
149
- const width = sceneContainer.clientWidth || 1;
150
- const height = sceneContainer.clientHeight || 1;
151
- camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 1000);
152
- camera.position.set(0, 6, 8); // Slightly different starting view
153
-
154
- renderer = new THREE.WebGLRenderer({ antialias: true });
155
- renderer.setSize(width, height);
156
- renderer.shadowMap.enabled = true;
157
- renderer.shadowMap.type = THREE.PCFSoftShadowMap;
158
- renderer.toneMapping = THREE.ACESFilmicToneMapping;
159
- renderer.outputColorSpace = THREE.SRGBColorSpace;
160
- sceneContainer.appendChild(renderer.domElement);
161
-
162
- controls = new OrbitControls(camera, renderer.domElement);
163
- controls.enableDamping = true;
164
- controls.dampingFactor = 0.1;
165
- controls.target.set(0, PLAYER_HEIGHT / 2, 0); // Target the avatar's center
166
- controls.maxPolarAngle = Math.PI / 2 - 0.05;
167
- controls.minDistance = 2;
168
- controls.maxDistance = 25;
169
-
170
- window.addEventListener('resize', onWindowResize, false);
171
- sceneContainer.addEventListener('click', handleRaycast, false); // Use scene container for clicks
172
- document.addEventListener('keydown', handleKeyPress); // Listen for key presses globally
173
- setTimeout(onWindowResize, 50);
174
- animate();
175
  }
176
 
177
  function onWindowResize() {
 
178
  if (!renderer || !camera || !sceneContainer) return;
179
- const width = sceneContainer.clientWidth || 1;
180
- const height = sceneContainer.clientHeight || 1;
181
- camera.aspect = width / height;
182
- camera.updateProjectionMatrix();
183
- renderer.setSize(width, height);
 
 
 
 
 
184
  }
185
 
 
186
  function animate() {
 
 
187
  requestAnimationFrame(animate);
188
- const delta = clock.getDelta();
189
- controls.update();
190
-
191
- // Update placement preview position if active
192
- if (placementMode && previewMesh) {
193
- updatePreviewMesh();
194
- }
195
-
196
- // Simple animation example (e.g., make portals pulse)
197
- worldGroup.traverse(child => {
198
- if (child.userData?.isPortal) {
199
- child.material.emissiveIntensity = Math.sin(clock.elapsedTime * 3) * 0.5 + 0.7;
200
- }
201
- if (child.userData?.isItem && child.userData.itemName === "Cave Crystal") {
202
- child.rotation.y += delta * 0.5;
203
- }
204
- });
205
-
206
-
207
  if (renderer && scene && camera) renderer.render(scene, camera);
208
  }
209
 
@@ -214,770 +165,297 @@
214
  mesh.scale.set(scale.x, scale.y, scale.z);
215
  mesh.castShadow = true; mesh.receiveShadow = true;
216
  return mesh;
217
- }
218
 
219
  function createGround(material = MAT.ground, size = 20) {
220
  const geo = new THREE.PlaneGeometry(size, size);
221
  const ground = new THREE.Mesh(geo, material);
222
  ground.rotation.x = -Math.PI / 2; ground.position.y = 0;
223
- ground.receiveShadow = true; ground.castShadow = false; // Ground doesn't cast shadows
224
- ground.userData.isGround = true; // Mark for raycasting
225
  return ground;
226
  }
227
 
228
  function setupLighting(type = 'default') {
229
- currentLights.forEach(light => {
230
- if (light && light.parent) light.parent.remove(light);
231
- if (light && !light.parent) scene.remove(light); // Remove from scene if not parented
 
232
  });
233
  currentLights = [];
234
 
235
- let ambientIntensity = 0.6;
236
  let dirIntensity = 1.0;
237
  let dirColor = 0xffffff;
238
- let dirPosition = new THREE.Vector3(10, 15, 8);
239
- let needsPointLight = false;
240
- let pointLightInfo = { color: 0xffaa55, intensity: 1.5, distance: 12, decay: 1, pos: {x:0, y:3, z:0} };
241
-
242
- if (type === 'forest') { ambientIntensity = 0.4; dirIntensity = 0.8; dirColor = 0xccffcc; dirPosition = new THREE.Vector3(5, 10, 5); }
243
- if (type === 'cave') {
244
- ambientIntensity = 0.15; dirIntensity = 0; // No sun in cave
245
- needsPointLight = true;
246
- }
247
- if (type === 'ruins') { ambientIntensity = 0.5; dirIntensity = 0.7; dirColor = 0xaaaaff; dirPosition = new THREE.Vector3(-8, 12, -5); }
248
- if (type === 'desert') { ambientIntensity = 0.7; dirIntensity = 1.5; dirColor = 0xffffcc; dirPosition = new THREE.Vector3(15, 20, 10); }
249
- if (type === 'mountain') { ambientIntensity = 0.6; dirIntensity = 1.2; dirColor = 0xddddff; dirPosition = new THREE.Vector3(0, 25, 15); }
250
 
251
  const ambientLight = new THREE.AmbientLight(0xffffff, ambientIntensity);
252
- scene.add(ambientLight); // Ambient always added to scene
253
  currentLights.push(ambientLight);
254
 
255
  if (dirIntensity > 0) {
256
  const directionalLight = new THREE.DirectionalLight(dirColor, dirIntensity);
257
  directionalLight.position.copy(dirPosition);
258
  directionalLight.castShadow = true;
259
- directionalLight.shadow.mapSize.set(1024*2, 1024*2); // Higher res shadow map
260
- directionalLight.shadow.camera.near = 0.5; directionalLight.shadow.camera.far = 60; // Increased range
261
- const sb = 25; // Increased shadow box size
262
- directionalLight.shadow.camera.left = -sb; directionalLight.shadow.camera.right = sb;
263
- directionalLight.shadow.camera.top = sb; directionalLight.shadow.camera.bottom = -sb;
264
- directionalLight.shadow.bias = -0.0005;
265
- scene.add(directionalLight); // Directional always added to scene
266
- // const helper = new THREE.CameraHelper( directionalLight.shadow.camera ); scene.add( helper ); // Debug Shadow Camera
267
  currentLights.push(directionalLight);
268
  }
269
-
270
- if (needsPointLight) {
271
- const ptLight = new THREE.PointLight(pointLightInfo.color, pointLightInfo.intensity, pointLightInfo.distance, pointLightInfo.decay);
272
- ptLight.position.set(pointLightInfo.pos.x, pointLightInfo.pos.y, pointLightInfo.pos.z);
273
- ptLight.castShadow = true;
274
- ptLight.shadow.mapSize.set(512, 512);
275
- ptLight.shadow.bias = -0.005; // Point lights might need more bias adjustment
276
- // Will be added to the zone group later
277
- currentLights.push(ptLight); // Track it
278
- }
279
  }
280
 
281
- // --- Zone Creation Functions (Add more for your 20 environments) ---
282
-
283
  function createFieldZone(zoneId) {
 
284
  const group = new THREE.Group();
285
  group.add(createGround(MAT.grass, 30));
286
- const rockGeo = new THREE.IcosahedronGeometry(0.5 + Math.random()*0.5, 0);
287
- for(let i=0; i<5; i++) {
288
- group.add(createMesh(rockGeo, MAT.stone, {x: (Math.random()-0.5)*25, y:0.3, z: (Math.random()-0.5)*25}));
289
- }
290
- // Add an item
291
- const potion = createMesh(itemsData["Health Potion"].meshGeo, itemsData["Health Potion"].meshMat, {x: 3, y: 0.2, z: 2});
292
- potion.userData = { isItem: true, itemName: "Health Potion" };
293
- group.add(potion);
294
-
295
  group.visible = false;
296
- return { group, lighting: 'default', title: "Open Field", entryText: "You stand in an open, grassy field. A small red potion lies nearby.", options: [
297
- { text: "Look for interesting plants (INT)", action: 'skillCheck', skill: 'int', difficulty: 10, successText: "You spot some edible herbs!", failureText:"Just grass.", reward: { item: "Health Potion" } } // Example skill check
298
- ], zoneId: zoneId, items: [potion], interactables: [] }; // Track items in zone
299
- }
300
-
301
- function createForestZone(zoneId) {
302
- const group = new THREE.Group();
303
- group.add(createGround(MAT.ground, 30));
304
- const trunkGeo = new THREE.CylinderGeometry(0.2, 0.3, 4, 8);
305
- const leafGeo = new THREE.SphereGeometry(1.5, 8, 6);
306
- for(let i=0; i<25; i++) {
307
- const x = (Math.random() - 0.5) * 28;
308
- const z = (Math.random() - 0.5) * 28;
309
- if(Math.sqrt(x*x+z*z) < 1) continue;
310
- const tree = new THREE.Group();
311
- const trunk = createMesh(trunkGeo, MAT.wood, {y: 2});
312
- const leaves = createMesh(leafGeo, MAT.leaf, {y: 4.5});
313
- tree.add(trunk); tree.add(leaves);
314
- tree.position.set(x, 0, z);
315
- tree.rotation.y = Math.random() * Math.PI * 2;
316
- group.add(tree);
317
- }
318
- // Add an item
319
- const sword = createMesh(itemsData["Rusty Sword"].meshGeo, itemsData["Rusty Sword"].meshMat, {x: -4, y: 0.5, z: -1}, {z: Math.PI/2});
320
- sword.userData = { isItem: true, itemName: "Rusty Sword" };
321
- group.add(sword);
322
-
323
- group.visible = false;
324
- return { group, lighting: 'forest', title: "Dark Forest", entryText: "Sunlight is sparse beneath the thick canopy. An old sword rests against a tree.", options: [
325
- { text: "Search for tracks (INT)", action: 'skillCheck', skill: 'int', difficulty: 12, successText: "You find signs of goblin passage.", failureText:"The forest floor reveals little." }
326
- ], zoneId: zoneId, items: [sword], interactables: [] };
327
- }
328
-
329
- function createCaveZone(zoneId) {
330
- const group = new THREE.Group();
331
- group.add(createGround(MAT.stone.clone().set({color: 0x555560}), 18));
332
- const wallGeo = new THREE.SphereGeometry(12, 32, 16, 0, Math.PI*2, 0, Math.PI*0.7);
333
- const walls = createMesh(wallGeo, MAT.stone, {y: 4});
334
- walls.material.side = THREE.BackSide;
335
- walls.material.receiveShadow = true; // Cave walls should receive shadow
336
- walls.castShadow = false;
337
- group.add(walls);
338
- const coneGeo = new THREE.ConeGeometry(0.2, 1.0, 8);
339
- for(let i=0; i<15; i++){
340
- 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});
341
- group.add(st);
342
- const sb = createMesh(coneGeo, MAT.stone, {x: (Math.random()-0.5)*16, y: 0.5, z: (Math.random()-0.5)*16});
343
- group.add(sb);
344
- }
345
- // Add an item
346
- const crystal = createMesh(itemsData["Cave Crystal"].meshGeo, itemsData["Cave Crystal"].meshMat, {x: 1, y: 0.8, z: -3});
347
- crystal.userData = { isItem: true, itemName: "Cave Crystal" };
348
- crystal.material.emissiveIntensity = 0.8; // Make it glow slightly
349
- group.add(crystal);
350
-
351
- group.visible = false;
352
- return { group, lighting: 'cave', title: "Dim Cave", entryText: "It's dark and damp. Water drips. A crystal glimmers faintly.", options: [
353
- { text: "Squeeze through a narrow gap (DEX)", action: 'skillCheck', skill: 'dex', difficulty: 14, successText: "You manage to wiggle through!", failureText:"You're too clumsy and get stuck briefly.", targetZoneId: 'zone_SPECIAL_SECRET' } // Example transition on success
354
- ], zoneId: zoneId, items: [crystal], interactables: [] };
355
  }
 
 
 
 
356
 
357
- function createRuinsZone(zoneId) {
358
- const group = new THREE.Group();
359
- group.add(createGround(MAT.dirt, 25));
360
- const wallGeo = new THREE.BoxGeometry(0.5, 2, 3);
361
- for(let i=0; i<8; i++) {
362
- const wall = createMesh(wallGeo, MAT.stone,
363
- {x: (Math.random()-0.5)*20, y:1, z: (Math.random()-0.5)*20},
364
- {y: Math.random() * Math.PI}
365
- );
366
- wall.scale.y = 0.5 + Math.random() * 0.8;
367
- wall.rotation.x = (Math.random()-0.5)*0.1;
368
- wall.rotation.z = (Math.random()-0.5)*0.1;
369
- group.add(wall);
370
- }
371
- // Add an interactable portal (Example of interactive movement point)
372
- const portalGeo = new THREE.RingGeometry(0.8, 1.2, 16);
373
- const portal = createMesh(portalGeo, MAT.portal, {x: 5, y: 1.5, z: -5}, {y: Math.PI/4});
374
- portal.userData = { isPortal: true, targetZoneId: getZoneId(0,0) }; // Example: links back to forest
375
- group.add(portal);
376
-
377
- group.visible = false;
378
- return { group, lighting: 'ruins', title: "Crumbling Ruins", entryText: "The wind whistles through broken walls. A strange shimmering portal hangs in the air.", options: [
379
- { text: "Try to decipher markings (INT)", action: 'skillCheck', skill: 'int', difficulty: 13, successText: "You recognize ancient symbols hinting at a hidden path.", failureText:"The markings are meaningless scratches." }
380
- ], zoneId: zoneId, items: [], interactables: [portal] };
381
- }
382
-
383
- function createDesertZone(zoneId) {
384
- const group = new THREE.Group();
385
- group.add(createGround(MAT.sand, 40));
386
- // Add some dunes (simple sine wave bumps)
387
- const duneGeo = new THREE.PlaneGeometry(40, 40, 20, 20);
388
- const posAttr = duneGeo.attributes.position;
389
- for (let i = 0; i < posAttr.count; i++) {
390
- const x = posAttr.getX(i);
391
- const z = posAttr.getZ(i);
392
- posAttr.setY(i, Math.sin(x * 0.2) * Math.cos(z * 0.3) * 0.8); // Simple dune formula
393
- }
394
- duneGeo.computeVertexNormals();
395
- const dunes = new THREE.Mesh(duneGeo, MAT.sand);
396
- dunes.rotation.x = -Math.PI / 2;
397
- dunes.receiveShadow = true;
398
- dunes.castShadow = false;
399
- dunes.userData.isGround = true; // Mark for raycasting
400
- group.add(dunes); // Replace flat ground with dunes
401
-
402
- // Add a key item
403
- const key = createMesh(itemsData["Ancient Key"].meshGeo, itemsData["Ancient Key"].meshMat, {x: -2, y: 0.2 + Math.sin(-2*0.2)*Math.cos(1*0.3)*0.8, z: 1}); // Place on dune surface
404
- key.userData = { isItem: true, itemName: "Ancient Key" };
405
- group.add(key);
406
-
407
- group.visible = false;
408
- return { group, lighting: 'desert', title: "Scorching Desert", entryText: "Endless dunes stretch under a blazing sun. Something glints in the sand.", options: [
409
- { text: "Endure the heat (STR - passive check?)", action: 'message', messageText:"The heat is oppressive but bearable for now." }, // Example passive/flavor option
410
- ], zoneId: zoneId, items: [key], interactables: [] };
411
- }
412
-
413
- function createMountainZone(zoneId) {
414
- const group = new THREE.Group();
415
- group.add(createGround(MAT.stone.clone().set({color: 0x9999aa}), 35)); // Rocky ground
416
-
417
- // Add jagged peaks
418
- const peakGeo = new THREE.ConeGeometry(3, 8, 8);
419
- for (let i=0; i<5; i++) {
420
- const peak = createMesh(peakGeo, MAT.stone.clone().set({color: 0xbbbbcc}),
421
- {x: (Math.random()-0.5)*30, y: 4, z: (Math.random()-0.5)*30},
422
- {x: (Math.random()-0.5)*0.2, z: (Math.random()-0.5)*0.2} // Slightly tilted
423
- );
424
- peak.scale.set(1 + Math.random(), 1 + Math.random()*1.5, 1 + Math.random());
425
- group.add(peak);
426
- }
427
- // Add icy patches
428
- const iceGeo = new THREE.PlaneGeometry(5,5);
429
- const icePatch = new THREE.Mesh(iceGeo, MAT.ice);
430
- icePatch.rotation.x = -Math.PI/2;
431
- icePatch.position.set(4, 0.1, -6); // Slightly above ground
432
- icePatch.receiveShadow = true;
433
- group.add(icePatch);
434
-
435
- group.visible = false;
436
- return { group, lighting: 'mountain', title: "Jagged Peaks", entryText: "Sharp rocks and biting wind dominate this high altitude pass. Patches of ice make footing treacherous.", options: [
437
- { text: "Climb a rocky outcrop (STR)", action: 'skillCheck', skill: 'str', difficulty: 14, successText: "You scale the rock, getting a better view!", failureText:"You slip and nearly fall. Best stay on the path." }
438
- ], zoneId: zoneId, items: [], interactables: [] };
439
- }
440
-
441
- // --- Utility Functions ---
442
-
443
- function getZoneId(row, col) { return `zone_${row}_${col}`; }
444
 
445
- function populateZoneCreators() {
446
- const creators = [
447
- createForestZone, createFieldZone, createCaveZone, createRuinsZone, createDesertZone, createMountainZone
448
- // Add ALL your 20 creator functions here
449
- ];
450
- let creatorIndex = 0;
451
  for (let r = 0; r < MAP_ROWS; r++) {
452
  for (let c = 0; c < MAP_COLS; c++) {
453
  const zoneId = getZoneId(r, c);
454
- // Cycle through the available creators - Customize this logic!
455
- const creatorFunc = creators[creatorIndex % creators.length];
456
- zoneCreators[zoneId] = () => creatorFunc(zoneId);
457
- creatorIndex++;
458
  }
459
  }
460
- console.log(`Zone creators populated for ${MAP_ROWS}x${MAP_COLS} grid.`);
461
- }
462
 
463
- function getZoneNeighbors(zoneId) {
464
  const parts = zoneId.split('_');
465
- if (parts.length !== 3 || parts[0] !== 'zone') return {};
466
  const r = parseInt(parts[1]);
467
  const c = parseInt(parts[2]);
468
  const neighbors = {};
469
- // Check bounds and if the zone exists in the creators map
470
  if (r > 0 && zoneCreators[getZoneId(r - 1, c)]) neighbors.north = getZoneId(r - 1, c);
471
  if (r < MAP_ROWS - 1 && zoneCreators[getZoneId(r + 1, c)]) neighbors.south = getZoneId(r + 1, c);
472
  if (c > 0 && zoneCreators[getZoneId(r, c - 1)]) neighbors.west = getZoneId(r, c - 1);
473
  if (c < MAP_COLS - 1 && zoneCreators[getZoneId(r, c + 1)]) neighbors.east = getZoneId(r, c + 1);
474
  return neighbors;
475
- }
476
-
477
- function logEvent(message) {
478
- if (!gameState.character || !gameState.character.history) return;
479
- gameState.character.history.push(message);
480
- // Optional: Limit history size
481
- // if (gameState.character.history.length > 20) {
482
- //     gameState.character.history.shift();
483
- // }
484
- updateHistoryDisplay(); // Update UI immediately
485
  }
486
 
487
- // --- Game Logic ---
488
-
489
  function startGame() {
 
490
  const defaultChar = {
491
- name: "Adventurer",
492
- stats: { hp: 25, maxHp: 25, xp: 0, str: 10, dex: 10, int: 10 }, // Added stats
493
- inventory: [],
494
- history: ["Game Started."] // Initialize history
495
  };
496
  gameState = {
497
- currentZoneId: null,
498
- character: JSON.parse(JSON.stringify(defaultChar))
499
  };
500
  populateZoneCreators();
501
- zoneGroups = {}; // Clear existing zones if restarting
502
  while(worldGroup.children.length > 0){
503
  worldGroup.remove(worldGroup.children[0]);
504
  }
505
- // Reset placement mode if game restarts
506
- placementMode = false;
507
- itemToPlace = null;
508
- if (previewMesh) {
509
- scene.remove(previewMesh);
510
- previewMesh = null;
511
- }
512
-
513
- console.log("Starting new game:", gameState);
514
- transitionToZone(getZoneId(MAP_ROWS-1, Math.floor(MAP_COLS/2))); // Start near bottom-center
515
  }
516
 
517
  function transitionToZone(newZoneId) {
518
- console.log(`Transitioning to ${newZoneId}`);
519
- currentMessage = ""; // Clear messages on zone change
520
 
521
- if (gameState.currentZoneId && zoneGroups[gameState.currentZoneId]) {
 
522
  zoneGroups[gameState.currentZoneId].group.visible = false;
523
- // Remove zone-specific lights from the old group before hiding
524
- currentLights.forEach(light => {
525
- if (light && light.isPointLight && light.parent === zoneGroups[gameState.currentZoneId].group) {
526
- zoneGroups[gameState.currentZoneId].group.remove(light);
527
- }
528
- });
529
- }
530
 
531
- let zoneInfo;
532
- if (zoneGroups[newZoneId]) {
533
  zoneInfo = zoneGroups[newZoneId];
534
  zoneInfo.group.visible = true;
535
- console.log(`Re-entering zone ${newZoneId}`);
536
- logEvent(`Entered ${zoneInfo.title || newZoneId}.`);
537
- } else {
538
  const creator = zoneCreators[newZoneId];
539
  if (creator) {
540
- zoneInfo = creator();
541
- zoneGroups[newZoneId] = zoneInfo;
542
- worldGroup.add(zoneInfo.group);
543
- zoneInfo.group.visible = true;
544
- console.log(`Created and entered zone ${newZoneId}`);
545
- logEvent(`Discovered and entered ${zoneInfo.title || newZoneId}.`);
546
- } else {
547
- console.error(`No creator found for zone ID: ${newZoneId}, going to fallback`);
548
- const fallbackId = getZoneId(MAP_ROWS-1, Math.floor(MAP_COLS/2)); // Fallback start
549
- if (!zoneGroups[fallbackId]) { // Ensure fallback exists
550
- zoneGroups[fallbackId] = zoneCreators[fallbackId]();
551
- worldGroup.add(zoneGroups[fallbackId].group);
552
- }
553
- zoneInfo = zoneGroups[fallbackId];
554
- zoneInfo.group.visible = true;
555
- newZoneId = fallbackId;
556
- currentMessage = `<p class="message message-failure">Error: Invalid zone transition. Returned to safety.</p>`;
557
- logEvent(`Error: Tried to enter invalid zone. Returned to ${zoneInfo.title || fallbackId}.`);
558
- }
559
- }
560
-
561
- gameState.currentZoneId = newZoneId;
562
- setupLighting(zoneInfo.lighting || 'default'); // Setup general lighting FIRST
563
-
564
- // Add zone-specific lights (like point lights for caves) to the *current* zone's group
565
- currentLights.forEach(light => {
566
- if (light && light.isPointLight && !light.parent) { // If it's a point light tracked but not in scene/group
567
- zoneInfo.group.add(light); // Add it to the now visible group
568
- console.log("Added point light to zone group:", newZoneId)
569
- }
570
- });
571
-
572
- // Move player avatar to center of the new zone (optional visual)
573
- playerAvatar.position.set(0, PLAYER_HEIGHT / 2, 0);
574
-
575
- // Reset camera target and position relative to avatar/center
576
- camera.position.set(0, 6, 8); // Reset relative position
577
- controls.target.set(0, PLAYER_HEIGHT / 2, 0); // Target avatar center
578
- controls.update();
579
-
580
- // End placement mode on zone transition
581
- if (placementMode) {
582
- togglePlacementMode();
583
- }
584
-
585
- renderCurrentPageUI();
586
- }
587
-
588
- // --- Interaction Handling ---
589
-
590
- function handleKeyPress(event) {
591
- if (event.key.toUpperCase() === 'P') {
592
- togglePlacementMode();
593
- }
594
- }
595
-
596
- function handleRaycast(event) {
597
- // Calculate mouse position in normalized device coordinates (-1 to +1)
598
- const rect = renderer.domElement.getBoundingClientRect();
599
- mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
600
- mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
601
-
602
- // Update the picking ray with the camera and mouse position
603
- raycaster.setFromCamera(mouse, camera);
604
-
605
- // Calculate objects intersecting the picking ray
606
- // Important: Check against the *current visible zone group's children* and potentially other global interactables
607
- const currentZoneGroup = zoneGroups[gameState.currentZoneId]?.group;
608
- if (!currentZoneGroup) return;
609
-
610
- const intersects = raycaster.intersectObjects(currentZoneGroup.children, true); // Check children recursively
611
-
612
- if (intersects.length > 0) {
613
- const firstIntersect = intersects[0];
614
- const obj = firstIntersect.object;
615
- const point = firstIntersect.point; // World coordinates of the intersection
616
-
617
- if (placementMode) {
618
- // If placing, click on ground places item
619
- if (obj.userData.isGround || obj.parent?.userData?.isGround) { // Check obj or its immediate parent if ground is complex
620
- placeItem(point);
621
- } else {
622
- console.log("Cannot place item here.");
623
- currentMessage = `<p class="message message-failure">You can only place items on the ground.</p>`;
624
- renderCurrentPageUI(); // Show message
625
  }
626
- } else {
627
- // Normal mode: Check for items or interactables
628
- if (obj.userData?.isItem) {
629
- pickupItem(obj, currentZoneGroup);
630
- } else if (obj.userData?.isPortal) {
631
- console.log(`Clicked Portal to: ${obj.userData.targetZoneId}`);
632
- logEvent(`Stepped through the portal.`);
633
- transitionToZone(obj.userData.targetZoneId);
634
- } else if (obj.userData?.isGround) {
635
- console.log("Clicked ground at:", point);
636
- // Future: Could implement click-to-move here
637
- } else {
638
- console.log("Clicked non-interactive object:", obj.name, obj.userData);
639
- }
640
- }
641
- }
642
- }
643
-
644
- function pickupItem(itemMesh, zoneGroup) {
645
- const itemName = itemMesh.userData.itemName;
646
- if (!itemName || !itemsData[itemName]) {
647
- console.error("Clicked item mesh has invalid data:", itemMesh);
648
- return;
649
- }
650
-
651
- // Add to inventory
652
- gameState.character.inventory.push(itemName);
653
-
654
- // Remove from scene / zone group
655
- zoneGroup.remove(itemMesh);
656
-
657
- // Remove from zoneInfo's item list (if tracked)
658
- const zoneInfo = zoneGroups[gameState.currentZoneId];
659
- if (zoneInfo && zoneInfo.items) {
660
- zoneInfo.items = zoneInfo.items.filter(item => item !== itemMesh);
661
- }
662
-
663
- // Set the item for potential placement later
664
- itemToPlace = itemName; // Track the last picked item's name
665
-
666
- console.log(`Picked up: ${itemName}`);
667
- currentMessage = `<p class="message message-success">You picked up: ${itemName}. Press 'P' to place it.</p>`;
668
- logEvent(`Picked up ${itemName}.`);
669
-
670
- renderCurrentPageUI(); // Update inventory display and message
671
- }
672
-
673
- function togglePlacementMode() {
674
- if (!itemToPlace) {
675
- currentMessage = `<p class="message message-info">You have no item selected to place. Pick up an item first.</p>`;
676
- renderCurrentPageUI();
677
- return; // Don't enter placement mode without an item
678
- }
679
-
680
- placementMode = !placementMode;
681
- console.log("Placement Mode:", placementMode);
682
-
683
- if (placementMode) {
684
- actionInfoElement.textContent = `Placement Mode ON (Placing: ${itemToPlace}). Click ground to place. Press P to cancel.`;
685
- // Create preview mesh if it doesn't exist
686
- if (!previewMesh) {
687
- const itemData = itemsData[itemToPlace];
688
- if (itemData && itemData.meshGeo) {
689
- // Use a generic preview material but original geometry
690
- previewMesh = new THREE.Mesh(itemData.meshGeo, MAT.preview);
691
- previewMesh.renderOrder = 1; // Try to render on top
692
- previewMesh.userData.isPreview = true;
693
- scene.add(previewMesh); // Add preview directly to the main scene
694
- } else {
695
- console.error("Cannot create preview for item:", itemToPlace);
696
- placementMode = false; // Exit placement mode if preview fails
697
- actionInfoElement.textContent = `Zone: ${gameState.currentZoneId || 'None'}`;
698
- return;
699
- }
700
- }
701
- previewMesh.visible = true;
702
- updatePreviewMesh(); // Position it initially
703
- } else {
704
- actionInfoElement.textContent = `Zone: ${gameState.currentZoneId || 'None'}`;
705
- if (previewMesh) {
706
- previewMesh.visible = false; // Hide preview instead of removing
707
- }
708
- // Optionally clear itemToPlace here if cancelling should forget the item
709
- // itemToPlace = null;
710
- currentMessage = `<p class="message message-info">Placement mode cancelled.</p>`;
711
- renderCurrentPageUI();
712
- }
713
- }
714
-
715
- function updatePreviewMesh() {
716
- if (!placementMode || !previewMesh) return;
717
-
718
- // Raycast from camera to mouse position, ONLY against ground objects
719
- raycaster.setFromCamera(mouse, camera);
720
- const currentZoneGroup = zoneGroups[gameState.currentZoneId]?.group;
721
- if (!currentZoneGroup) return;
722
-
723
- // Filter for ground objects in the current zone
724
- const groundObjects = [];
725
- currentZoneGroup.traverse(child => {
726
- if (child.userData?.isGround) {
727
- groundObjects.push(child);
728
- }
729
- })
730
-
731
- const intersects = raycaster.intersectObjects(groundObjects, false); // Don't check recursively if ground is simple plane
732
-
733
- if (intersects.length > 0) {
734
- const point = intersects[0].point;
735
- previewMesh.position.copy(point);
736
- // Add small offset based on item geometry bounds if available
737
- const itemData = itemsData[itemToPlace];
738
- if (itemData && itemData.meshGeo) {
739
- itemData.meshGeo.computeBoundingBox();
740
- const height = itemData.meshGeo.boundingBox.max.y - itemData.meshGeo.boundingBox.min.y;
741
- previewMesh.position.y += height / 2 + 0.01; // Place bottom slightly above ground
742
  } else {
743
- previewMesh.position.y += 0.1; // Default offset
 
 
 
 
 
 
744
  }
745
- previewMesh.visible = true;
746
- } else {
747
- previewMesh.visible = false; // Hide if not pointing at ground
748
  }
749
- }
750
 
 
 
751
 
752
- function placeItem(position) {
753
- if (!placementMode || !itemToPlace) return;
754
-
755
- const itemData = itemsData[itemToPlace];
756
- const currentZoneGroup = zoneGroups[gameState.currentZoneId]?.group;
757
- const zoneInfo = zoneGroups[gameState.currentZoneId];
758
-
759
- if (!itemData || !currentZoneGroup || !zoneInfo) {
760
- console.error("Cannot place item, missing data.");
761
- togglePlacementMode(); // Exit placement mode on error
762
- return;
763
- }
764
-
765
- // Create the actual item mesh
766
- const newItemMesh = createMesh(itemData.meshGeo, itemData.meshMat);
767
- newItemMesh.userData = { isItem: true, itemName: itemToPlace };
768
-
769
- // Adjust position based on geometry height, like the preview
770
- itemData.meshGeo.computeBoundingBox();
771
- const height = itemData.meshGeo.boundingBox.max.y - itemData.meshGeo.boundingBox.min.y;
772
- position.y += height / 2 + 0.01;
773
- newItemMesh.position.copy(position);
774
-
775
-
776
- // Add to the scene (current zone's group)
777
- currentZoneGroup.add(newItemMesh);
778
-
779
- // Add to the zone's item list for tracking
780
- if (!zoneInfo.items) zoneInfo.items = [];
781
- zoneInfo.items.push(newItemMesh);
782
-
783
- // Remove from inventory
784
- const invIndex = gameState.character.inventory.indexOf(itemToPlace);
785
- if (invIndex > -1) {
786
- gameState.character.inventory.splice(invIndex, 1);
787
- }
788
-
789
- console.log(`Placed ${itemToPlace} at ${position.x.toFixed(2)}, ${position.y.toFixed(2)}, ${position.z.toFixed(2)}`);
790
- currentMessage = `<p class="message message-success">You placed the ${itemToPlace}.</p>`;
791
- logEvent(`Placed ${itemToPlace} in ${zoneInfo.title || gameState.currentZoneId}.`);
792
-
793
- itemToPlace = null; // Clear the item being placed
794
- togglePlacementMode(); // Exit placement mode
795
- renderCurrentPageUI(); // Update UI
796
- }
797
-
798
- function performSkillCheck(skill, difficulty) {
799
- if (!gameState.character || !gameState.character.stats[skill]) {
800
- console.error("Invalid skill check:", skill);
801
- return { success: false, roll: 0, resultValue: 0, message: "Error: Invalid skill." };
802
- }
803
-
804
- const roll = Math.floor(Math.random() * 20) + 1;
805
- const skillValue = gameState.character.stats[skill] || 0; // Use 0 if stat doesn't exist
806
- const resultValue = roll + skillValue;
807
- const success = resultValue >= difficulty;
808
 
809
- console.log(`Skill Check: ${skill.toUpperCase()} (DC ${difficulty}) | Roll: ${roll} + Stat: ${skillValue} = ${resultValue} | ${success ? 'Success' : 'Failure'}`);
 
 
 
810
 
811
- return { success, roll, skillValue, resultValue };
 
812
  }
813
 
814
-
815
- // --- UI Rendering ---
816
-
817
  function renderCurrentPageUI() {
 
818
  const zoneInfo = zoneGroups[gameState.currentZoneId];
819
  const zoneId = gameState.currentZoneId;
820
 
821
- if (!zoneInfo) {
822
- console.error(`No zone info loaded for ${zoneId}`);
823
- storyTitleElement.textContent = "Error";
824
- storyContentElement.innerHTML = currentMessage + "<p>Cannot render current zone. World broken.</p>";
825
- choicesElement.innerHTML = `<button class="choice-button" onclick="startGame()">Restart Game</button>`; // Offer restart
826
- updateStatsDisplay();
827
- updateInventoryDisplay();
828
- updateHistoryDisplay();
829
- updateActionInfo();
830
- return;
 
 
 
 
 
 
831
  }
 
832
 
833
  storyTitleElement.textContent = zoneInfo.title || "Unknown Zone";
834
- // Combine current message (from actions) with the zone's entry text
835
  storyContentElement.innerHTML = currentMessage + (zoneInfo.entryText ? `<p>${zoneInfo.entryText}</p>` : '');
836
  choicesElement.innerHTML = ''; // Clear previous choices
837
 
838
- // 1. Add Zone-Specific Options (including skill checks)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
839
  if (zoneInfo.options && zoneInfo.options.length > 0) {
840
- zoneInfo.options.forEach((option, index) => {
 
841
  const button = document.createElement('button');
842
  button.classList.add('choice-button');
843
  button.textContent = option.text;
844
-
845
- button.onclick = () => {
846
- currentMessage = ""; // Clear previous action message before processing new one
847
- if (option.action === 'skillCheck') {
848
- const checkResult = performSkillCheck(option.skill, option.difficulty);
849
- const messageClass = checkResult.success ? 'message-success' : 'message-failure';
850
- const outcomeText = checkResult.success ? option.successText : option.failureText;
851
- currentMessage = `<p class="message ${messageClass}">(${option.skill.toUpperCase()} DC ${option.difficulty}: ${checkResult.roll}+${checkResult.skillValue}=${checkResult.resultValue}) ${outcomeText}</p>`;
852
- logEvent(`Skill Check ${option.skill.toUpperCase()}: ${checkResult.success ? 'Success' : 'Failure'} (${checkResult.resultValue} vs ${option.difficulty}).`);
853
-
854
- if (checkResult.success) {
855
- // Grant reward if defined
856
- if (option.reward?.item && itemsData[option.reward.item]) {
857
- gameState.character.inventory.push(option.reward.item);
858
- currentMessage += `<p class="message message-success">You gained: ${option.reward.item}!</p>`;
859
- logEvent(`Gained ${option.reward.item}.`);
860
- }
861
- // Transition if defined
862
- if(option.targetZoneId) {
863
- transitionToZone(option.targetZoneId);
864
- return; // Stop further processing for this click if transitioning
865
- }
866
- // TODO: Could also modify zoneInfo.options or zoneInfo.entryText here for persistent changes
867
- } else {
868
- // Handle failure consequences if any (e.g., take damage, trigger event)
869
- }
870
-
871
- } else if (option.action === 'message') {
872
- currentMessage = `<p class="message message-info">${option.messageText}</p>`;
873
- logEvent(`Observed: ${option.messageText}`);
874
- } else if (option.action === 'goToZone') {
875
- logEvent(`Chose to go to ${option.targetZoneId}.`);
876
- transitionToZone(option.targetZoneId);
877
- return; // Stop processing
878
- }
879
- // Add more action types here (combat, useItem, etc.)
880
-
881
- renderCurrentPageUI(); // Re-render UI to show the result message
882
- };
883
  choicesElement.appendChild(button);
 
884
  });
885
  }
886
 
887
- // 2. Add Movement Options (Buttons)
888
- const neighbors = getZoneNeighbors(zoneId);
889
- const directions = {'north': 'North', 'south': 'South', 'east': 'East', 'west': 'West'};
890
- for(const dir in neighbors) {
891
- const neighborId = neighbors[dir];
892
- const button = document.createElement('button');
893
- button.classList.add('choice-button');
894
- // Maybe get the title of the neighbor zone for a hint?
895
- const neighborInfo = zoneGroups[neighborId] ?? zoneCreators[neighborId]?.(); // Preview or get existing
896
- const neighborTitle = neighborInfo?.title ? ` (${neighborInfo.title})` : '';
897
-
898
- button.textContent = `Go ${directions[dir]}${neighborTitle}`;
899
- button.onclick = () => transitionToZone(neighborId);
900
- choicesElement.appendChild(button);
901
-
902
- // Cleanup preview if generated just for title
903
- if(neighborInfo && !zoneGroups[neighborId]) {
904
- // If we generated it just for the title and it wasn't already loaded, dispose of it? Or just let it be created on transition.
905
- }
906
- }
907
 
908
- // 3. Update other UI elements
909
  updateStatsDisplay();
910
  updateInventoryDisplay();
911
- updateHistoryDisplay(); // Make sure history is up-to-date
912
  updateActionInfo();
 
 
 
 
 
 
913
  }
 
914
 
915
- // Make transitionToZone globally accessible for initial call and error recovery button if needed
916
- window.transitionToZone = transitionToZone;
917
- window.startGame = startGame; // Allow restarting from console or error button
918
 
919
  function updateStatsDisplay() {
920
- const { hp, maxHp, xp, str, dex, int } = gameState.character.stats;
 
921
  const hpColor = hp / maxHp < 0.3 ? '#f88' : (hp / maxHp < 0.6 ? '#fd5' : '#8f8');
922
- statsElement.innerHTML = `<strong>Stats:</strong> <span style="color:${hpColor}">HP: ${hp}/${maxHp}</span> <span>XP: ${xp}</span> <span>STR: ${str}</span> <span>DEX: ${dex}</span> <span>INT: ${int}</span>`;
923
  }
924
 
925
  function updateInventoryDisplay() {
 
926
  let invHtml = '<strong>Inventory:</strong> ';
927
  if (gameState.character.inventory.length === 0) {
928
  invHtml += '<em>Empty</em>';
929
  } else {
930
- // Group items
931
- const itemCounts = gameState.character.inventory.reduce((acc, item) => {
932
- acc[item] = (acc[item] || 0) + 1;
933
- return acc;
934
- }, {});
935
-
936
- Object.entries(itemCounts).forEach(([itemName, count]) => {
937
- const itemDef = itemsData[itemName] || { type: 'unknown', description: '???' };
938
- const itemClass = `item-${itemDef.type || 'unknown'}`;
939
- const countDisplay = count > 1 ? ` (x${count})` : '';
940
- invHtml += `<span class="item-tag ${itemClass}" title="${itemDef.description}">${itemName}${countDisplay}</span>`;
941
  });
942
  }
943
  inventoryElement.innerHTML = invHtml;
944
  }
945
 
946
- function updateHistoryDisplay() {
947
- if (!historyElement || !gameState.character || !gameState.character.history) return;
948
- let historyHtml = '<strong>History:</strong>';
949
- // Display latest events first
950
- for (let i = gameState.character.history.length - 1; i >= 0; i--) {
951
- historyHtml += `<div>${gameState.character.history[i]}</div>`;
952
- }
953
- historyElement.innerHTML = historyHtml;
954
- historyElement.scrollTop = 0; // Scroll to top to show latest
955
- }
956
-
957
  function updateActionInfo() {
958
- if (!actionInfoElement) return;
959
- if (placementMode) {
960
- actionInfoElement.textContent = `Placement Mode ON (Placing: ${itemToPlace}). Click ground to place. Press P to cancel.`;
961
- } else {
962
- actionInfoElement.textContent = `Zone: ${gameState.currentZoneId || 'None'}`;
963
- }
964
  }
965
 
966
- // --- Initialization ---
 
 
967
  document.addEventListener('DOMContentLoaded', () => {
968
- console.log("DOM Ready - Initializing Enhanced CYOA.");
969
  try {
970
- initThreeJS();
971
  if (!scene || !camera || !renderer) throw new Error("Three.js failed to initialize.");
972
- startGame(); // Start game directly
973
- console.log("Game world initialized and started.");
974
  } catch (error) {
975
  console.error("Initialization failed:", error);
976
  storyTitleElement.textContent = "Initialization Error";
977
- storyContentElement.innerHTML = `<p style="color:red;">Failed to start game:</p><pre style="color:red; white-space: pre-wrap;">${error.stack || error}</pre>`;
978
  if(sceneContainer) sceneContainer.innerHTML = '<p style="color:red; padding: 20px;">3D Scene Failed</p>';
979
- document.getElementById('stats-inventory-container').style.display = 'none';
980
- document.getElementById('choices-container').innerHTML = '<button class="choice-button" onclick="location.reload()">Reload Page</button>'; // Offer reload on critical fail
 
 
 
981
  }
982
  });
983
 
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>World Grid Test (Startup Debug)</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; }
14
+ #story-content { margin-bottom: 25px; line-height: 1.7; flex-grow: 1; font-size: 1.1em; }
15
+ #stats-inventory-container { margin-bottom: 25px; padding: 15px; border: 1px solid #444; border-radius: 4px; background-color: #333; font-size: 0.95em; }
16
+ #stats-display, #inventory-display { margin-bottom: 10px; line-height: 1.8; }
 
 
 
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
  .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;}
 
26
  .message-failure { color: #f88; border-left-color: #a44; }
 
27
  #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;}
28
+
 
29
  </style>
30
  </head>
31
  <body>
 
37
  <h2 id="story-title">Initializing...</h2>
38
  <div id="story-content"><p>Loading assets...</p></div>
39
  <div id="stats-inventory-container">
40
+ <div id="stats-display">HP: ?/? | XP: ?</div>
41
  <div id="inventory-display">Inventory: Empty</div>
 
42
  </div>
43
  <div id="choices-container">
44
  <h3>What will you do?</h3>
 
49
 
50
  <script type="importmap">
51
  { "imports": {
52
+ "three": "https://unpkg.com/[email protected]/build/three.module.js",
53
+ "three/addons/": "https://unpkg.com/[email protected]/examples/jsm/"
54
  }}
55
  </script>
56
 
57
  <script type="module">
58
  import * as THREE from 'three';
59
+ // import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; // Temporarily removed
60
+
61
+ console.log("Script module execution started.");
62
 
 
63
  const sceneContainer = document.getElementById('scene-container');
64
  const storyTitleElement = document.getElementById('story-title');
65
  const storyContentElement = document.getElementById('story-content');
66
  const choicesElement = document.getElementById('choices');
67
  const statsElement = document.getElementById('stats-display');
68
  const inventoryElement = document.getElementById('inventory-display');
 
69
  const actionInfoElement = document.getElementById('action-info');
70
 
71
+ console.log("DOM elements obtained.");
 
 
 
 
 
72
 
73
+ let scene, camera, renderer, clock; // Removed controls, raycaster, mouse
74
+ let worldGroup = null;
75
+ let zoneGroups = {};
76
  let currentMessage = "";
77
+ let currentLights = [];
 
 
 
 
 
 
 
78
 
79
+ const MAT = { // Simplified materials
 
80
  stone: new THREE.MeshStandardMaterial({ color: 0x777788, roughness: 0.85 }),
81
  wood: new THREE.MeshStandardMaterial({ color: 0x9F6633, roughness: 0.75 }),
82
  leaf: new THREE.MeshStandardMaterial({ color: 0x3E9B4E, roughness: 0.6, side: THREE.DoubleSide }),
83
  ground: new THREE.MeshStandardMaterial({ color: 0x556B2F, roughness: 0.95 }),
84
  dirt: new THREE.MeshStandardMaterial({ color: 0x8B5E3C, roughness: 0.9 }),
85
  grass: new THREE.MeshStandardMaterial({ color: 0x4CB781, roughness: 0.85 }),
 
 
 
 
 
 
 
86
  };
87
 
88
+ let gameState = {};
89
+
90
  const itemsData = {
91
+ "Rock": {type:"unknown", description:"A simple rock."} // Minimal items
 
 
 
 
92
  };
93
 
94
+ const zoneCreators = {};
95
+ const MAP_ROWS = 3;
96
+ const MAP_COLS = 4;
97
 
98
  function initThreeJS() {
99
+ console.log("initThreeJS started.");
100
+ try {
101
+ scene = new THREE.Scene();
102
+ scene.background = new THREE.Color(0x1a1a1a);
103
+ clock = new THREE.Clock();
104
+ worldGroup = new THREE.Group();
105
+ scene.add(worldGroup);
106
+ console.log("Scene and worldGroup created.");
107
+
108
+ const width = sceneContainer.clientWidth || 300; // Default size if 0
109
+ const height = sceneContainer.clientHeight || 200;
110
+ console.log(`Renderer dimensions: ${width}x${height}`);
111
+
112
+ camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 1000);
113
+ camera.position.set(0, 6, 12);
114
+ camera.lookAt(0, 1, 0);
115
+ console.log("Camera created.");
116
+
117
+ renderer = new THREE.WebGLRenderer({ antialias: true });
118
+ renderer.setSize(width, height);
119
+ renderer.shadowMap.enabled = true; // Basic shadows
120
+ renderer.shadowMap.type = THREE.PCFSoftShadowMap;
121
+ sceneContainer.appendChild(renderer.domElement);
122
+ console.log("Renderer created and appended.");
123
+
124
+ // controls = new OrbitControls(camera, renderer.domElement); // Removed
125
+ // controls.target.set(0, 1, 0);
126
+
127
+ window.addEventListener('resize', onWindowResize, false);
128
+ setTimeout(onWindowResize, 100); // Try later resize
129
+ animate();
130
+ console.log("initThreeJS finished successfully.");
131
+ } catch (error) {
132
+ console.error("Error during initThreeJS:", error);
133
+ throw error; // Re-throw to be caught by DOMContentLoaded listener
134
+ }
 
 
 
 
 
 
135
  }
136
 
137
  function onWindowResize() {
138
+ console.log("onWindowResize called.");
139
  if (!renderer || !camera || !sceneContainer) return;
140
+ const width = sceneContainer.clientWidth || 300;
141
+ const height = sceneContainer.clientHeight || 200;
142
+ if (width > 0 && height > 0) {
143
+ camera.aspect = width / height;
144
+ camera.updateProjectionMatrix();
145
+ renderer.setSize(width, height);
146
+ console.log(`Resized renderer to ${width}x${height}`);
147
+ } else {
148
+ console.warn("Skipping resize, zero dimensions detected.");
149
+ }
150
  }
151
 
152
+ let frameCount = 0;
153
  function animate() {
154
+ frameCount++;
155
+ if(frameCount % 300 === 0) console.log("Animate loop running..."); // Log every ~5 seconds
156
  requestAnimationFrame(animate);
157
+ // controls?.update(); // Removed controls
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
  if (renderer && scene && camera) renderer.render(scene, camera);
159
  }
160
 
 
165
  mesh.scale.set(scale.x, scale.y, scale.z);
166
  mesh.castShadow = true; mesh.receiveShadow = true;
167
  return mesh;
168
+ }
169
 
170
  function createGround(material = MAT.ground, size = 20) {
171
  const geo = new THREE.PlaneGeometry(size, size);
172
  const ground = new THREE.Mesh(geo, material);
173
  ground.rotation.x = -Math.PI / 2; ground.position.y = 0;
174
+ ground.receiveShadow = true; ground.castShadow = false;
175
+ ground.userData.isGround = true;
176
  return ground;
177
  }
178
 
179
  function setupLighting(type = 'default') {
180
+ console.log(`Setting up lighting for type: ${type}`);
181
+ currentLights.forEach(light => {
182
+ if (light && light.parent) light.parent.remove(light); // Remove from parent if attached
183
+ if (light && scene.children.includes(light)) scene.remove(light); // Ensure removed from scene
184
  });
185
  currentLights = [];
186
 
187
+ let ambientIntensity = 0.6; // Brighter default ambient
188
  let dirIntensity = 1.0;
189
  let dirColor = 0xffffff;
190
+ let dirPosition = new THREE.Vector3(5, 10, 7); // Adjusted angle
 
 
 
 
 
 
 
 
 
 
 
191
 
192
  const ambientLight = new THREE.AmbientLight(0xffffff, ambientIntensity);
193
+ scene.add(ambientLight);
194
  currentLights.push(ambientLight);
195
 
196
  if (dirIntensity > 0) {
197
  const directionalLight = new THREE.DirectionalLight(dirColor, dirIntensity);
198
  directionalLight.position.copy(dirPosition);
199
  directionalLight.castShadow = true;
200
+ directionalLight.shadow.mapSize.set(512, 512); // Smaller shadow map for testing
201
+ scene.add(directionalLight);
 
 
 
 
 
 
202
  currentLights.push(directionalLight);
203
  }
204
+ console.log(`Lighting setup complete. Lights: ${currentLights.length}`);
 
 
 
 
 
 
 
 
 
205
  }
206
 
207
+ // Simplified Zone Creation
 
208
  function createFieldZone(zoneId) {
209
+ console.log(`Creating zone: ${zoneId} (Field)`);
210
  const group = new THREE.Group();
211
  group.add(createGround(MAT.grass, 30));
212
+ const rockGeo = new THREE.IcosahedronGeometry(1, 0); // Bigger, simpler rock
213
+ group.add(createMesh(rockGeo, MAT.stone, {x: 5, y:0.5, z: 5}));
 
 
 
 
 
 
 
214
  group.visible = false;
215
+ return { group, lighting: 'default', title: "Open Field", entryText: `You are in ${zoneId}. It's grassy.`, options: [], zoneId: zoneId };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
  }
217
+ // Use Field for all zones for now
218
+ const createForestZone = (zoneId) => createFieldZone(zoneId);
219
+ const createCaveZone = (zoneId) => createFieldZone(zoneId);
220
+ const createRuinsZone = (zoneId) => createFieldZone(zoneId);
221
 
222
+ function getZoneId(row, col) { return `zone_${row}_${col}`; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
 
224
+ function populateZoneCreators() {
225
+ console.log("Populating zone creators...");
226
+ // No reassignment here
 
 
 
227
  for (let r = 0; r < MAP_ROWS; r++) {
228
  for (let c = 0; c < MAP_COLS; c++) {
229
  const zoneId = getZoneId(r, c);
230
+ // Force all to be FieldZone for testing stability
231
+ zoneCreators[zoneId] = () => createFieldZone(zoneId);
 
 
232
  }
233
  }
234
+ console.log(`Zone creators populated with ${Object.keys(zoneCreators).length} entries.`);
235
+ }
236
 
237
+ function getZoneNeighbors(zoneId) {
238
  const parts = zoneId.split('_');
239
+ if (parts.length !== 3) return {};
240
  const r = parseInt(parts[1]);
241
  const c = parseInt(parts[2]);
242
  const neighbors = {};
 
243
  if (r > 0 && zoneCreators[getZoneId(r - 1, c)]) neighbors.north = getZoneId(r - 1, c);
244
  if (r < MAP_ROWS - 1 && zoneCreators[getZoneId(r + 1, c)]) neighbors.south = getZoneId(r + 1, c);
245
  if (c > 0 && zoneCreators[getZoneId(r, c - 1)]) neighbors.west = getZoneId(r, c - 1);
246
  if (c < MAP_COLS - 1 && zoneCreators[getZoneId(r, c + 1)]) neighbors.east = getZoneId(r, c + 1);
247
  return neighbors;
 
 
 
 
 
 
 
 
 
 
248
  }
249
 
 
 
250
  function startGame() {
251
+ console.log("startGame called.");
252
  const defaultChar = {
253
+ name: "Player",
254
+ stats: { hp: 20, maxHp: 20, xp: 0 },
255
+ inventory: []
 
256
  };
257
  gameState = {
258
+ currentZoneId: null,
259
+ character: JSON.parse(JSON.stringify(defaultChar))
260
  };
261
  populateZoneCreators();
262
+ zoneGroups = {};
263
  while(worldGroup.children.length > 0){
264
  worldGroup.remove(worldGroup.children[0]);
265
  }
266
+ console.log("Starting new game state:", gameState);
267
+ transitionToZone(getZoneId(1, 1));
268
+ console.log("startGame finished.");
 
 
 
 
 
 
 
269
  }
270
 
271
  function transitionToZone(newZoneId) {
272
+ console.log(`Attempting transition to ${newZoneId}`);
273
+ currentMessage = "";
274
 
275
+ if (gameState.currentZoneId && zoneGroups[gameState.currentZoneId]) {
276
+ console.log(`Hiding old zone: ${gameState.currentZoneId}`);
277
  zoneGroups[gameState.currentZoneId].group.visible = false;
278
+ } else {
279
+ console.log("No current zone to hide.");
280
+ }
 
 
 
 
281
 
282
+ let zoneInfo;
283
+ if (zoneGroups[newZoneId]) {
284
  zoneInfo = zoneGroups[newZoneId];
285
  zoneInfo.group.visible = true;
286
+ console.log(`Re-entering cached zone ${newZoneId}`);
287
+ } else {
 
288
  const creator = zoneCreators[newZoneId];
289
  if (creator) {
290
+ try {
291
+ zoneInfo = creator();
292
+ zoneGroups[newZoneId] = zoneInfo;
293
+ worldGroup.add(zoneInfo.group);
294
+ zoneInfo.group.visible = true;
295
+ console.log(`Created and entered new zone ${newZoneId}`);
296
+ } catch (creationError) {
297
+ console.error(`Error creating zone ${newZoneId}:`, creationError);
298
+ currentMessage = `<p class="message message-failure">Error creating zone ${newZoneId}.</p>`;
299
+ // Fallback to start or previous state might be needed here
300
+ // For now, just log and potentially break rendering
301
+ renderCurrentPageUI(); // Render even if zone creation failed to show error
302
+ return; // Stop transition
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
303
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
304
  } else {
305
+ console.error(`No creator found for zone ID: ${newZoneId}, critical error.`);
306
+ currentMessage = `<p class="message message-failure">Error: Zone creator missing for ${newZoneId}. Cannot proceed.</p>`;
307
+ // Render minimal UI or stop
308
+ storyTitleElement.textContent = "Fatal Error";
309
+ storyContentElement.innerHTML = currentMessage;
310
+ choicesElement.innerHTML = '';
311
+ return; // Stop transition
312
  }
 
 
 
313
  }
 
314
 
315
+ gameState.currentZoneId = newZoneId;
316
+ setupLighting(zoneInfo.lighting || 'default');
317
 
318
+ // Add point lights from lighting setup to the current group
319
+ currentLights.forEach(light => {
320
+ if (light && light.isPointLight) {
321
+ if(light.parent) light.parent.remove(light); // Ensure it's not attached elsewhere
322
+ zoneInfo.group.add(light);
323
+ console.log("Added point light to zone group:", newZoneId);
324
+ }
325
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
326
 
327
+ camera.position.set(0, 6, 12);
328
+ // controls.target.set(0, 1, 0); // Controls removed
329
+ // controls.update();
330
+ camera.lookAt(0, 1, 0); // Look at center
331
 
332
+ console.log(`Transition to ${newZoneId} complete. Rendering UI.`);
333
+ renderCurrentPageUI();
334
  }
335
 
 
 
 
336
  function renderCurrentPageUI() {
337
+ console.log(`renderCurrentPageUI called for zone: ${gameState.currentZoneId}`);
338
  const zoneInfo = zoneGroups[gameState.currentZoneId];
339
  const zoneId = gameState.currentZoneId;
340
 
341
+ // Ensure critical elements exist before proceeding
342
+ if (!storyTitleElement || !storyContentElement || !choicesElement || !statsElement || !inventoryElement || !actionInfoElement) {
343
+ console.error("Crucial UI element missing!");
344
+ alert("UI Error - Cannot render page. Check console.");
345
+ return;
346
+ }
347
+
348
+ if (!zoneInfo || !zoneInfo.group) {
349
+ console.error(`No zone info or group loaded for ${zoneId}`);
350
+ storyTitleElement.textContent = "Error";
351
+ storyContentElement.innerHTML = currentMessage + "<p>Cannot render current zone data.</p>";
352
+ choicesElement.innerHTML = `<button class="choice-button" onclick="transitionToZoneWrapper('${getZoneId(1, 1)}')">Return to Start</button>`; // Use wrapper
353
+ updateStatsDisplay();
354
+ updateInventoryDisplay();
355
+ updateActionInfo();
356
+ return;
357
  }
358
+ console.log(`Rendering UI for zone ${zoneId} with title "${zoneInfo.title}"`);
359
 
360
  storyTitleElement.textContent = zoneInfo.title || "Unknown Zone";
 
361
  storyContentElement.innerHTML = currentMessage + (zoneInfo.entryText ? `<p>${zoneInfo.entryText}</p>` : '');
362
  choicesElement.innerHTML = ''; // Clear previous choices
363
 
364
+ // Add Navigation Options
365
+ const neighbors = getZoneNeighbors(zoneId);
366
+ const directions = {'north': 'North', 'south': 'South', 'east': 'East', 'west': 'West'};
367
+ let addedChoices = 0;
368
+ console.log("Adding neighbor buttons:", neighbors);
369
+ for(const dir in neighbors) {
370
+ const neighborId = neighbors[dir];
371
+ const button = document.createElement('button');
372
+ button.classList.add('choice-button');
373
+ button.textContent = `Go ${directions[dir]} (${neighborId})`; // Show ID for debug
374
+ // Use addEventListener for cleaner separation
375
+ button.addEventListener('click', () => transitionToZone(neighborId));
376
+ choicesElement.appendChild(button);
377
+ addedChoices++;
378
+ }
379
+
380
+ // Add Zone Specific Options (Simplified)
381
  if (zoneInfo.options && zoneInfo.options.length > 0) {
382
+ console.log("Adding zone specific options");
383
+ zoneInfo.options.forEach(option => {
384
  const button = document.createElement('button');
385
  button.classList.add('choice-button');
386
  button.textContent = option.text;
387
+ // Add onclick based on option.action later (placeholder)
388
+ button.addEventListener('click', () => console.log("Zone action clicked (TBC):", option.action));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
389
  choicesElement.appendChild(button);
390
+ addedChoices++;
391
  });
392
  }
393
 
394
+ if (addedChoices === 0) {
395
+ choicesElement.innerHTML = '<p><i>No exits or actions available yet.</i></p>';
396
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
397
 
398
+ console.log("Updating stats, inventory, action info...");
399
  updateStatsDisplay();
400
  updateInventoryDisplay();
 
401
  updateActionInfo();
402
+ console.log("renderCurrentPageUI finished.");
403
+ }
404
+
405
+ // Wrapper because inline onclick can have scope issues with modules
406
+ function transitionToZoneWrapper(zoneId) {
407
+ transitionToZone(zoneId);
408
  }
409
+ window.transitionToZoneWrapper = transitionToZoneWrapper;
410
 
 
 
 
411
 
412
  function updateStatsDisplay() {
413
+ if (!gameState.character || !statsElement) return;
414
+ const { hp, maxHp, xp } = gameState.character.stats;
415
  const hpColor = hp / maxHp < 0.3 ? '#f88' : (hp / maxHp < 0.6 ? '#fd5' : '#8f8');
416
+ statsElement.innerHTML = `<strong>Stats:</strong> <span style="color:${hpColor}">HP: ${hp}/${maxHp}</span> <span>XP: ${xp}</span>`;
417
  }
418
 
419
  function updateInventoryDisplay() {
420
+ if (!gameState.character || !inventoryElement) return;
421
  let invHtml = '<strong>Inventory:</strong> ';
422
  if (gameState.character.inventory.length === 0) {
423
  invHtml += '<em>Empty</em>';
424
  } else {
425
+ gameState.character.inventory.forEach(item => {
426
+ const itemDef = itemsData[item] || { type: 'unknown', description: '???' };
427
+ const itemClass = `item-${itemDef.type || 'unknown'}`;
428
+ invHtml += `<span class="item-tag ${itemClass}" title="${itemDef.description}">${item}</span>`;
 
 
 
 
 
 
 
429
  });
430
  }
431
  inventoryElement.innerHTML = invHtml;
432
  }
433
 
 
 
 
 
 
 
 
 
 
 
 
434
  function updateActionInfo() {
435
+ if (!actionInfoElement || !gameState ) return;
436
+ actionInfoElement.textContent = `Zone: ${gameState.currentZoneId || 'None'} | Mode: Explore`;
 
 
 
 
437
  }
438
 
439
+ function pickupItem() { console.log("Pickup disabled."); }
440
+
441
+
442
  document.addEventListener('DOMContentLoaded', () => {
443
+ console.log("DOM Ready - Initializing World Grid Test.");
444
  try {
445
+ initThreeJS(); // Setup ThreeJS first
446
  if (!scene || !camera || !renderer) throw new Error("Three.js failed to initialize.");
447
+ startGame(); // Now start game logic
448
+ console.log("Game world initialization sequence complete.");
449
  } catch (error) {
450
  console.error("Initialization failed:", error);
451
  storyTitleElement.textContent = "Initialization Error";
452
+ storyContentElement.innerHTML = `<p style="color:red;">Failed to start game:</p><pre style="color:red; white-space: pre-wrap;">${error.stack || error}</pre><p style="color:yellow;">Check console (F12) for details.</p>`;
453
  if(sceneContainer) sceneContainer.innerHTML = '<p style="color:red; padding: 20px;">3D Scene Failed</p>';
454
+ // Hide game UI elements on error
455
+ const statsInvContainer = document.getElementById('stats-inventory-container');
456
+ const choicesCont = document.getElementById('choices-container');
457
+ if (statsInvContainer) statsInvContainer.style.display = 'none';
458
+ if (choicesCont) choicesCont.style.display = 'none';
459
  }
460
  });
461