awacke1 commited on
Commit
51cd77d
·
verified ·
1 Parent(s): 85d52ca

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +108 -377
index.html CHANGED
@@ -5,6 +5,7 @@
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>Choose Your Own Procedural Adventure</title>
7
  <style>
 
8
  body {
9
  font-family: 'Courier New', monospace;
10
  background-color: #222;
@@ -38,7 +39,7 @@
38
  padding: 20px;
39
  overflow-y: auto;
40
  background-color: #333;
41
- min-width: 280px;
42
  height: 100%;
43
  box-sizing: border-box;
44
  display: flex;
@@ -59,7 +60,7 @@
59
  #story-content {
60
  margin-bottom: 20px;
61
  line-height: 1.6;
62
- flex-grow: 1;
63
  }
64
  #story-content p { margin-bottom: 1em; }
65
  #story-content p:last-child { margin-bottom: 0; }
@@ -100,7 +101,7 @@
100
  #inventory-display .item-unknown { background-color: #555; border-color: #777;}
101
 
102
  #choices-container {
103
- margin-top: auto;
104
  padding-top: 15px;
105
  border-top: 1px solid #555;
106
  }
@@ -118,8 +119,15 @@
118
  .choice-button:hover:not(:disabled) { background-color: #d4a017; color: #222; border-color: #b8860b; }
119
  .choice-button:disabled { background-color: #444; color: #888; cursor: not-allowed; border-color: #666; opacity: 0.7; }
120
 
121
- .sell-button { background-color: #4a4a4a; border-color: #6a6a6a; }
122
- .sell-button:hover:not(:disabled) { background-color: #a07017; border-color: #80500b; }
 
 
 
 
 
 
 
123
 
124
  .roll-success { color: #7f7; border-left: 3px solid #4a4; padding-left: 8px; margin-bottom: 1em; font-size: 0.9em; }
125
  .roll-failure { color: #f77; border-left: 3px solid #a44; padding-left: 8px; margin-bottom: 1em; font-size: 0.9em; }
@@ -131,7 +139,7 @@
131
  <div id="ui-container">
132
  <h2 id="story-title">Loading Adventure...</h2>
133
  <div id="story-content">
134
- <p>Please wait while the adventure loads.</p>
135
  </div>
136
  <div id="stats-inventory-container">
137
  <div id="stats-display"></div>
@@ -156,19 +164,21 @@
156
  <script type="module">
157
  import * as THREE from 'three';
158
 
159
- // DOM Element References (Checked - OK)
160
  const sceneContainer = document.getElementById('scene-container');
161
  const storyTitleElement = document.getElementById('story-title');
162
  const storyContentElement = document.getElementById('story-content');
163
  const choicesElement = document.getElementById('choices');
164
  const statsElement = document.getElementById('stats-display');
165
  const inventoryElement = document.getElementById('inventory-display');
 
166
 
167
- // Global Three.js Variables (Checked - OK)
168
  let scene, camera, renderer;
169
  let currentAssemblyGroup = null;
 
170
 
171
- // Materials (Checked - OK)
172
  const stoneMaterial = new THREE.MeshStandardMaterial({ color: 0x888888, roughness: 0.8, metalness: 0.1 });
173
  const woodMaterial = new THREE.MeshStandardMaterial({ color: 0x8B4513, roughness: 0.7, metalness: 0 });
174
  const darkWoodMaterial = new THREE.MeshStandardMaterial({ color: 0x5C3D20, roughness: 0.7, metalness: 0 });
@@ -185,94 +195,16 @@
185
  const wetStoneMaterial = new THREE.MeshStandardMaterial({ color: 0x2F4F4F, roughness: 0.7 });
186
  const glowMaterial = new THREE.MeshStandardMaterial({ color: 0x00FFAA, emissive: 0x00FFAA, emissiveIntensity: 0.5 });
187
 
188
- // --- Three.js Setup --- (Checked - OK)
189
- function initThreeJS() {
190
- if (!sceneContainer) { console.error("Scene container not found!"); return false; } // Return boolean on failure
191
- try {
192
- scene = new THREE.Scene();
193
- scene.background = new THREE.Color(0x222222);
194
- const width = sceneContainer.clientWidth;
195
- const height = sceneContainer.clientHeight;
196
- if (!width || !height) {
197
- console.warn("Scene container has zero dimensions initially.");
198
- // Use fallback or wait? For now, proceed but log.
199
- }
200
- camera = new THREE.PerspectiveCamera(75, (width / height) || 1, 0.1, 1000);
201
- camera.position.set(0, 2.5, 7);
202
- camera.lookAt(0, 0.5, 0);
203
- renderer = new THREE.WebGLRenderer({ antialias: true });
204
- renderer.setSize(width || 400, height || 300); // Use fallback dimensions
205
- renderer.shadowMap.enabled = true;
206
- renderer.shadowMap.type = THREE.PCFSoftShadowMap;
207
- sceneContainer.innerHTML = ''; // Clear previous content/errors
208
- sceneContainer.appendChild(renderer.domElement);
209
- const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
210
- scene.add(ambientLight);
211
- window.addEventListener('resize', onWindowResize, false);
212
- setTimeout(onWindowResize, 100);
213
- animate();
214
- return true; // Indicate success
215
- } catch (error) {
216
- console.error("Error during Three.js initialization:", error);
217
- return false; // Indicate failure
218
- }
219
- }
220
-
221
- function onWindowResize() {
222
- if (!renderer || !camera || !sceneContainer) return;
223
- const width = sceneContainer.clientWidth;
224
- const height = sceneContainer.clientHeight;
225
- if (width > 0 && height > 0) {
226
- camera.aspect = width / height;
227
- camera.updateProjectionMatrix();
228
- renderer.setSize(width, height);
229
- } else {
230
- console.warn("onWindowResize called with zero dimensions.");
231
- }
232
- }
233
-
234
- function animate() {
235
- // Guard against errors if renderer/scene is not valid
236
- if (!renderer || !scene || !camera) {
237
- // console.warn("Animation loop called before Three.js fully initialized or after error.");
238
- return;
239
- }
240
- requestAnimationFrame(animate); // Request next frame first
241
- try {
242
- const time = performance.now() * 0.001;
243
- scene.traverse(obj => {
244
- if (obj.userData && obj.userData.update && typeof obj.userData.update === 'function') {
245
- obj.userData.update(time);
246
- }
247
- });
248
- renderer.render(scene, camera);
249
- } catch (error) {
250
- console.error("Error during animation/render loop:", error);
251
- // Optionally stop the loop or display an error overlay
252
- // For now, just log it to avoid crashing the browser if possible
253
- }
254
- }
255
-
256
- function createMesh(geometry, material, position = { x: 0, y: 0, z: 0 }, rotation = { x: 0, y: 0, z: 0 }, scale = { x: 1, y: 1, z: 1 }) {
257
- const mesh = new THREE.Mesh(geometry, material);
258
- mesh.position.set(position.x, position.y, position.z);
259
- mesh.rotation.set(rotation.x, rotation.y, rotation.z);
260
- mesh.scale.set(scale.x, scale.y, scale.z);
261
- mesh.castShadow = true; mesh.receiveShadow = true;
262
- return mesh;
263
- }
264
 
265
- function createGroundPlane(material = groundMaterial, size = 20) {
266
- const groundGeo = new THREE.PlaneGeometry(size, size);
267
- const ground = new THREE.Mesh(groundGeo, material);
268
- ground.rotation.x = -Math.PI / 2; ground.position.y = -0.05;
269
- ground.receiveShadow = true; ground.castShadow = false;
270
- return ground;
271
- }
272
 
273
- // --- Procedural Generation Functions --- (Checked - OK)
274
- // [Keep all create...Assembly functions here - unchanged from previous]
275
- // ... (createDefaultAssembly, createCityGatesAssembly, ..., createDarkCaveAssembly) ...
276
  function createDefaultAssembly() { const group = new THREE.Group(); const sphereGeo = new THREE.SphereGeometry(0.5, 16, 16); group.add(createMesh(sphereGeo, stoneMaterial, { x: 0, y: 0.5, z: 0 })); group.add(createGroundPlane()); return group; }
277
  function createCityGatesAssembly() { const group = new THREE.Group(); const gh = 4, gw = 1.5, gd = 0.8, ah = 1, aw = 3; const tlGeo = new THREE.BoxGeometry(gw, gh, gd); group.add(createMesh(tlGeo, stoneMaterial, { x: -(aw / 2 + gw / 2), y: gh / 2, z: 0 })); const trGeo = new THREE.BoxGeometry(gw, gh, gd); group.add(createMesh(trGeo, stoneMaterial, { x: (aw / 2 + gw / 2), y: gh / 2, z: 0 })); const aGeo = new THREE.BoxGeometry(aw, ah, gd); group.add(createMesh(aGeo, stoneMaterial, { x: 0, y: gh - ah / 2, z: 0 })); const cs = 0.4; const cg = new THREE.BoxGeometry(cs, cs, gd * 1.1); for (let i = -1; i <= 1; i += 2) { group.add(createMesh(cg.clone(), stoneMaterial, { x: -(aw / 2 + gw / 2) + i * cs * 0.7, y: gh + cs / 2, z: 0 })); group.add(createMesh(cg.clone(), stoneMaterial, { x: (aw / 2 + gw / 2) + i * cs * 0.7, y: gh + cs / 2, z: 0 })); } group.add(createMesh(cg.clone(), stoneMaterial, { x: 0, y: gh + ah - cs / 2, z: 0 })); group.add(createGroundPlane(stoneMaterial)); return group; }
278
  function createWeaponsmithAssembly() { const group = new THREE.Group(); const bw = 3, bh = 2.5, bd = 3.5; const bGeo = new THREE.BoxGeometry(bw, bh, bd); group.add(createMesh(bGeo, darkWoodMaterial, { x: 0, y: bh / 2, z: 0 })); const ch = 3.5; const cGeo = new THREE.CylinderGeometry(0.3, 0.4, ch, 8); group.add(createMesh(cGeo, stoneMaterial, { x: bw * 0.3, y: ch / 2, z: -bd * 0.3 })); group.add(createGroundPlane(dirtMaterial)); return group; }
@@ -295,25 +227,23 @@
295
  function createDarkCaveAssembly() { const group = new THREE.Group(); const caveRadius = 5; const caveHeight = 4; group.add(createGroundPlane(wetStoneMaterial, caveRadius * 2)); const wallGeo = new THREE.SphereGeometry(caveRadius, 32, 16, 0, Math.PI * 2, 0, Math.PI / 1.5); const wallMat = wetStoneMaterial.clone(); wallMat.side = THREE.BackSide; const wall = new THREE.Mesh(wallGeo, wallMat); wall.position.y = caveHeight * 0.6; group.add(wall); const stalactiteGeo = new THREE.ConeGeometry(0.1, 0.8, 8); const stalagmiteGeo = new THREE.ConeGeometry(0.15, 0.5, 8); for (let i = 0; i < 15; i++) { const x = (Math.random() - 0.5) * caveRadius * 1.5; const z = (Math.random() - 0.5) * caveRadius * 1.5; if (Math.random() > 0.5) { group.add(createMesh(stalactiteGeo, wetStoneMaterial, { x: x, y: caveHeight - 0.4, z: z })) } else { group.add(createMesh(stalagmiteGeo, wetStoneMaterial, { x: x, y: 0.25, z: z })) } } const dripGeo = new THREE.SphereGeometry(0.05, 8, 8); for (let i = 0; i < 5; i++) { const drip = createMesh(dripGeo, oceanMaterial, { x: (Math.random() - 0.5) * caveRadius, y: caveHeight - 0.2, z: (Math.random() - 0.5) * caveRadius }); drip.userData.startY = caveHeight - 0.2; drip.userData.update = (time) => { drip.position.y -= 0.1; if (drip.position.y < 0) { drip.position.y = drip.userData.startY; drip.position.x = (Math.random() - 0.5) * caveRadius; drip.position.z = (Math.random() - 0.5) * caveRadius; } }; group.add(drip); } return group; }
296
 
297
  // ========================================
298
- // Game Data (Checked - OK, added goldValue)
299
  // ========================================
300
- const itemsData = {
301
  "Flaming Sword": {type:"weapon", description:"A legendary blade, wreathed in magical fire.", goldValue: 500},
302
  "Whispering Bow": {type:"weapon", description:"Crafted by elves, its arrows fly almost silently.", goldValue: 350},
303
  "Guardian Shield": {type:"armor", description:"A sturdy shield imbued with protective enchantments.", goldValue: 250},
304
  "Healing Light Spell":{type:"spell", description:"A scroll containing the incantation to mend minor wounds.", goldValue: 50},
305
  "Shield of Faith Spell":{type:"spell",description:"A scroll containing a prayer that grants temporary magical protection.", goldValue: 75},
306
  "Binding Runes Scroll":{type:"spell", description:"Complex runes scribbled on parchment, said to temporarily immobilize a foe.", goldValue: 100},
307
- "Secret Tunnel Map": {type:"quest", description:"A crudely drawn map showing a hidden path, perhaps into the fortress?", goldValue: 0}, // Quest items non-sellable
308
  "Poison Daggers": {type:"weapon", description:"A pair of wicked-looking daggers coated in a fast-acting toxin.", goldValue: 150},
309
  "Master Key": {type:"quest", description:"An ornate key rumored to unlock many doors, though perhaps not all.", goldValue: 0},
310
  "Crude Dagger": {type:"weapon", description:"A roughly made dagger, chipped and stained.", goldValue: 10},
311
- "Scout's Pouch": {type:"quest", description:"A small leather pouch containing flint & steel, jerky, and some odd coins.", goldValue: 20} // Pouch itself has value
312
  };
313
 
314
- const gameData = { // (Checked - OK, added allowSell flag to 99)
315
- // [Keep all page data here - unchanged from previous]
316
- // ... (pages 1 to 224) ...
317
  "1": { title: "The Crossroads", content: `<p>Dust swirls around a weathered signpost under a bright, midday sun. Paths lead north into the gloomy Shadowwood, east towards rolling green hills, and west towards coastal cliffs battered by sea spray. Which path calls to you?</p>`, options: [ { text: "Enter the Shadowwood Forest (North)", next: 5 }, { text: "Head towards the Rolling Hills (East)", next: 2 }, { text: "Investigate the Coastal Cliffs (West)", next: 3 } ], illustration: "crossroads-signpost-sunny" },
318
  "2": { title: "Rolling Hills", content: `<p>Verdant hills stretch before you, dotted with wildflowers. A gentle breeze whispers through the tall grass. In the distance, you see a lone figure tending to a flock of sheep. It feels peaceful, almost unnervingly so after the crossroads.</p>`, options: [ { text: "Follow the narrow path winding through the hills", next: 4 }, { text: "Try to hail the distant shepherd (Charisma Check?)", next: 99 } ], illustration: "rolling-green-hills-shepherd-distance" },
319
  "3": { title: "Coastal Cliffs Edge", content: `<p>You stand atop windswept cliffs, the roar of crashing waves filling the air below. Seabirds circle overhead. A precarious-looking path, seemingly carved by desperate hands, descends the cliff face towards a hidden cove.</p>`, options: [ { text: "Attempt the precarious descent (Dexterity Check)", check: { stat: 'dexterity', dc: 12, onFailure: 31 }, next: 30 }, { text: "Scan the cliff face for easier routes (Wisdom Check)", check: { stat: 'wisdom', dc: 11, onFailure: 32 }, next: 33 } ], illustration: "windy-sea-cliffs-crashing-waves-path-down" },
@@ -328,7 +258,6 @@
328
  "13": { title: "Daring Escape", content: `<p>With surprising agility, you feint left, then dive right, tumbling past the goblins' clumsy spear thrusts! You scramble to your feet and sprint down the path, leaving the surprised goblins behind.</p><p>(+25 XP)</p>`, options: [ { text: "Keep running!", next: 14 } ], illustration: "blurred-motion-running-past-goblins-forest", reward: { xp: 25 } },
329
  "14": { title: "Forest Stream Crossing", content: `<p>The overgrown path eventually leads to the bank of a clear, shallow stream. Smooth, mossy stones line the streambed, and dappled sunlight filters through the leaves overhead, sparkling on the water's surface.</p>`, options: [ { text: "Wade across the stream", next: 16 }, { text: "Look for a drier crossing point (fallen log?) upstream", next: 15 } ], illustration: "forest-stream-crossing-dappled-sunlight-stones" },
330
  "15": { title: "Log Bridge", content: `<p>A short walk upstream reveals a large, fallen tree spanning the stream. It's covered in slick, green moss, making it look like a potentially treacherous crossing.</p>`, options: [ { text: "Cross carefully on the mossy log (Dexterity Check)", check: { stat: 'dexterity', dc: 9, onFailure: 151 }, next: 16 }, { text: "Decide it's too risky and go back to wade across", next: 14 } ], illustration: "mossy-log-bridge-over-forest-stream" },
331
- "151": { title: "Splash!", content: `<p>You place a foot carefully on the log, but the moss is slicker than it looks! Your feet shoot out from under you, and you tumble into the cold stream with a loud splash! You're soaked and slightly embarrassed, but otherwise unharmed.</p>`, options: [ { text: "Shake yourself off and continue on the other side", next: 16 } ], illustration: "character-splashing-into-stream-from-log" },
332
  "16": { title: "Edge of the Woods", content: `<p>Finally, the trees begin to thin, and you emerge from the oppressive gloom of the Shadowwood. Before you lie steep, rocky foothills leading up towards a formidable-looking mountain fortress perched high above.</p>`, options: [ { text: "Begin the ascent into the foothills towards the fortress", next: 17 }, { text: "Scan the fortress and surrounding terrain from afar (Wisdom Check)", check: { stat: 'wisdom', dc: 14, onFailure: 17 }, next: 18 } ], illustration: "forest-edge-view-rocky-foothills-distant-mountain-fortress" },
333
  "17": { title: "Rocky Foothills Path", content: `<p>The climb is arduous, the path winding steeply upwards over loose scree and jagged rocks. The air thins slightly. The dark stone walls of the mountain fortress loom much larger now, seeming to watch your approach.</p>`, options: [ { text: "Continue the direct ascent", next: 19 }, { text: "Look for signs of a hidden trail or less obvious route (Wisdom Check)", check: { stat: 'wisdom', dc: 15, onFailure: 19 }, next: 20 } ], illustration: "climbing-rocky-foothills-path-fortress-closer" },
334
  "18": { title: "Distant Observation", content: `<p>Taking a moment to study the fortress from this distance, your keen eyes notice something interesting. The main approach looks heavily guarded, but along the western ridge, the terrain seems slightly less sheer, potentially offering a less-guarded, albeit more treacherous, approach.</p><p>(+30 XP)</p>`, options: [ { text: "Decide against the risk and take the main path into the foothills", next: 17 }, { text: "Attempt the western ridge approach", next: 21 } ], illustration: "zoomed-view-mountain-fortress-western-ridge", reward: { xp: 30 } },
@@ -364,19 +293,19 @@
364
  "401": { title: "Mysterious Carvings", content:"<p>The carvings are too worn and abstract to decipher their specific meaning, though you sense they are very old.</p>", options: [{text:"Continue towards the badlands", next: 41}], illustration: "overgrown-stone-shrine-wildflowers-close"},
365
  "402": { title: "Moment of Peace", content:"<p>You spend a quiet moment in reflection. While no divine voice answers, the tranquility of the place settles your nerves.</p>", options: [{text:"Continue towards the badlands", next: 41}], illustration: "overgrown-stone-shrine-wildflowers-close"},
366
 
367
- // Game Over Page (allowSell enabled)
368
  "99": {
369
  title: "Game Over / To Be Continued...",
370
  content: "<p>Your adventure ends here... for now. You can sell unwanted items for gold before starting again.</p>",
371
  options: [ /* Restart button added dynamically */ ],
372
  illustration: "game-over-generic",
373
  gameOver: true,
374
- allowSell: true // Enable selling feature on this page
375
  }
376
  };
377
 
 
378
  // ========================================
379
- // Game State (Checked - OK, uses default)
380
  // ========================================
381
  const defaultCharacterState = {
382
  name: "Hero", race: "Human", alignment: "Neutral Good", class: "Adventurer",
@@ -386,317 +315,119 @@
386
  };
387
  let gameState = {
388
  currentPageId: 1,
389
- character: JSON.parse(JSON.stringify(defaultCharacterState)) // Start with default
390
  };
391
 
 
392
  // ========================================
393
- // Game Logic Functions (Checked - OK)
394
  // ========================================
395
 
 
 
 
 
 
 
 
 
 
 
 
396
  function startNewGame() {
397
  console.log("Starting brand new game...");
 
398
  gameState = {
399
  currentPageId: 1,
400
- character: JSON.parse(JSON.stringify(defaultCharacterState)) // Full reset
401
  };
402
- renderPage(gameState.currentPageId); // Render page 1
403
  }
404
 
405
  function restartGamePlus() {
406
  console.log("Restarting game (keeping progress)...");
407
- // Only reset current location, keep character object
408
  gameState.currentPageId = 1;
409
- renderPage(gameState.currentPageId); // Render page 1
410
  }
411
 
412
- function handleChoiceClick(choiceData) {
413
  console.log("Choice clicked:", choiceData);
414
- let feedbackMessage = ""; // Message passed to render function
415
-
416
- // --- Special Actions ---
417
- if (choiceData.action === 'restart_plus') {
418
- restartGamePlus();
419
- return;
420
- }
421
- if (choiceData.action === 'sell_item') {
422
- // handleSellItem now returns the feedback message
423
- feedbackMessage = handleSellItem(choiceData.item);
424
- // Re-render the current page (99) with the feedback
425
- renderPageInternal(gameState.currentPageId, gameData[gameState.currentPageId], feedbackMessage);
426
- return;
427
- }
428
-
429
- // --- Standard Page Navigation ---
430
- const optionNextPageId = parseInt(choiceData.next);
431
- const itemToAdd = choiceData.addItem;
432
- let nextPageId = optionNextPageId;
433
- const check = choiceData.check;
434
-
435
- if (isNaN(optionNextPageId) && !check) {
436
- console.error("Invalid choice data: Missing 'next' page ID and no check defined.", choiceData);
437
- feedbackMessage = `<p class="feedback-error">Error: Invalid choice data! Cannot proceed.</p>`;
438
- renderPageInternal(gameState.currentPageId, gameData[gameState.currentPageId], feedbackMessage);
439
- choicesElement.querySelectorAll('button').forEach(b => b.disabled = true);
440
- return;
441
- }
442
-
443
- // --- Skill Check ---
444
- if (check) {
445
- const statValue = gameState.character.stats[check.stat] || 10;
446
- const modifier = Math.floor((statValue - 10) / 2);
447
- const roll = Math.floor(Math.random() * 20) + 1;
448
- const totalResult = roll + modifier;
449
- const dc = check.dc;
450
- const statName = check.stat.charAt(0).toUpperCase() + check.stat.slice(1);
451
- console.log(`Check: ${statName} (DC ${dc}) | Roll: ${roll} + Mod: ${modifier} = ${totalResult}`);
452
- if (totalResult >= dc) {
453
- nextPageId = optionNextPageId;
454
- feedbackMessage += `<p class="roll-success"><em>${statName} Check Success! (${totalResult} vs DC ${dc})</em></p>`;
455
- } else {
456
- nextPageId = parseInt(check.onFailure);
457
- feedbackMessage += `<p class="roll-failure"><em>${statName} Check Failed! (${totalResult} vs DC ${dc})</em></p>`;
458
- if (isNaN(nextPageId)) {
459
- console.error("Invalid onFailure ID:", check.onFailure);
460
- nextPageId = 99;
461
- feedbackMessage += `<p class="feedback-error">Error: Invalid failure path defined!</p>`;
462
- }
463
- }
464
- }
465
-
466
- // --- Page Transition & Consequences ---
467
  const targetPageData = gameData[nextPageId];
468
- if (!targetPageData) {
469
- console.error(`Data for target page ${nextPageId} not found!`);
470
- feedbackMessage = `<p class="feedback-error">Error: Next page data missing! Cannot continue.</p>`;
471
- renderPageInternal(99, gameData[99], feedbackMessage); // Go to game over
472
- return;
473
- }
474
-
475
- let hpLostThisTurn = 0;
476
- if (targetPageData.hpLoss) {
477
- hpLostThisTurn = targetPageData.hpLoss;
478
- gameState.character.stats.hp -= hpLostThisTurn;
479
- console.log(`Lost ${hpLostThisTurn} HP.`);
480
- }
481
- if (targetPageData.reward && targetPageData.reward.hpGain) {
482
- const hpGained = targetPageData.reward.hpGain;
483
- gameState.character.stats.hp += hpGained;
484
- console.log(`Gained ${hpGained} HP.`);
485
- }
486
-
487
- if (gameState.character.stats.hp <= 0) {
488
- gameState.character.stats.hp = 0;
489
- console.log("Player died!");
490
- nextPageId = 99;
491
- feedbackMessage += `<p class="feedback-error"><em>You have succumbed to your injuries!${hpLostThisTurn > 0 ? ` (-${hpLostThisTurn} HP)` : ''}</em></p>`;
492
- renderPageInternal(nextPageId, gameData[nextPageId], feedbackMessage); // Render game over immediately
493
- return;
494
- }
495
-
496
- if (targetPageData.reward) {
497
- if (targetPageData.reward.xp) {
498
- gameState.character.xp += targetPageData.reward.xp;
499
- console.log(`Gained ${targetPageData.reward.xp} XP! Total: ${gameState.character.xp}`);
500
- // checkLevelUp();
501
- }
502
- if (targetPageData.reward.statIncrease) {
503
- const stat = targetPageData.reward.statIncrease.stat;
504
- const amount = targetPageData.reward.statIncrease.amount;
505
- if (gameState.character.stats.hasOwnProperty(stat)) {
506
- gameState.character.stats[stat] += amount;
507
- console.log(`Stat ${stat} increased by ${amount}. New value: ${gameState.character.stats[stat]}`);
508
- if (stat === 'constitution') recalculateMaxHp();
509
- }
510
- }
511
- if (targetPageData.reward.addItem && !gameState.character.inventory.includes(targetPageData.reward.addItem)) {
512
- const itemName = targetPageData.reward.addItem;
513
- // Check if item exists in itemsData before adding
514
- if (itemsData[itemName]) {
515
- gameState.character.inventory.push(itemName);
516
- console.log(`Found item: ${itemName}`);
517
- feedbackMessage += `<p class="feedback-message"><em>Item acquired: ${itemName}</em></p>`;
518
- } else {
519
- console.warn(`Attempted to add unknown item from reward: ${itemName}`);
520
- feedbackMessage += `<p class="feedback-error"><em>Error: Tried to acquire unknown item '${itemName}'!</em></p>`;
521
- }
522
- }
523
- }
524
- if (itemToAdd && !gameState.character.inventory.includes(itemToAdd)) {
525
- if (itemsData[itemToAdd]) { // Check item exists
526
- gameState.character.inventory.push(itemToAdd);
527
- console.log("Added item:", itemToAdd);
528
- feedbackMessage += `<p class="feedback-message"><em>Item acquired: ${itemToAdd}</em></p>`;
529
- } else {
530
- console.warn(`Attempted to add unknown item from choice: ${itemToAdd}`);
531
- feedbackMessage += `<p class="feedback-error"><em>Error: Tried to acquire unknown item '${itemToAdd}'!</em></p>`;
532
- }
533
- }
534
-
535
- gameState.currentPageId = nextPageId;
536
- recalculateMaxHp();
537
- gameState.character.stats.hp = Math.min(gameState.character.stats.hp, gameState.character.stats.maxHp);
538
-
539
- console.log("Transitioning to page:", nextPageId, " New state:", JSON.stringify(gameState));
540
- renderPageInternal(nextPageId, gameData[nextPageId], feedbackMessage);
541
  }
542
 
543
- // Returns feedback message string
544
- function handleSellItem(itemName) {
545
- console.log("Attempting to sell:", itemName);
546
- const itemIndex = gameState.character.inventory.indexOf(itemName);
547
- const itemInfo = itemsData[itemName];
548
- let message = "";
549
-
550
- // Add extra checks
551
- if (itemIndex === -1) {
552
- console.warn(`Sell failed: Item "${itemName}" not in inventory.`);
553
- message = `<p class="feedback-error">Cannot sell ${itemName} - you don't have it!</p>`;
554
- } else if (!itemInfo) {
555
- console.warn(`Sell failed: Item data for "${itemName}" not found.`);
556
- message = `<p class="feedback-error">Cannot sell ${itemName} - item data missing!</p>`;
557
- } else if (itemInfo.type === 'quest') {
558
- console.log(`Sell blocked: Item "${itemName}" is a quest item.`);
559
- message = `<p class="feedback-message">Cannot sell ${itemName} - it seems important.</p>`;
560
- } else if (itemInfo.goldValue === undefined || itemInfo.goldValue <= 0) {
561
- console.log(`Sell blocked: Item "${itemName}" has no gold value.`);
562
- message = `<p class="feedback-message">${itemName} isn't worth any gold.</p>`;
563
- } else {
564
- // Proceed with selling
565
- const value = itemInfo.goldValue;
566
- gameState.character.gold += value;
567
- gameState.character.inventory.splice(itemIndex, 1);
568
- message = `<p class="feedback-success">Sold ${itemName} for ${value} Gold.</p>`;
569
- console.log(`Sold ${itemName} for ${value} gold. Current gold: ${gameState.character.gold}`);
570
- }
571
- return message; // Return the message to be displayed
572
  }
573
 
574
-
575
- function recalculateMaxHp() { // Checked - OK
576
- const baseHp = 10;
577
- const conModifier = Math.floor((gameState.character.stats.constitution - 10) / 2);
578
- gameState.character.stats.maxHp = Math.max(1, baseHp + conModifier * gameState.character.level);
579
  }
580
 
581
- function renderPageInternal(pageId, pageData, message = "") { // Checked - OK (added sell buttons)
582
- if (!pageData) {
583
- console.error(`Render Error: No data for page ${pageId}`);
584
- pageData = gameData["99"] || { title: "Error", content: "<p>Render Error! Critical page data missing.</p>", illustration: "error", gameOver: true };
585
- message += `<p class="feedback-error">Render Error: Page data for ID ${pageId} was missing!</p>`;
586
- pageId = 99;
587
- }
588
- console.log(`Rendering page ${pageId}: "${pageData.title}"`);
589
-
590
- storyTitleElement.textContent = pageData.title || "Untitled Page";
591
- storyContentElement.innerHTML = message + (pageData.content || "<p>...</p>");
592
-
593
- updateStatsDisplay();
594
- updateInventoryDisplay();
595
- choicesElement.innerHTML = '';
596
-
597
- const options = pageData.options || [];
598
- const isGameOverPage = pageData.gameOver === true;
599
-
600
- // Generate Sell Buttons if applicable
601
- if (isGameOverPage && pageData.allowSell === true) {
602
- const sellableItems = gameState.character.inventory.filter(itemName => {
603
- const itemInfo = itemsData[itemName];
604
- // Check itemInfo exists before accessing properties
605
- return itemInfo && itemInfo.type !== 'quest' && itemInfo.goldValue !== undefined && itemInfo.goldValue > 0;
606
- });
607
-
608
- if (sellableItems.length > 0) {
609
- choicesElement.innerHTML += `<h3 style="margin-bottom: 5px;">Sell Items:</h3>`;
610
- sellableItems.forEach(itemName => {
611
- // Double check itemInfo exists here too before creating button
612
- const itemInfo = itemsData[itemName];
613
- if (!itemInfo) return; // Skip if item data somehow missing
614
-
615
- const sellButton = document.createElement('button');
616
- sellButton.classList.add('choice-button', 'sell-button');
617
- sellButton.textContent = `Sell ${itemName} (${itemInfo.goldValue} Gold)`;
618
- sellButton.onclick = () => handleChoiceClick({ action: 'sell_item', item: itemName });
619
- choicesElement.appendChild(sellButton);
620
- });
621
- choicesElement.innerHTML += `<hr style="border-color: #555; margin: 15px 0 10px 0;">`; // Separator
622
- }
623
- }
624
-
625
- // Generate Standard Choices / Restart Button
626
- if (!isGameOverPage && options.length > 0) {
627
- options.forEach(option => {
628
- const button = document.createElement('button');
629
- button.classList.add('choice-button');
630
- button.textContent = option.text;
631
- let requirementMet = true;
632
- let requirementText = [];
633
- if (option.requireItem) { if (!gameState.character.inventory.includes(option.requireItem)) { requirementMet = false; requirementText.push(`Requires: ${option.requireItem}`); } }
634
- if (option.requireStat) { const currentStat = gameState.character.stats[option.requireStat.stat] || 0; if (currentStat < option.requireStat.value) { requirementMet = false; requirementText.push(`Requires: ${option.requireStat.stat.charAt(0).toUpperCase() + option.requireStat.stat.slice(1)} ${option.requireStat.value}`); } }
635
- button.disabled = !requirementMet;
636
- if (!requirementMet) button.title = requirementText.join(', ');
637
- else { const choiceData = { next: option.next, addItem: option.addItem, check: option.check }; button.onclick = () => handleChoiceClick(choiceData); }
638
- choicesElement.appendChild(button);
639
- });
640
- } else if (isGameOverPage) {
641
- const restartButton = document.createElement('button');
642
- restartButton.classList.add('choice-button');
643
- restartButton.textContent = "Restart Adventure (Keep Progress)";
644
- restartButton.onclick = () => handleChoiceClick({ action: 'restart_plus' });
645
- choicesElement.appendChild(restartButton);
646
- } else if (pageId !== 99) { // End of branch, not explicit game over page
647
- choicesElement.insertAdjacentHTML('beforeend', '<p><i>There are no further paths from here.</i></p>');
648
- const restartButton = document.createElement('button');
649
- restartButton.classList.add('choice-button');
650
- restartButton.textContent = "Restart Adventure (Keep Progress)";
651
- restartButton.onclick = () => handleChoiceClick({ action: 'restart_plus' });
652
- choicesElement.appendChild(restartButton);
653
- }
654
-
655
  updateScene(pageData.illustration || 'default');
656
  }
657
 
658
- function renderPage(pageId) { renderPageInternal(pageId, gameData[pageId]); }
659
-
660
- function updateStatsDisplay() { // Checked - OK (added gold)
661
- const char=gameState.character;
662
- statsElement.innerHTML = `<strong>Stats:</strong> <span class="stat-gold">Gold: ${char.gold}</span> <span>Lvl: ${char.level}</span> <span>XP: ${char.xp}/${char.xpToNextLevel}</span> <span>HP: ${char.stats.hp}/${char.stats.maxHp}</span> <span>Str: ${char.stats.strength}</span> <span>Int: ${char.stats.intelligence}</span> <span>Wis: ${char.stats.wisdom}</span> <span>Dex: ${char.stats.dexterity}</span> <span>Con: ${char.stats.constitution}</span> <span>Cha: ${char.stats.charisma}</span>`;
663
- }
664
 
665
- function updateInventoryDisplay() { // Checked - OK
666
- let h='<strong>Inventory:</strong> ';
667
- if(gameState.character.inventory.length === 0){
668
- h+='<em>Empty</em>';
669
- } else {
670
- gameState.character.inventory.forEach(itemName=>{
671
- const item = itemsData[itemName] || {type:'unknown',description:'An unknown item.'};
672
- const itemClass = `item-${item.type || 'unknown'}`;
673
- const descriptionText = typeof item.description === 'string' ? item.description : 'No description available.';
674
- h += `<span class="${itemClass}" title="${descriptionText.replace(/"/g, '&quot;')}">${itemName}</span>`;
675
- });
676
- }
677
- inventoryElement.innerHTML = h;
678
- }
679
-
680
- // --- Scene Update and Lighting --- (Checked - OK)
681
- // [Keep updateScene and adjustLighting functions here - unchanged from previous]
682
  function updateScene(illustrationKey) { if (!scene) { console.warn("Scene not initialized, cannot update visual."); return; } console.log("Updating scene for illustration key:", illustrationKey); if (currentAssemblyGroup) { scene.remove(currentAssemblyGroup); currentAssemblyGroup.traverse(child => { if (child.isMesh) { child.geometry.dispose(); } }); currentAssemblyGroup = null; } scene.fog = null; scene.background = new THREE.Color(0x222222); camera.position.set(0, 2.5, 7); camera.lookAt(0, 0.5, 0); let assemblyFunction; switch (illustrationKey) { case 'city-gates': assemblyFunction = createCityGatesAssembly; break; case 'weaponsmith': assemblyFunction = createWeaponsmithAssembly; break; case 'temple': assemblyFunction = createTempleAssembly; break; case 'resistance-meeting': assemblyFunction = createResistanceMeetingAssembly; break; case 'prisoner-cell': assemblyFunction = createPrisonerCellAssembly; break; case 'game-over': case 'game-over-generic': assemblyFunction = createGameOverAssembly; break; case 'error': assemblyFunction = createErrorAssembly; break; case 'crossroads-signpost-sunny': scene.fog = new THREE.Fog(0x87CEEB, 10, 35); camera.position.set(0, 3, 10); camera.lookAt(0, 1, 0); scene.background = new THREE.Color(0x87CEEB); assemblyFunction = createCrossroadsAssembly; break; case 'rolling-green-hills-shepherd-distance': case 'hilltop-view-overgrown-shrine-wildflowers': case 'overgrown-stone-shrine-wildflowers-close': scene.fog = new THREE.Fog(0xA8E4A0, 15, 50); camera.position.set(0, 5, 15); camera.lookAt(0, 2, -5); scene.background = new THREE.Color(0x90EE90); if (illustrationKey === 'overgrown-stone-shrine-wildflowers-close') camera.position.set(1, 2, 4); if (illustrationKey === 'hilltop-view-overgrown-shrine-wildflowers') camera.position.set(3, 4, 8); assemblyFunction = createRollingHillsAssembly; break; case 'windy-sea-cliffs-crashing-waves-path-down': case 'scanning-sea-cliffs-no-other-paths-visible': case 'close-up-handholds-carved-in-cliff-face': scene.fog = new THREE.Fog(0x6699CC, 10, 40); camera.position.set(5, 5, 10); camera.lookAt(-2, 0, -5); scene.background = new THREE.Color(0x6699CC); assemblyFunction = createCoastalCliffsAssembly; break; case 'hidden-cove-beach-dark-cave-entrance': case 'character-fallen-at-bottom-of-cliff-path-cove': scene.fog = new THREE.Fog(0x336699, 5, 30); camera.position.set(0, 2, 8); camera.lookAt(0, 1, -2); scene.background = new THREE.Color(0x336699); assemblyFunction = createHiddenCoveAssembly; break; case 'rocky-badlands-cracked-earth-harsh-sun': scene.fog = new THREE.Fog(0xD2B48C, 15, 40); camera.position.set(0, 3, 12); camera.lookAt(0, 1, 0); scene.background = new THREE.Color(0xCD853F); assemblyFunction = createDefaultAssembly; break; case 'shadowwood-forest': scene.fog = new THREE.Fog(0x2E2E2E, 5, 20); camera.position.set(0, 2, 8); camera.lookAt(0, 1, 0); scene.background = new THREE.Color(0x1A1A1A); assemblyFunction = createForestAssembly; break; case 'dark-forest-entrance-gnarled-roots-filtered-light': scene.fog = new THREE.Fog(0x2E2E2E, 5, 20); camera.position.set(0, 2, 8); camera.lookAt(0, 1, 0); scene.background = new THREE.Color(0x1A1A1A); assemblyFunction = createForestEntranceAssembly; break; case 'overgrown-forest-path-glowing-fungi-vines': case 'pushing-through-forest-undergrowth': scene.fog = new THREE.Fog(0x1A2F2A, 3, 15); camera.position.set(0, 1.5, 6); camera.lookAt(0, 0.5, 0); scene.background = new THREE.Color(0x112211); assemblyFunction = createOvergrownPathAssembly; break; case 'forest-clearing-mossy-statue-weathered-stone': case 'forest-clearing-mossy-statue-hidden-compartment': case 'forest-clearing-mossy-statue-offering': scene.fog = new THREE.Fog(0x2E4F3A, 5, 25); camera.position.set(0, 2, 5); camera.lookAt(0, 1, 0); scene.background = new THREE.Color(0x223322); assemblyFunction = createClearingStatueAssembly; break; case 'narrow-game-trail-forest-rope-bridge-ravine': case 'character-crossing-rope-bridge-safely': case 'rope-bridge-snapping-character-falling': case 'fallen-log-crossing-ravine': scene.fog = new THREE.Fog(0x2E2E2E, 5, 20); camera.position.set(2, 3, 6); camera.lookAt(0, -1, -2); scene.background = new THREE.Color(0x1A1A1A); assemblyFunction = createForestAssembly; break; case 'two-goblins-ambush-forest-path-spears': case 'forest-shadows-hiding-goblins-walking-past': case 'defeated-goblins-forest-path-loot': case 'blurred-motion-running-past-goblins-forest': scene.fog = new THREE.Fog(0x1A2F2A, 3, 15); camera.position.set(0, 2, 7); camera.lookAt(0, 1, 0); scene.background = new THREE.Color(0x112211); assemblyFunction = createGoblinAmbushAssembly; break; case 'forest-stream-crossing-dappled-sunlight-stones': case 'mossy-log-bridge-over-forest-stream': case 'character-splashing-into-stream-from-log': scene.fog = new THREE.Fog(0x668866, 8, 25); camera.position.set(0, 2, 6); camera.lookAt(0, 0.5, 0); scene.background = new THREE.Color(0x446644); if (illustrationKey === 'mossy-log-bridge-over-forest-stream') camera.position.set(1, 2, 5); assemblyFunction = createForestAssembly; break; case 'forest-edge-view-rocky-foothills-distant-mountain-fortress': case 'forest-edge': scene.fog = new THREE.Fog(0xAAAAAA, 10, 40); camera.position.set(0, 3, 10); camera.lookAt(0, 1, -5); scene.background = new THREE.Color(0x888888); assemblyFunction = createForestEdgeAssembly; break; case 'climbing-rocky-foothills-path-fortress-closer': case 'rockslide-blocking-mountain-path-boulders': case 'character-climbing-over-boulders': case 'character-slipping-on-rockslide-boulders': case 'rough-detour-path-around-rockslide': scene.fog = new THREE.Fog(0x778899, 8, 35); camera.position.set(0, 4, 9); camera.lookAt(0, 2, 0); scene.background = new THREE.Color(0x708090); assemblyFunction = createDefaultAssembly; break; case 'zoomed-view-mountain-fortress-western-ridge': scene.fog = new THREE.Fog(0x778899, 8, 35); camera.position.set(5, 6, 12); camera.lookAt(-2, 3, -5); scene.background = new THREE.Color(0x708090); assemblyFunction = createDefaultAssembly; break; case 'narrow-goat-trail-mountainside-fortress-view': scene.fog = new THREE.Fog(0x778899, 5, 30); camera.position.set(1, 3, 6); camera.lookAt(0, 2, -2); scene.background = new THREE.Color(0x708090); assemblyFunction = createDefaultAssembly; break; case 'narrow-windy-mountain-ridge-path': case 'character-falling-off-windy-ridge': scene.fog = new THREE.Fog(0x8899AA, 6, 25); camera.position.set(2, 5, 7); camera.lookAt(0, 3, -3); scene.background = new THREE.Color(0x778899); assemblyFunction = createDefaultAssembly; break; case 'approaching-dark-fortress-walls-guards': scene.fog = new THREE.Fog(0x444455, 5, 20); camera.position.set(0, 3, 8); camera.lookAt(0, 2, 0); scene.background = new THREE.Color(0x333344); assemblyFunction = createDefaultAssembly; break; case 'dark-cave-entrance-dripping-water': scene.fog = new THREE.Fog(0x1A1A1A, 2, 10); camera.position.set(0, 1.5, 4); camera.lookAt(0, 1, 0); scene.background = new THREE.Color(0x111111); assemblyFunction = createDarkCaveAssembly; break; default: console.warn(`Unknown illustration key: "${illustrationKey}". Using default scene.`); assemblyFunction = createDefaultAssembly; break; } try { currentAssemblyGroup = assemblyFunction(); if (currentAssemblyGroup && currentAssemblyGroup.isGroup) { scene.add(currentAssemblyGroup); adjustLighting(illustrationKey); } else { throw new Error("Assembly function did not return a valid THREE.Group."); } } catch (error) { console.error(`Error creating assembly for ${illustrationKey}:`, error); if (currentAssemblyGroup) { scene.remove(currentAssemblyGroup); } currentAssemblyGroup = createErrorAssembly(); scene.add(currentAssemblyGroup); adjustLighting('error'); } onWindowResize(); }
683
  function adjustLighting(illustrationKey) { if (!scene) return; const lightsToRemove = scene.children.filter(child => child.isLight && !child.isAmbientLight); lightsToRemove.forEach(light => scene.remove(light)); const ambient = scene.children.find(c => c.isAmbientLight); if (!ambient) { console.warn("No ambient light found, adding default."); scene.add(new THREE.AmbientLight(0xffffff, 0.5)); } let directionalLight; let lightIntensity = 1.2; let ambientIntensity = 0.5; let lightColor = 0xffffff; let lightPosition = { x: 8, y: 15, z: 10 }; switch (illustrationKey) { case 'crossroads-signpost-sunny': case 'rolling-green-hills-shepherd-distance': case 'hilltop-view-overgrown-shrine-wildflowers': case 'overgrown-stone-shrine-wildflowers-close': ambientIntensity = 0.7; lightIntensity = 1.5; lightColor = 0xFFF8E1; lightPosition = { x: 10, y: 15, z: 10 }; break; case 'shadowwood-forest': case 'dark-forest-entrance-gnarled-roots-filtered-light': case 'overgrown-forest-path-glowing-fungi-vines': case 'forest-clearing-mossy-statue-weathered-stone': case 'narrow-game-trail-forest-rope-bridge-ravine': case 'two-goblins-ambush-forest-path-spears': case 'forest-stream-crossing-dappled-sunlight-stones': case 'forest-edge-view-rocky-foothills-distant-mountain-fortress': ambientIntensity = 0.4; lightIntensity = 0.8; lightColor = 0xB0C4DE; lightPosition = { x: 5, y: 12, z: 5 }; break; case 'dark-cave-entrance-dripping-water': ambientIntensity = 0.1; lightIntensity = 0.3; lightColor = 0x667799; lightPosition = { x: 0, y: 5, z: 3 }; break; case 'prisoner-cell': ambientIntensity = 0.2; lightIntensity = 0.5; lightColor = 0x7777AA; lightPosition = { x: 0, y: 10, z: 5 }; break; case 'windy-sea-cliffs-crashing-waves-path-down': case 'hidden-cove-beach-dark-cave-entrance': ambientIntensity = 0.6; lightIntensity = 1.0; lightColor = 0xCCDDFF; lightPosition = { x: -10, y: 12, z: 8 }; break; case 'rocky-badlands-cracked-earth-harsh-sun': ambientIntensity = 0.7; lightIntensity = 1.8; lightColor = 0xFFFFDD; lightPosition = { x: 5, y: 20, z: 5 }; break; case 'climbing-rocky-foothills-path-fortress-closer': case 'zoomed-view-mountain-fortress-western-ridge': case 'narrow-goat-trail-mountainside-fortress-view': case 'narrow-windy-mountain-ridge-path': case 'approaching-dark-fortress-walls-guards': ambientIntensity = 0.5; lightIntensity = 1.3; lightColor = 0xDDEEFF; lightPosition = { x: 10, y: 18, z: 15 }; break; case 'game-over': case 'game-over-generic': ambientIntensity = 0.2; lightIntensity = 0.8; lightColor = 0xFF6666; lightPosition = { x: 0, y: 5, z: 5 }; break; case 'error': ambientIntensity = 0.4; lightIntensity = 1.0; lightColor = 0xFFCC00; lightPosition = { x: 0, y: 5, z: 5 }; break; default: ambientIntensity = 0.5; lightIntensity = 1.2; lightColor = 0xffffff; lightPosition = { x: 8, y: 15, z: 10 }; break; } const currentAmbient = scene.children.find(c => c.isAmbientLight); if (currentAmbient) { currentAmbient.intensity = ambientIntensity; } directionalLight = new THREE.DirectionalLight(lightColor, lightIntensity); directionalLight.position.set(lightPosition.x, lightPosition.y, lightPosition.z); directionalLight.castShadow = true; directionalLight.shadow.mapSize.set(1024, 1024); directionalLight.shadow.camera.near = 0.5; directionalLight.shadow.camera.far = 50; directionalLight.shadow.camera.left = -20; directionalLight.shadow.camera.right = 20; directionalLight.shadow.camera.top = 20; directionalLight.shadow.camera.bottom = -20; directionalLight.shadow.bias = -0.001; scene.add(directionalLight); }
684
 
685
- // --- Potential Future Improvements Comment --- (Checked - OK)
686
  /* [Keep comment block here] */
687
 
688
  // ========================================
689
- // Initialization (Checked - OK)
690
  // ========================================
691
  document.addEventListener('DOMContentLoaded', () => {
692
- console.log("DOM Ready. Initializing game...");
693
- // Attempt to initialize Three.js first
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
694
  if (initThreeJS()) {
695
- // If Three.js setup succeeds, start the game
 
696
  startNewGame(); // Call startNewGame for the very first load
697
- console.log("Game Started Successfully.");
698
  } else {
699
- // If Three.js setup failed, display error in UI
 
700
  console.error("Initialization failed: Three.js setup error.");
701
  storyTitleElement.textContent = "Initialization Error";
702
  storyContentElement.innerHTML = `<p>A critical error occurred during 3D scene setup. The adventure cannot begin. Please check the console (F12) for technical details.</p>`;
 
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>Choose Your Own Procedural Adventure</title>
7
  <style>
8
+ /* [Keep all existing CSS rules here] */
9
  body {
10
  font-family: 'Courier New', monospace;
11
  background-color: #222;
 
39
  padding: 20px;
40
  overflow-y: auto;
41
  background-color: #333;
42
+ min-width: 280px; /* Increased min-width slightly */
43
  height: 100%;
44
  box-sizing: border-box;
45
  display: flex;
 
60
  #story-content {
61
  margin-bottom: 20px;
62
  line-height: 1.6;
63
+ flex-grow: 1; /* Let story content grow */
64
  }
65
  #story-content p { margin-bottom: 1em; }
66
  #story-content p:last-child { margin-bottom: 0; }
 
101
  #inventory-display .item-unknown { background-color: #555; border-color: #777;}
102
 
103
  #choices-container {
104
+ margin-top: auto; /* Push choices to bottom */
105
  padding-top: 15px;
106
  border-top: 1px solid #555;
107
  }
 
119
  .choice-button:hover:not(:disabled) { background-color: #d4a017; color: #222; border-color: #b8860b; }
120
  .choice-button:disabled { background-color: #444; color: #888; cursor: not-allowed; border-color: #666; opacity: 0.7; }
121
 
122
+ /* Specific style for Sell buttons */
123
+ .sell-button {
124
+ background-color: #4a4a4a;
125
+ border-color: #6a6a6a;
126
+ }
127
+ .sell-button:hover:not(:disabled) {
128
+ background-color: #a07017; /* Different hover for sell */
129
+ border-color: #80500b;
130
+ }
131
 
132
  .roll-success { color: #7f7; border-left: 3px solid #4a4; padding-left: 8px; margin-bottom: 1em; font-size: 0.9em; }
133
  .roll-failure { color: #f77; border-left: 3px solid #a44; padding-left: 8px; margin-bottom: 1em; font-size: 0.9em; }
 
139
  <div id="ui-container">
140
  <h2 id="story-title">Loading Adventure...</h2>
141
  <div id="story-content">
142
+ <p id="loading-message">Please wait while the adventure loads.</p>
143
  </div>
144
  <div id="stats-inventory-container">
145
  <div id="stats-display"></div>
 
164
  <script type="module">
165
  import * as THREE from 'three';
166
 
167
+ // DOM Element References
168
  const sceneContainer = document.getElementById('scene-container');
169
  const storyTitleElement = document.getElementById('story-title');
170
  const storyContentElement = document.getElementById('story-content');
171
  const choicesElement = document.getElementById('choices');
172
  const statsElement = document.getElementById('stats-display');
173
  const inventoryElement = document.getElementById('inventory-display');
174
+ const loadingMessageElement = document.getElementById('loading-message'); // Get reference to loading message
175
 
176
+ // Global Three.js Variables
177
  let scene, camera, renderer;
178
  let currentAssemblyGroup = null;
179
+ let loadingIntervalId = null; // Variable to hold the timer ID
180
 
181
+ // --- Materials --- [Unchanged]
182
  const stoneMaterial = new THREE.MeshStandardMaterial({ color: 0x888888, roughness: 0.8, metalness: 0.1 });
183
  const woodMaterial = new THREE.MeshStandardMaterial({ color: 0x8B4513, roughness: 0.7, metalness: 0 });
184
  const darkWoodMaterial = new THREE.MeshStandardMaterial({ color: 0x5C3D20, roughness: 0.7, metalness: 0 });
 
195
  const wetStoneMaterial = new THREE.MeshStandardMaterial({ color: 0x2F4F4F, roughness: 0.7 });
196
  const glowMaterial = new THREE.MeshStandardMaterial({ color: 0x00FFAA, emissive: 0x00FFAA, emissiveIntensity: 0.5 });
197
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
 
199
+ // --- Three.js Setup --- [Unchanged]
200
+ function initThreeJS() { if (!sceneContainer) { console.error("Scene container not found!"); return false; } try { scene = new THREE.Scene(); scene.background = new THREE.Color(0x222222); const width = sceneContainer.clientWidth; const height = sceneContainer.clientHeight; if (!width || !height) { console.warn("Scene container has zero dimensions initially."); } camera = new THREE.PerspectiveCamera(75, (width / height) || 1, 0.1, 1000); camera.position.set(0, 2.5, 7); camera.lookAt(0, 0.5, 0); renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(width || 400, height || 300); renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFSoftShadowMap; sceneContainer.innerHTML = ''; sceneContainer.appendChild(renderer.domElement); const ambientLight = new THREE.AmbientLight(0xffffff, 0.5); scene.add(ambientLight); window.addEventListener('resize', onWindowResize, false); setTimeout(onWindowResize, 100); animate(); return true; } catch (error) { console.error("Error during Three.js initialization:", error); return false; } }
201
+ function onWindowResize() { if (!renderer || !camera || !sceneContainer) return; const width = sceneContainer.clientWidth; const height = sceneContainer.clientHeight; if (width > 0 && height > 0) { camera.aspect = width / height; camera.updateProjectionMatrix(); renderer.setSize(width, height); } else { console.warn("onWindowResize called with zero dimensions."); } }
202
+ function animate() { if (!renderer || !scene || !camera) { return; } requestAnimationFrame(animate); try { const time = performance.now() * 0.001; scene.traverse(obj => { if (obj.userData && obj.userData.update && typeof obj.userData.update === 'function') { obj.userData.update(time); } }); renderer.render(scene, camera); } catch (error) { console.error("Error during animation/render loop:", error); } }
203
+ function createMesh(geometry, material, position = { x: 0, y: 0, z: 0 }, rotation = { x: 0, y: 0, z: 0 }, scale = { x: 1, y: 1, z: 1 }) { const mesh = new THREE.Mesh(geometry, material); mesh.position.set(position.x, position.y, position.z); mesh.rotation.set(rotation.x, rotation.y, rotation.z); mesh.scale.set(scale.x, scale.y, scale.z); mesh.castShadow = true; mesh.receiveShadow = true; return mesh; }
204
+ function createGroundPlane(material = groundMaterial, size = 20) { const groundGeo = new THREE.PlaneGeometry(size, size); const ground = new THREE.Mesh(groundGeo, material); ground.rotation.x = -Math.PI / 2; ground.position.y = -0.05; ground.receiveShadow = true; ground.castShadow = false; return ground; }
 
205
 
206
+ // --- Procedural Generation Functions --- [Unchanged]
207
+ // [Keep all create...Assembly functions here]
 
208
  function createDefaultAssembly() { const group = new THREE.Group(); const sphereGeo = new THREE.SphereGeometry(0.5, 16, 16); group.add(createMesh(sphereGeo, stoneMaterial, { x: 0, y: 0.5, z: 0 })); group.add(createGroundPlane()); return group; }
209
  function createCityGatesAssembly() { const group = new THREE.Group(); const gh = 4, gw = 1.5, gd = 0.8, ah = 1, aw = 3; const tlGeo = new THREE.BoxGeometry(gw, gh, gd); group.add(createMesh(tlGeo, stoneMaterial, { x: -(aw / 2 + gw / 2), y: gh / 2, z: 0 })); const trGeo = new THREE.BoxGeometry(gw, gh, gd); group.add(createMesh(trGeo, stoneMaterial, { x: (aw / 2 + gw / 2), y: gh / 2, z: 0 })); const aGeo = new THREE.BoxGeometry(aw, ah, gd); group.add(createMesh(aGeo, stoneMaterial, { x: 0, y: gh - ah / 2, z: 0 })); const cs = 0.4; const cg = new THREE.BoxGeometry(cs, cs, gd * 1.1); for (let i = -1; i <= 1; i += 2) { group.add(createMesh(cg.clone(), stoneMaterial, { x: -(aw / 2 + gw / 2) + i * cs * 0.7, y: gh + cs / 2, z: 0 })); group.add(createMesh(cg.clone(), stoneMaterial, { x: (aw / 2 + gw / 2) + i * cs * 0.7, y: gh + cs / 2, z: 0 })); } group.add(createMesh(cg.clone(), stoneMaterial, { x: 0, y: gh + ah - cs / 2, z: 0 })); group.add(createGroundPlane(stoneMaterial)); return group; }
210
  function createWeaponsmithAssembly() { const group = new THREE.Group(); const bw = 3, bh = 2.5, bd = 3.5; const bGeo = new THREE.BoxGeometry(bw, bh, bd); group.add(createMesh(bGeo, darkWoodMaterial, { x: 0, y: bh / 2, z: 0 })); const ch = 3.5; const cGeo = new THREE.CylinderGeometry(0.3, 0.4, ch, 8); group.add(createMesh(cGeo, stoneMaterial, { x: bw * 0.3, y: ch / 2, z: -bd * 0.3 })); group.add(createGroundPlane(dirtMaterial)); return group; }
 
227
  function createDarkCaveAssembly() { const group = new THREE.Group(); const caveRadius = 5; const caveHeight = 4; group.add(createGroundPlane(wetStoneMaterial, caveRadius * 2)); const wallGeo = new THREE.SphereGeometry(caveRadius, 32, 16, 0, Math.PI * 2, 0, Math.PI / 1.5); const wallMat = wetStoneMaterial.clone(); wallMat.side = THREE.BackSide; const wall = new THREE.Mesh(wallGeo, wallMat); wall.position.y = caveHeight * 0.6; group.add(wall); const stalactiteGeo = new THREE.ConeGeometry(0.1, 0.8, 8); const stalagmiteGeo = new THREE.ConeGeometry(0.15, 0.5, 8); for (let i = 0; i < 15; i++) { const x = (Math.random() - 0.5) * caveRadius * 1.5; const z = (Math.random() - 0.5) * caveRadius * 1.5; if (Math.random() > 0.5) { group.add(createMesh(stalactiteGeo, wetStoneMaterial, { x: x, y: caveHeight - 0.4, z: z })) } else { group.add(createMesh(stalagmiteGeo, wetStoneMaterial, { x: x, y: 0.25, z: z })) } } const dripGeo = new THREE.SphereGeometry(0.05, 8, 8); for (let i = 0; i < 5; i++) { const drip = createMesh(dripGeo, oceanMaterial, { x: (Math.random() - 0.5) * caveRadius, y: caveHeight - 0.2, z: (Math.random() - 0.5) * caveRadius }); drip.userData.startY = caveHeight - 0.2; drip.userData.update = (time) => { drip.position.y -= 0.1; if (drip.position.y < 0) { drip.position.y = drip.userData.startY; drip.position.x = (Math.random() - 0.5) * caveRadius; drip.position.z = (Math.random() - 0.5) * caveRadius; } }; group.add(drip); } return group; }
228
 
229
  // ========================================
230
+ // Game Data
231
  // ========================================
232
+ const itemsData = { // [Unchanged]
233
  "Flaming Sword": {type:"weapon", description:"A legendary blade, wreathed in magical fire.", goldValue: 500},
234
  "Whispering Bow": {type:"weapon", description:"Crafted by elves, its arrows fly almost silently.", goldValue: 350},
235
  "Guardian Shield": {type:"armor", description:"A sturdy shield imbued with protective enchantments.", goldValue: 250},
236
  "Healing Light Spell":{type:"spell", description:"A scroll containing the incantation to mend minor wounds.", goldValue: 50},
237
  "Shield of Faith Spell":{type:"spell",description:"A scroll containing a prayer that grants temporary magical protection.", goldValue: 75},
238
  "Binding Runes Scroll":{type:"spell", description:"Complex runes scribbled on parchment, said to temporarily immobilize a foe.", goldValue: 100},
239
+ "Secret Tunnel Map": {type:"quest", description:"A crudely drawn map showing a hidden path, perhaps into the fortress?", goldValue: 0},
240
  "Poison Daggers": {type:"weapon", description:"A pair of wicked-looking daggers coated in a fast-acting toxin.", goldValue: 150},
241
  "Master Key": {type:"quest", description:"An ornate key rumored to unlock many doors, though perhaps not all.", goldValue: 0},
242
  "Crude Dagger": {type:"weapon", description:"A roughly made dagger, chipped and stained.", goldValue: 10},
243
+ "Scout's Pouch": {type:"quest", description:"A small leather pouch containing flint & steel, jerky, and some odd coins.", goldValue: 20}
244
  };
245
 
246
+ const gameData = { // [Unchanged except page 99]
 
 
247
  "1": { title: "The Crossroads", content: `<p>Dust swirls around a weathered signpost under a bright, midday sun. Paths lead north into the gloomy Shadowwood, east towards rolling green hills, and west towards coastal cliffs battered by sea spray. Which path calls to you?</p>`, options: [ { text: "Enter the Shadowwood Forest (North)", next: 5 }, { text: "Head towards the Rolling Hills (East)", next: 2 }, { text: "Investigate the Coastal Cliffs (West)", next: 3 } ], illustration: "crossroads-signpost-sunny" },
248
  "2": { title: "Rolling Hills", content: `<p>Verdant hills stretch before you, dotted with wildflowers. A gentle breeze whispers through the tall grass. In the distance, you see a lone figure tending to a flock of sheep. It feels peaceful, almost unnervingly so after the crossroads.</p>`, options: [ { text: "Follow the narrow path winding through the hills", next: 4 }, { text: "Try to hail the distant shepherd (Charisma Check?)", next: 99 } ], illustration: "rolling-green-hills-shepherd-distance" },
249
  "3": { title: "Coastal Cliffs Edge", content: `<p>You stand atop windswept cliffs, the roar of crashing waves filling the air below. Seabirds circle overhead. A precarious-looking path, seemingly carved by desperate hands, descends the cliff face towards a hidden cove.</p>`, options: [ { text: "Attempt the precarious descent (Dexterity Check)", check: { stat: 'dexterity', dc: 12, onFailure: 31 }, next: 30 }, { text: "Scan the cliff face for easier routes (Wisdom Check)", check: { stat: 'wisdom', dc: 11, onFailure: 32 }, next: 33 } ], illustration: "windy-sea-cliffs-crashing-waves-path-down" },
 
258
  "13": { title: "Daring Escape", content: `<p>With surprising agility, you feint left, then dive right, tumbling past the goblins' clumsy spear thrusts! You scramble to your feet and sprint down the path, leaving the surprised goblins behind.</p><p>(+25 XP)</p>`, options: [ { text: "Keep running!", next: 14 } ], illustration: "blurred-motion-running-past-goblins-forest", reward: { xp: 25 } },
259
  "14": { title: "Forest Stream Crossing", content: `<p>The overgrown path eventually leads to the bank of a clear, shallow stream. Smooth, mossy stones line the streambed, and dappled sunlight filters through the leaves overhead, sparkling on the water's surface.</p>`, options: [ { text: "Wade across the stream", next: 16 }, { text: "Look for a drier crossing point (fallen log?) upstream", next: 15 } ], illustration: "forest-stream-crossing-dappled-sunlight-stones" },
260
  "15": { title: "Log Bridge", content: `<p>A short walk upstream reveals a large, fallen tree spanning the stream. It's covered in slick, green moss, making it look like a potentially treacherous crossing.</p>`, options: [ { text: "Cross carefully on the mossy log (Dexterity Check)", check: { stat: 'dexterity', dc: 9, onFailure: 151 }, next: 16 }, { text: "Decide it's too risky and go back to wade across", next: 14 } ], illustration: "mossy-log-bridge-over-forest-stream" },
 
261
  "16": { title: "Edge of the Woods", content: `<p>Finally, the trees begin to thin, and you emerge from the oppressive gloom of the Shadowwood. Before you lie steep, rocky foothills leading up towards a formidable-looking mountain fortress perched high above.</p>`, options: [ { text: "Begin the ascent into the foothills towards the fortress", next: 17 }, { text: "Scan the fortress and surrounding terrain from afar (Wisdom Check)", check: { stat: 'wisdom', dc: 14, onFailure: 17 }, next: 18 } ], illustration: "forest-edge-view-rocky-foothills-distant-mountain-fortress" },
262
  "17": { title: "Rocky Foothills Path", content: `<p>The climb is arduous, the path winding steeply upwards over loose scree and jagged rocks. The air thins slightly. The dark stone walls of the mountain fortress loom much larger now, seeming to watch your approach.</p>`, options: [ { text: "Continue the direct ascent", next: 19 }, { text: "Look for signs of a hidden trail or less obvious route (Wisdom Check)", check: { stat: 'wisdom', dc: 15, onFailure: 19 }, next: 20 } ], illustration: "climbing-rocky-foothills-path-fortress-closer" },
263
  "18": { title: "Distant Observation", content: `<p>Taking a moment to study the fortress from this distance, your keen eyes notice something interesting. The main approach looks heavily guarded, but along the western ridge, the terrain seems slightly less sheer, potentially offering a less-guarded, albeit more treacherous, approach.</p><p>(+30 XP)</p>`, options: [ { text: "Decide against the risk and take the main path into the foothills", next: 17 }, { text: "Attempt the western ridge approach", next: 21 } ], illustration: "zoomed-view-mountain-fortress-western-ridge", reward: { xp: 30 } },
 
293
  "401": { title: "Mysterious Carvings", content:"<p>The carvings are too worn and abstract to decipher their specific meaning, though you sense they are very old.</p>", options: [{text:"Continue towards the badlands", next: 41}], illustration: "overgrown-stone-shrine-wildflowers-close"},
294
  "402": { title: "Moment of Peace", content:"<p>You spend a quiet moment in reflection. While no divine voice answers, the tranquility of the place settles your nerves.</p>", options: [{text:"Continue towards the badlands", next: 41}], illustration: "overgrown-stone-shrine-wildflowers-close"},
295
 
 
296
  "99": {
297
  title: "Game Over / To Be Continued...",
298
  content: "<p>Your adventure ends here... for now. You can sell unwanted items for gold before starting again.</p>",
299
  options: [ /* Restart button added dynamically */ ],
300
  illustration: "game-over-generic",
301
  gameOver: true,
302
+ allowSell: true // Enable selling feature
303
  }
304
  };
305
 
306
+
307
  // ========================================
308
+ // Game State
309
  // ========================================
310
  const defaultCharacterState = {
311
  name: "Hero", race: "Human", alignment: "Neutral Good", class: "Adventurer",
 
315
  };
316
  let gameState = {
317
  currentPageId: 1,
318
+ character: JSON.parse(JSON.stringify(defaultCharacterState))
319
  };
320
 
321
+
322
  // ========================================
323
+ // Game Logic Functions
324
  // ========================================
325
 
326
+ // Clears the loading animation interval
327
+ function stopLoadingAnimation() {
328
+ if (loadingIntervalId !== null) {
329
+ clearInterval(loadingIntervalId);
330
+ loadingIntervalId = null;
331
+ // Optional: Reset loading message text if needed, though renderPageInternal will overwrite it
332
+ if(loadingMessageElement) loadingMessageElement.textContent = "Please wait while the adventure loads.";
333
+ console.log("Loading animation stopped.");
334
+ }
335
+ }
336
+
337
  function startNewGame() {
338
  console.log("Starting brand new game...");
339
+ stopLoadingAnimation(); // Stop animation when game actually starts
340
  gameState = {
341
  currentPageId: 1,
342
+ character: JSON.parse(JSON.stringify(defaultCharacterState))
343
  };
344
+ renderPage(gameState.currentPageId);
345
  }
346
 
347
  function restartGamePlus() {
348
  console.log("Restarting game (keeping progress)...");
349
+ stopLoadingAnimation(); // Stop animation if restarting from an error state perhaps
350
  gameState.currentPageId = 1;
351
+ renderPage(gameState.currentPageId);
352
  }
353
 
354
+ function handleChoiceClick(choiceData) { // [Unchanged from previous working version]
355
  console.log("Choice clicked:", choiceData);
356
+ let feedbackMessage = "";
357
+ if (choiceData.action === 'restart_plus') { restartGamePlus(); return; }
358
+ if (choiceData.action === 'sell_item') { feedbackMessage = handleSellItem(choiceData.item); renderPageInternal(gameState.currentPageId, gameData[gameState.currentPageId], feedbackMessage); return; }
359
+ const optionNextPageId = parseInt(choiceData.next); const itemToAdd = choiceData.addItem; let nextPageId = optionNextPageId; const check = choiceData.check;
360
+ if (isNaN(optionNextPageId) && !check) { console.error("Invalid choice data:", choiceData); feedbackMessage = `<p class="feedback-error">Error: Invalid choice data!</p>`; renderPageInternal(gameState.currentPageId, gameData[gameState.currentPageId], feedbackMessage); choicesElement.querySelectorAll('button').forEach(b => b.disabled = true); return; }
361
+ if (check) { const statValue = gameState.character.stats[check.stat] || 10; const modifier = Math.floor((statValue - 10) / 2); const roll = Math.floor(Math.random() * 20) + 1; const totalResult = roll + modifier; const dc = check.dc; const statName = check.stat.charAt(0).toUpperCase() + check.stat.slice(1); console.log(`Check: ${statName} (DC ${dc}) | Roll: ${roll} + Mod: ${modifier} = ${totalResult}`); if (totalResult >= dc) { nextPageId = optionNextPageId; feedbackMessage += `<p class="roll-success"><em>${statName} Check Success! (${totalResult} vs DC ${dc})</em></p>`; } else { nextPageId = parseInt(check.onFailure); feedbackMessage += `<p class="roll-failure"><em>${statName} Check Failed! (${totalResult} vs DC ${dc})</em></p>`; if (isNaN(nextPageId)) { console.error("Invalid onFailure ID:", check.onFailure); nextPageId = 99; feedbackMessage += `<p class="feedback-error">Error: Invalid failure path!</p>`; } } }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
362
  const targetPageData = gameData[nextPageId];
363
+ if (!targetPageData) { console.error(`Data for target page ${nextPageId} not found!`); feedbackMessage = `<p class="feedback-error">Error: Next page data missing!</p>`; renderPageInternal(99, gameData[99], feedbackMessage); return; }
364
+ let hpLostThisTurn = 0; if (targetPageData.hpLoss) { hpLostThisTurn = targetPageData.hpLoss; gameState.character.stats.hp -= hpLostThisTurn; console.log(`Lost ${hpLostThisTurn} HP.`); } if (targetPageData.reward && targetPageData.reward.hpGain) { const hpGained = targetPageData.reward.hpGain; gameState.character.stats.hp += hpGained; console.log(`Gained ${hpGained} HP.`); }
365
+ if (gameState.character.stats.hp <= 0) { gameState.character.stats.hp = 0; console.log("Player died!"); nextPageId = 99; feedbackMessage += `<p class="feedback-error"><em>You have succumbed to your injuries!${hpLostThisTurn > 0 ? ` (-${hpLostThisTurn} HP)` : ''}</em></p>`; renderPageInternal(nextPageId, gameData[nextPageId], feedbackMessage); return; }
366
+ if (targetPageData.reward) { if (targetPageData.reward.xp) { gameState.character.xp += targetPageData.reward.xp; console.log(`Gained ${targetPageData.reward.xp} XP! Total: ${gameState.character.xp}`); } if (targetPageData.reward.statIncrease) { const stat = targetPageData.reward.statIncrease.stat; const amount = targetPageData.reward.statIncrease.amount; if (gameState.character.stats.hasOwnProperty(stat)) { gameState.character.stats[stat] += amount; console.log(`Stat ${stat} increased by ${amount}. New value: ${gameState.character.stats[stat]}`); if (stat === 'constitution') recalculateMaxHp(); } } if (targetPageData.reward.addItem && !gameState.character.inventory.includes(targetPageData.reward.addItem)) { const itemName = targetPageData.reward.addItem; if (itemsData[itemName]) { gameState.character.inventory.push(itemName); console.log(`Found item: ${itemName}`); feedbackMessage += `<p class="feedback-message"><em>Item acquired: ${itemName}</em></p>`; } else { console.warn(`Attempted to add unknown item reward: ${itemName}`); feedbackMessage += `<p class="feedback-error"><em>Error: Tried to acquire unknown item '${itemName}'!</em></p>`; } } }
367
+ if (itemToAdd && !gameState.character.inventory.includes(itemToAdd)) { if (itemsData[itemToAdd]) { gameState.character.inventory.push(itemToAdd); console.log("Added item:", itemToAdd); feedbackMessage += `<p class="feedback-message"><em>Item acquired: ${itemToAdd}</em></p>`; } else { console.warn(`Attempted to add unknown item choice: ${itemToAdd}`); feedbackMessage += `<p class="feedback-error"><em>Error: Tried to acquire unknown item '${itemToAdd}'!</em></p>`; } }
368
+ gameState.currentPageId = nextPageId; recalculateMaxHp(); gameState.character.stats.hp = Math.min(gameState.character.stats.hp, gameState.character.stats.maxHp);
369
+ console.log("Transitioning to page:", nextPageId, " New state:", JSON.stringify(gameState)); renderPageInternal(nextPageId, gameData[nextPageId], feedbackMessage);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
370
  }
371
 
372
+ function handleSellItem(itemName) { // [Unchanged from previous working version]
373
+ console.log("Attempting to sell:", itemName); const itemIndex = gameState.character.inventory.indexOf(itemName); const itemInfo = itemsData[itemName]; let message = ""; if (itemIndex === -1) { console.warn(`Sell failed: Item "${itemName}" not in inventory.`); message = `<p class="feedback-error">Cannot sell ${itemName} - you don't have it!</p>`; } else if (!itemInfo) { console.warn(`Sell failed: Item data for "${itemName}" not found.`); message = `<p class="feedback-error">Cannot sell ${itemName} - item data missing!</p>`; } else if (itemInfo.type === 'quest') { console.log(`Sell blocked: Item "${itemName}" is a quest item.`); message = `<p class="feedback-message">Cannot sell ${itemName} - it seems important.</p>`; } else if (itemInfo.goldValue === undefined || itemInfo.goldValue <= 0) { console.log(`Sell blocked: Item "${itemName}" has no gold value.`); message = `<p class="feedback-message">${itemName} isn't worth any gold.</p>`; } else { const value = itemInfo.goldValue; gameState.character.gold += value; gameState.character.inventory.splice(itemIndex, 1); message = `<p class="feedback-success">Sold ${itemName} for ${value} Gold.</p>`; console.log(`Sold ${itemName} for ${value} gold. Current gold: ${gameState.character.gold}`); } return message;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
374
  }
375
 
376
+ function recalculateMaxHp() { // [Unchanged from previous working version]
377
+ const baseHp = 10; const conModifier = Math.floor((gameState.character.stats.constitution - 10) / 2); gameState.character.stats.maxHp = Math.max(1, baseHp + conModifier * gameState.character.level);
 
 
 
378
  }
379
 
380
+ function renderPageInternal(pageId, pageData, message = "") { // [Unchanged from previous working version]
381
+ stopLoadingAnimation(); // Stop loading animation whenever a page renders
382
+ if (!pageData) { console.error(`Render Error: No data for page ${pageId}`); pageData = gameData["99"] || { title: "Error", content: "<p>Render Error! Critical page data missing.</p>", illustration: "error", gameOver: true }; message += `<p class="feedback-error">Render Error: Page data for ID ${pageId} was missing!</p>`; pageId = 99; } console.log(`Rendering page ${pageId}: "${pageData.title}"`);
383
+ storyTitleElement.textContent = pageData.title || "Untitled Page"; storyContentElement.innerHTML = message + (pageData.content || "<p>...</p>"); updateStatsDisplay(); updateInventoryDisplay(); choicesElement.innerHTML = '';
384
+ const options = pageData.options || []; const isGameOverPage = pageData.gameOver === true;
385
+ if (isGameOverPage && pageData.allowSell === true) { const sellableItems = gameState.character.inventory.filter(itemName => { const itemInfo = itemsData[itemName]; return itemInfo && itemInfo.type !== 'quest' && itemInfo.goldValue !== undefined && itemInfo.goldValue > 0; }); if (sellableItems.length > 0) { choicesElement.innerHTML += `<h3 style="margin-bottom: 5px;">Sell Items:</h3>`; sellableItems.forEach(itemName => { const itemInfo = itemsData[itemName]; if (!itemInfo) return; const sellButton = document.createElement('button'); sellButton.classList.add('choice-button', 'sell-button'); sellButton.textContent = `Sell ${itemName} (${itemInfo.goldValue} Gold)`; sellButton.onclick = () => handleChoiceClick({ action: 'sell_item', item: itemName }); choicesElement.appendChild(sellButton); }); choicesElement.innerHTML += `<hr style="border-color: #555; margin: 15px 0 10px 0;">`; } }
386
+ if (!isGameOverPage && options.length > 0) { options.forEach(option => { const button = document.createElement('button'); button.classList.add('choice-button'); button.textContent = option.text; let requirementMet = true; let requirementText = []; if (option.requireItem) { if (!gameState.character.inventory.includes(option.requireItem)) { requirementMet = false; requirementText.push(`Requires: ${option.requireItem}`); } } if (option.requireStat) { const currentStat = gameState.character.stats[option.requireStat.stat] || 0; if (currentStat < option.requireStat.value) { requirementMet = false; requirementText.push(`Requires: ${option.requireStat.stat.charAt(0).toUpperCase() + option.requireStat.stat.slice(1)} ${option.requireStat.value}`); } } button.disabled = !requirementMet; if (!requirementMet) button.title = requirementText.join(', '); else { const choiceData = { next: option.next, addItem: option.addItem, check: option.check }; button.onclick = () => handleChoiceClick(choiceData); } choicesElement.appendChild(button); }); } else if (isGameOverPage) { const restartButton = document.createElement('button'); restartButton.classList.add('choice-button'); restartButton.textContent = "Restart Adventure (Keep Progress)"; restartButton.onclick = () => handleChoiceClick({ action: 'restart_plus' }); choicesElement.appendChild(restartButton); } else if (pageId !== 99) { choicesElement.insertAdjacentHTML('beforeend', '<p><i>There are no further paths from here.</i></p>'); const restartButton = document.createElement('button'); restartButton.classList.add('choice-button'); restartButton.textContent = "Restart Adventure (Keep Progress)"; restartButton.onclick = () => handleChoiceClick({ action: 'restart_plus' }); choicesElement.appendChild(restartButton); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
387
  updateScene(pageData.illustration || 'default');
388
  }
389
 
390
+ function renderPage(pageId) { renderPageInternal(pageId, gameData[pageId]); } // [Unchanged]
391
+ function updateStatsDisplay() { const char = gameState.character; statsElement.innerHTML = `<strong>Stats:</strong> <span class="stat-gold">Gold: ${char.gold}</span> <span>Lvl: ${char.level}</span> <span>XP: ${char.xp}/${char.xpToNextLevel}</span> <span>HP: ${char.stats.hp}/${char.stats.maxHp}</span> <span>Str: ${char.stats.strength}</span> <span>Int: ${char.stats.intelligence}</span> <span>Wis: ${char.stats.wisdom}</span> <span>Dex: ${char.stats.dexterity}</span> <span>Con: ${char.stats.constitution}</span> <span>Cha: ${char.stats.charisma}</span>`; } // [Unchanged]
392
+ function updateInventoryDisplay() { let h = '<strong>Inventory:</strong> '; if (gameState.character.inventory.length === 0) { h += '<em>Empty</em>'; } else { gameState.character.inventory.forEach(itemName => { const item = itemsData[itemName] || { type: 'unknown', description: 'An unknown item.' }; const itemClass = `item-${item.type || 'unknown'}`; const descriptionText = typeof item.description === 'string' ? item.description : 'No description available.'; h += `<span class="${itemClass}" title="${descriptionText.replace(/"/g, '&quot;')}">${itemName}</span>`; }); } inventoryElement.innerHTML = h; } // [Unchanged]
 
 
 
393
 
394
+ // --- Scene Update and Lighting --- [Unchanged]
395
+ // [Keep updateScene and adjustLighting functions here]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
396
  function updateScene(illustrationKey) { if (!scene) { console.warn("Scene not initialized, cannot update visual."); return; } console.log("Updating scene for illustration key:", illustrationKey); if (currentAssemblyGroup) { scene.remove(currentAssemblyGroup); currentAssemblyGroup.traverse(child => { if (child.isMesh) { child.geometry.dispose(); } }); currentAssemblyGroup = null; } scene.fog = null; scene.background = new THREE.Color(0x222222); camera.position.set(0, 2.5, 7); camera.lookAt(0, 0.5, 0); let assemblyFunction; switch (illustrationKey) { case 'city-gates': assemblyFunction = createCityGatesAssembly; break; case 'weaponsmith': assemblyFunction = createWeaponsmithAssembly; break; case 'temple': assemblyFunction = createTempleAssembly; break; case 'resistance-meeting': assemblyFunction = createResistanceMeetingAssembly; break; case 'prisoner-cell': assemblyFunction = createPrisonerCellAssembly; break; case 'game-over': case 'game-over-generic': assemblyFunction = createGameOverAssembly; break; case 'error': assemblyFunction = createErrorAssembly; break; case 'crossroads-signpost-sunny': scene.fog = new THREE.Fog(0x87CEEB, 10, 35); camera.position.set(0, 3, 10); camera.lookAt(0, 1, 0); scene.background = new THREE.Color(0x87CEEB); assemblyFunction = createCrossroadsAssembly; break; case 'rolling-green-hills-shepherd-distance': case 'hilltop-view-overgrown-shrine-wildflowers': case 'overgrown-stone-shrine-wildflowers-close': scene.fog = new THREE.Fog(0xA8E4A0, 15, 50); camera.position.set(0, 5, 15); camera.lookAt(0, 2, -5); scene.background = new THREE.Color(0x90EE90); if (illustrationKey === 'overgrown-stone-shrine-wildflowers-close') camera.position.set(1, 2, 4); if (illustrationKey === 'hilltop-view-overgrown-shrine-wildflowers') camera.position.set(3, 4, 8); assemblyFunction = createRollingHillsAssembly; break; case 'windy-sea-cliffs-crashing-waves-path-down': case 'scanning-sea-cliffs-no-other-paths-visible': case 'close-up-handholds-carved-in-cliff-face': scene.fog = new THREE.Fog(0x6699CC, 10, 40); camera.position.set(5, 5, 10); camera.lookAt(-2, 0, -5); scene.background = new THREE.Color(0x6699CC); assemblyFunction = createCoastalCliffsAssembly; break; case 'hidden-cove-beach-dark-cave-entrance': case 'character-fallen-at-bottom-of-cliff-path-cove': scene.fog = new THREE.Fog(0x336699, 5, 30); camera.position.set(0, 2, 8); camera.lookAt(0, 1, -2); scene.background = new THREE.Color(0x336699); assemblyFunction = createHiddenCoveAssembly; break; case 'rocky-badlands-cracked-earth-harsh-sun': scene.fog = new THREE.Fog(0xD2B48C, 15, 40); camera.position.set(0, 3, 12); camera.lookAt(0, 1, 0); scene.background = new THREE.Color(0xCD853F); assemblyFunction = createDefaultAssembly; break; case 'shadowwood-forest': scene.fog = new THREE.Fog(0x2E2E2E, 5, 20); camera.position.set(0, 2, 8); camera.lookAt(0, 1, 0); scene.background = new THREE.Color(0x1A1A1A); assemblyFunction = createForestAssembly; break; case 'dark-forest-entrance-gnarled-roots-filtered-light': scene.fog = new THREE.Fog(0x2E2E2E, 5, 20); camera.position.set(0, 2, 8); camera.lookAt(0, 1, 0); scene.background = new THREE.Color(0x1A1A1A); assemblyFunction = createForestEntranceAssembly; break; case 'overgrown-forest-path-glowing-fungi-vines': case 'pushing-through-forest-undergrowth': scene.fog = new THREE.Fog(0x1A2F2A, 3, 15); camera.position.set(0, 1.5, 6); camera.lookAt(0, 0.5, 0); scene.background = new THREE.Color(0x112211); assemblyFunction = createOvergrownPathAssembly; break; case 'forest-clearing-mossy-statue-weathered-stone': case 'forest-clearing-mossy-statue-hidden-compartment': case 'forest-clearing-mossy-statue-offering': scene.fog = new THREE.Fog(0x2E4F3A, 5, 25); camera.position.set(0, 2, 5); camera.lookAt(0, 1, 0); scene.background = new THREE.Color(0x223322); assemblyFunction = createClearingStatueAssembly; break; case 'narrow-game-trail-forest-rope-bridge-ravine': case 'character-crossing-rope-bridge-safely': case 'rope-bridge-snapping-character-falling': case 'fallen-log-crossing-ravine': scene.fog = new THREE.Fog(0x2E2E2E, 5, 20); camera.position.set(2, 3, 6); camera.lookAt(0, -1, -2); scene.background = new THREE.Color(0x1A1A1A); assemblyFunction = createForestAssembly; break; case 'two-goblins-ambush-forest-path-spears': case 'forest-shadows-hiding-goblins-walking-past': case 'defeated-goblins-forest-path-loot': case 'blurred-motion-running-past-goblins-forest': scene.fog = new THREE.Fog(0x1A2F2A, 3, 15); camera.position.set(0, 2, 7); camera.lookAt(0, 1, 0); scene.background = new THREE.Color(0x112211); assemblyFunction = createGoblinAmbushAssembly; break; case 'forest-stream-crossing-dappled-sunlight-stones': case 'mossy-log-bridge-over-forest-stream': case 'character-splashing-into-stream-from-log': scene.fog = new THREE.Fog(0x668866, 8, 25); camera.position.set(0, 2, 6); camera.lookAt(0, 0.5, 0); scene.background = new THREE.Color(0x446644); if (illustrationKey === 'mossy-log-bridge-over-forest-stream') camera.position.set(1, 2, 5); assemblyFunction = createForestAssembly; break; case 'forest-edge-view-rocky-foothills-distant-mountain-fortress': case 'forest-edge': scene.fog = new THREE.Fog(0xAAAAAA, 10, 40); camera.position.set(0, 3, 10); camera.lookAt(0, 1, -5); scene.background = new THREE.Color(0x888888); assemblyFunction = createForestEdgeAssembly; break; case 'climbing-rocky-foothills-path-fortress-closer': case 'rockslide-blocking-mountain-path-boulders': case 'character-climbing-over-boulders': case 'character-slipping-on-rockslide-boulders': case 'rough-detour-path-around-rockslide': scene.fog = new THREE.Fog(0x778899, 8, 35); camera.position.set(0, 4, 9); camera.lookAt(0, 2, 0); scene.background = new THREE.Color(0x708090); assemblyFunction = createDefaultAssembly; break; case 'zoomed-view-mountain-fortress-western-ridge': scene.fog = new THREE.Fog(0x778899, 8, 35); camera.position.set(5, 6, 12); camera.lookAt(-2, 3, -5); scene.background = new THREE.Color(0x708090); assemblyFunction = createDefaultAssembly; break; case 'narrow-goat-trail-mountainside-fortress-view': scene.fog = new THREE.Fog(0x778899, 5, 30); camera.position.set(1, 3, 6); camera.lookAt(0, 2, -2); scene.background = new THREE.Color(0x708090); assemblyFunction = createDefaultAssembly; break; case 'narrow-windy-mountain-ridge-path': case 'character-falling-off-windy-ridge': scene.fog = new THREE.Fog(0x8899AA, 6, 25); camera.position.set(2, 5, 7); camera.lookAt(0, 3, -3); scene.background = new THREE.Color(0x778899); assemblyFunction = createDefaultAssembly; break; case 'approaching-dark-fortress-walls-guards': scene.fog = new THREE.Fog(0x444455, 5, 20); camera.position.set(0, 3, 8); camera.lookAt(0, 2, 0); scene.background = new THREE.Color(0x333344); assemblyFunction = createDefaultAssembly; break; case 'dark-cave-entrance-dripping-water': scene.fog = new THREE.Fog(0x1A1A1A, 2, 10); camera.position.set(0, 1.5, 4); camera.lookAt(0, 1, 0); scene.background = new THREE.Color(0x111111); assemblyFunction = createDarkCaveAssembly; break; default: console.warn(`Unknown illustration key: "${illustrationKey}". Using default scene.`); assemblyFunction = createDefaultAssembly; break; } try { currentAssemblyGroup = assemblyFunction(); if (currentAssemblyGroup && currentAssemblyGroup.isGroup) { scene.add(currentAssemblyGroup); adjustLighting(illustrationKey); } else { throw new Error("Assembly function did not return a valid THREE.Group."); } } catch (error) { console.error(`Error creating assembly for ${illustrationKey}:`, error); if (currentAssemblyGroup) { scene.remove(currentAssemblyGroup); } currentAssemblyGroup = createErrorAssembly(); scene.add(currentAssemblyGroup); adjustLighting('error'); } onWindowResize(); }
397
  function adjustLighting(illustrationKey) { if (!scene) return; const lightsToRemove = scene.children.filter(child => child.isLight && !child.isAmbientLight); lightsToRemove.forEach(light => scene.remove(light)); const ambient = scene.children.find(c => c.isAmbientLight); if (!ambient) { console.warn("No ambient light found, adding default."); scene.add(new THREE.AmbientLight(0xffffff, 0.5)); } let directionalLight; let lightIntensity = 1.2; let ambientIntensity = 0.5; let lightColor = 0xffffff; let lightPosition = { x: 8, y: 15, z: 10 }; switch (illustrationKey) { case 'crossroads-signpost-sunny': case 'rolling-green-hills-shepherd-distance': case 'hilltop-view-overgrown-shrine-wildflowers': case 'overgrown-stone-shrine-wildflowers-close': ambientIntensity = 0.7; lightIntensity = 1.5; lightColor = 0xFFF8E1; lightPosition = { x: 10, y: 15, z: 10 }; break; case 'shadowwood-forest': case 'dark-forest-entrance-gnarled-roots-filtered-light': case 'overgrown-forest-path-glowing-fungi-vines': case 'forest-clearing-mossy-statue-weathered-stone': case 'narrow-game-trail-forest-rope-bridge-ravine': case 'two-goblins-ambush-forest-path-spears': case 'forest-stream-crossing-dappled-sunlight-stones': case 'forest-edge-view-rocky-foothills-distant-mountain-fortress': ambientIntensity = 0.4; lightIntensity = 0.8; lightColor = 0xB0C4DE; lightPosition = { x: 5, y: 12, z: 5 }; break; case 'dark-cave-entrance-dripping-water': ambientIntensity = 0.1; lightIntensity = 0.3; lightColor = 0x667799; lightPosition = { x: 0, y: 5, z: 3 }; break; case 'prisoner-cell': ambientIntensity = 0.2; lightIntensity = 0.5; lightColor = 0x7777AA; lightPosition = { x: 0, y: 10, z: 5 }; break; case 'windy-sea-cliffs-crashing-waves-path-down': case 'hidden-cove-beach-dark-cave-entrance': ambientIntensity = 0.6; lightIntensity = 1.0; lightColor = 0xCCDDFF; lightPosition = { x: -10, y: 12, z: 8 }; break; case 'rocky-badlands-cracked-earth-harsh-sun': ambientIntensity = 0.7; lightIntensity = 1.8; lightColor = 0xFFFFDD; lightPosition = { x: 5, y: 20, z: 5 }; break; case 'climbing-rocky-foothills-path-fortress-closer': case 'zoomed-view-mountain-fortress-western-ridge': case 'narrow-goat-trail-mountainside-fortress-view': case 'narrow-windy-mountain-ridge-path': case 'approaching-dark-fortress-walls-guards': ambientIntensity = 0.5; lightIntensity = 1.3; lightColor = 0xDDEEFF; lightPosition = { x: 10, y: 18, z: 15 }; break; case 'game-over': case 'game-over-generic': ambientIntensity = 0.2; lightIntensity = 0.8; lightColor = 0xFF6666; lightPosition = { x: 0, y: 5, z: 5 }; break; case 'error': ambientIntensity = 0.4; lightIntensity = 1.0; lightColor = 0xFFCC00; lightPosition = { x: 0, y: 5, z: 5 }; break; default: ambientIntensity = 0.5; lightIntensity = 1.2; lightColor = 0xffffff; lightPosition = { x: 8, y: 15, z: 10 }; break; } const currentAmbient = scene.children.find(c => c.isAmbientLight); if (currentAmbient) { currentAmbient.intensity = ambientIntensity; } directionalLight = new THREE.DirectionalLight(lightColor, lightIntensity); directionalLight.position.set(lightPosition.x, lightPosition.y, lightPosition.z); directionalLight.castShadow = true; directionalLight.shadow.mapSize.set(1024, 1024); directionalLight.shadow.camera.near = 0.5; directionalLight.shadow.camera.far = 50; directionalLight.shadow.camera.left = -20; directionalLight.shadow.camera.right = 20; directionalLight.shadow.camera.top = 20; directionalLight.shadow.camera.bottom = -20; directionalLight.shadow.bias = -0.001; scene.add(directionalLight); }
398
 
399
+ // --- Potential Future Improvements Comment --- [Unchanged]
400
  /* [Keep comment block here] */
401
 
402
  // ========================================
403
+ // Initialization
404
  // ========================================
405
  document.addEventListener('DOMContentLoaded', () => {
406
+ console.log("DOM Ready. Initializing...");
407
+
408
+ // Start loading animation
409
+ let dotCount = 0;
410
+ const baseLoadingText = "Please wait while the adventure loads";
411
+ if (loadingMessageElement) { // Check if element exists
412
+ loadingIntervalId = setInterval(() => {
413
+ dotCount = (dotCount + 1) % 4; // Cycle 0, 1, 2, 3
414
+ const dots = '.'.repeat(dotCount);
415
+ loadingMessageElement.textContent = baseLoadingText + dots;
416
+ }, 500); // Update every 500ms
417
+ console.log("Loading animation started.");
418
+ } else {
419
+ console.warn("Loading message element not found.");
420
+ }
421
+
422
+ // Attempt to initialize Three.js
423
  if (initThreeJS()) {
424
+ // If Three.js setup succeeds, start the game logic
425
+ // The loading animation will be stopped by the first call to renderPageInternal
426
  startNewGame(); // Call startNewGame for the very first load
427
+ console.log("Game setup initiated.");
428
  } else {
429
+ // If Three.js setup failed, display error in UI and stop animation
430
+ stopLoadingAnimation(); // Stop animation on error
431
  console.error("Initialization failed: Three.js setup error.");
432
  storyTitleElement.textContent = "Initialization Error";
433
  storyContentElement.innerHTML = `<p>A critical error occurred during 3D scene setup. The adventure cannot begin. Please check the console (F12) for technical details.</p>`;