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

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +245 -254
index.html CHANGED
@@ -38,7 +38,7 @@
38
  padding: 20px;
39
  overflow-y: auto;
40
  background-color: #333;
41
- min-width: 280px; /* Increased min-width slightly */
42
  height: 100%;
43
  box-sizing: border-box;
44
  display: flex;
@@ -59,11 +59,14 @@
59
  #story-content {
60
  margin-bottom: 20px;
61
  line-height: 1.6;
62
- flex-grow: 1; /* Let story content grow */
63
  }
64
  #story-content p { margin-bottom: 1em; }
65
  #story-content p:last-child { margin-bottom: 0; }
66
- #story-content .sell-feedback { color: #9f9; font-style: italic; margin-top: 1em;} /* Feedback style */
 
 
 
67
 
68
  #stats-inventory-container {
69
  margin-bottom: 20px;
@@ -97,7 +100,7 @@
97
  #inventory-display .item-unknown { background-color: #555; border-color: #777;}
98
 
99
  #choices-container {
100
- margin-top: auto; /* Push choices to bottom */
101
  padding-top: 15px;
102
  border-top: 1px solid #555;
103
  }
@@ -115,15 +118,8 @@
115
  .choice-button:hover:not(:disabled) { background-color: #d4a017; color: #222; border-color: #b8860b; }
116
  .choice-button:disabled { background-color: #444; color: #888; cursor: not-allowed; border-color: #666; opacity: 0.7; }
117
 
118
- /* Specific style for Sell buttons */
119
- .sell-button {
120
- background-color: #4a4a4a;
121
- border-color: #6a6a6a;
122
- }
123
- .sell-button:hover:not(:disabled) {
124
- background-color: #a07017; /* Different hover for sell */
125
- border-color: #80500b;
126
- }
127
 
128
  .roll-success { color: #7f7; border-left: 3px solid #4a4; padding-left: 8px; margin-bottom: 1em; font-size: 0.9em; }
129
  .roll-failure { color: #f77; border-left: 3px solid #a44; padding-left: 8px; margin-bottom: 1em; font-size: 0.9em; }
@@ -160,6 +156,7 @@
160
  <script type="module">
161
  import * as THREE from 'three';
162
 
 
163
  const sceneContainer = document.getElementById('scene-container');
164
  const storyTitleElement = document.getElementById('story-title');
165
  const storyContentElement = document.getElementById('story-content');
@@ -167,10 +164,11 @@
167
  const statsElement = document.getElementById('stats-display');
168
  const inventoryElement = document.getElementById('inventory-display');
169
 
 
170
  let scene, camera, renderer;
171
  let currentAssemblyGroup = null;
172
 
173
- // --- Materials --- (Unchanged)
174
  const stoneMaterial = new THREE.MeshStandardMaterial({ color: 0x888888, roughness: 0.8, metalness: 0.1 });
175
  const woodMaterial = new THREE.MeshStandardMaterial({ color: 0x8B4513, roughness: 0.7, metalness: 0 });
176
  const darkWoodMaterial = new THREE.MeshStandardMaterial({ color: 0x5C3D20, roughness: 0.7, metalness: 0 });
@@ -187,26 +185,37 @@
187
  const wetStoneMaterial = new THREE.MeshStandardMaterial({ color: 0x2F4F4F, roughness: 0.7 });
188
  const glowMaterial = new THREE.MeshStandardMaterial({ color: 0x00FFAA, emissive: 0x00FFAA, emissiveIntensity: 0.5 });
189
 
190
- // --- Three.js Setup --- (Unchanged initThreeJS, onWindowResize, animate, createMesh, createGroundPlane)
191
  function initThreeJS() {
192
- if (!sceneContainer) { console.error("Scene container not found!"); return; }
193
- scene = new THREE.Scene();
194
- scene.background = new THREE.Color(0x222222);
195
- const width = sceneContainer.clientWidth;
196
- const height = sceneContainer.clientHeight;
197
- camera = new THREE.PerspectiveCamera(75, (width / height) || 1, 0.1, 1000);
198
- camera.position.set(0, 2.5, 7);
199
- camera.lookAt(0, 0.5, 0);
200
- renderer = new THREE.WebGLRenderer({ antialias: true });
201
- renderer.setSize(width || 400, height || 300);
202
- renderer.shadowMap.enabled = true;
203
- renderer.shadowMap.type = THREE.PCFSoftShadowMap;
204
- sceneContainer.appendChild(renderer.domElement);
205
- const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
206
- scene.add(ambientLight);
207
- window.addEventListener('resize', onWindowResize, false);
208
- setTimeout(onWindowResize, 100); // Adjust size shortly after init
209
- animate();
 
 
 
 
 
 
 
 
 
 
 
210
  }
211
 
212
  function onWindowResize() {
@@ -217,18 +226,31 @@
217
  camera.aspect = width / height;
218
  camera.updateProjectionMatrix();
219
  renderer.setSize(width, height);
 
 
220
  }
221
  }
222
 
223
  function animate() {
224
- requestAnimationFrame(animate);
225
- const time = performance.now() * 0.001;
226
- scene.traverse(obj => {
227
- if (obj.userData.update) obj.userData.update(time);
228
- });
229
- if (renderer && scene && camera) {
230
- renderer.render(scene, camera);
231
- }
 
 
 
 
 
 
 
 
 
 
 
232
  }
233
 
234
  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 }) {
@@ -248,9 +270,8 @@
248
  return ground;
249
  }
250
 
251
-
252
- // --- Procedural Generation Functions --- (Unchanged create...Assembly functions)
253
- // [Keep all the create...Assembly functions from the previous listing here]
254
  // ... (createDefaultAssembly, createCityGatesAssembly, ..., createDarkCaveAssembly) ...
255
  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; }
256
  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; }
@@ -273,9 +294,8 @@
273
  function createHiddenCoveAssembly() { const group = new THREE.Group(); group.add(createGroundPlane(sandMaterial, 15)); const caveGeo = new THREE.BoxGeometry(3, 2.5, 3); const caveMat = new THREE.MeshStandardMaterial({ color: 0x111111 }); group.add(createMesh(caveGeo, caveMat, { z: -6, y: 1.25 })); const rockGeo = new THREE.SphereGeometry(0.5, 6, 6); const rockMat = wetStoneMaterial.clone(); for (let i = 0; i < 15; i++) { group.add(createMesh(rockGeo, rockMat, { x: (Math.random() - 0.5) * 12, y: 0.25, z: (Math.random() - 0.5) * 12 }, { y: Math.random() * Math.PI })); } const seaweedGeo = new THREE.ConeGeometry(0.2, 1.2, 6); const seaweedMat = leafMaterial.clone().set({ color: 0x1E4D2B }); for (let i = 0; i < 10; i++) { group.add(createMesh(seaweedGeo, seaweedMat, { x: (Math.random() - 0.5) * 10, y: 0.6, z: (Math.random() - 0.5) * 10 + 2 }, { x: (Math.random() - 0.5) * 0.2, z: (Math.random() - 0.5) * 0.2 })); } return group; }
274
  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; }
275
 
276
-
277
  // ========================================
278
- // Game Data
279
  // ========================================
280
  const itemsData = {
281
  "Flaming Sword": {type:"weapon", description:"A legendary blade, wreathed in magical fire.", goldValue: 500},
@@ -284,153 +304,125 @@
284
  "Healing Light Spell":{type:"spell", description:"A scroll containing the incantation to mend minor wounds.", goldValue: 50},
285
  "Shield of Faith Spell":{type:"spell",description:"A scroll containing a prayer that grants temporary magical protection.", goldValue: 75},
286
  "Binding Runes Scroll":{type:"spell", description:"Complex runes scribbled on parchment, said to temporarily immobilize a foe.", goldValue: 100},
287
- "Secret Tunnel Map": {type:"quest", description:"A crudely drawn map showing a hidden path, perhaps into the fortress?", goldValue: 5}, // Quest items low value?
288
  "Poison Daggers": {type:"weapon", description:"A pair of wicked-looking daggers coated in a fast-acting toxin.", goldValue: 150},
289
- "Master Key": {type:"quest", description:"An ornate key rumored to unlock many doors, though perhaps not all.", goldValue: 10},
290
  "Crude Dagger": {type:"weapon", description:"A roughly made dagger, chipped and stained.", goldValue: 10},
291
- "Scout's Pouch": {type:"quest", description:"A small leather pouch containing flint & steel, jerky, and some odd coins.", goldValue: 20} // Value includes contents
292
- // TODO: Add more items
293
  };
294
 
295
- const gameData = {
296
- // --- Start and Crossroads ---
297
- "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" },
298
-
299
- // --- Eastern Path: Hills -> Badlands ---
300
- "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" }, // TODO: Add Shepherd interaction branch
301
- "4": { title: "Hill Path Overlook", content: `<p>The path crests a hill, offering a panoramic view. To the east, the hills gradually give way to rugged, barren badlands. Nearby, nestled amongst wildflowers, you spot a small, ancient-looking shrine, heavily overgrown with vines.</p>`, options: [ { text: "Investigate the overgrown shrine", next: 40 }, { text: "Continue east towards the badlands", next: 41 } ], illustration: "hilltop-view-overgrown-shrine-wildflowers" },
302
- "40": { title: "Overgrown Shrine", content: `<p>Pushing aside thick vines reveals a small stone shrine dedicated to a forgotten nature deity. Intricate carvings, though worn, are still visible beneath the moss and grime. A sense of ancient peace emanates from the stones. Wildflowers grow in profusion around its base.</p>`, options: [{ text: "Examine the carvings for meaning (Intelligence Check)", check:{stat:'intelligence', dc:11, onFailure: 401}, next: 400 }, {text: "Leave the shrine undisturbed", next: 4}, {text: "Say a quiet prayer for guidance", next: 402}], illustration: "overgrown-stone-shrine-wildflowers-close" },
303
- "400": { title: "Shrine Insights", content:"<p>The carvings depict cycles of growth and renewal. You feel a sense of calm wash over you, slightly restoring your vitality. (+1 HP)</p>", options: [{text:"Continue towards the badlands", next: 41}], illustration: "overgrown-stone-shrine-wildflowers-close", reward: {hpGain: 1}}, // TODO: Implement hpGain
304
- "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"},
305
- "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"},
306
- "41": { title: "Rocky Badlands", content: `<p>The gentle green hills give way abruptly to cracked, sun-baked earth and jagged rock formations. The air is hot and still under a harsh, unforgiving sun. This land looks hostile and sparsely populated.</p>`, options: [{ text: "Scout ahead cautiously", next: 99 } ], illustration: "rocky-badlands-cracked-earth-harsh-sun" }, // TODO: Expand Badlands
307
-
308
- // --- Western Path: Cliffs -> Cove -> Cave ---
309
- "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" },
310
- "30": { title: "Hidden Cove", content: `<p>Your careful descent, whether via the main path or hidden steps, brings you safely to a secluded, sandy cove sheltered by the towering cliffs. The air smells strongly of salt and seaweed. Half-hidden in the shadows at the back of the cove is the dark, foreboding entrance to a sea cave.</p><p>(+25 XP)</p>`, options: [ { text: "Explore the dark cave", next: 35 } ], illustration: "hidden-cove-beach-dark-cave-entrance", reward: { xp: 25 } },
311
- "31": { title: "Tumbled Down", content: `<p>You lose your footing on the steep, treacherous path! You tumble and slide the last few feet, landing hard on the sandy cove floor. You take 5 points of damage from the fall. Shaking your head to clear it, you see the dark entrance to a sea cave nearby.</p>`, options: [ { text: "Gingerly get up and explore the dark cave", next: 35 } ], illustration: "character-fallen-at-bottom-of-cliff-path-cove", hpLoss: 5 },
312
- "32": { title: "No Easier Path", content: `<p>You scan the towering cliffs intently, searching for any alternative routes down. Despite your efforts, you find no obviously easier or safer paths than the precarious one directly before you.</p>`, options: [ { text: "Attempt the precarious descent again (Dexterity Check)", check: { stat: 'dexterity', dc: 12, onFailure: 31 }, next: 30 } ], illustration: "scanning-sea-cliffs-no-other-paths-visible" },
313
- "33": { title: "Smuggler's Steps?", content: `<p>Your keen eyes spot what others might miss: a series of barely visible handholds and footholds carved into the rock face, slightly hidden by an overhang. They look old but might offer a slightly less treacherous descent.</p><p>(+15 XP)</p>`, options: [ { text: "Use the hidden steps (Easier Dex Check)", check: { stat: 'dexterity', dc: 8, onFailure: 31 }, next: 30 } ], illustration: "close-up-handholds-carved-in-cliff-face", reward: { xp: 15 } },
314
- "35": { title: "Dark Cave", content: `<p>You cautiously enter the sea cave. The air inside is heavy with the smell of salt, damp rock, and something else... decay. Water drips rhythmically from unseen stalactites somewhere deeper within the oppressive darkness.</p>`, options: [{ text: "Press deeper into the darkness (Requires Light Source?)", next: 99 } ], illustration: "dark-cave-entrance-dripping-water" }, // TODO: Add light source requirement/check, expand cave
315
-
316
- // --- Northern Path: Forest -> Foothills -> Fortress ---
317
- "5": { title: "Shadowwood Entrance", content: `<p>The air grows cool and damp as you step beneath the dense canopy of the Shadowwood. Sunlight struggles to pierce the gloom, illuminating gnarled roots that writhe across the forest floor. A narrow, overgrown path leads deeper into the woods.</p>`, options: [ { text: "Follow the main, albeit overgrown, path", next: 6 }, { text: "Try to navigate through the lighter undergrowth beside the path", next: 7 }, { text: "Look for animal trails or signs of passage (Wisdom Check)", check: { stat: 'wisdom', dc: 10, onFailure: 6 }, next: 8 } ], illustration: "dark-forest-entrance-gnarled-roots-filtered-light" },
318
- // Forest Path Branches
319
- "6": { title: "Overgrown Forest Path", content: `<p>The path is barely visible beneath a thick layer of fallen leaves and creeping vines. Strange, faintly glowing fungi cling to rotting logs. You push deeper into the oppressive silence when suddenly, you hear a twig snap nearby!</p>`, options: [ { text: "Ready your weapon and investigate the sound", next: 10 }, { text: "Attempt to hide quietly amongst the ferns (Dexterity Check)", check: { stat: 'dexterity', dc: 11, onFailure: 10 }, next: 11 }, { text: "Call out cautiously, 'Who's there?'", next: 10 } ], illustration: "overgrown-forest-path-glowing-fungi-vines" },
320
- "7": { title: "Tangled Undergrowth", content: `<p>Pushing through thick ferns and thorny bushes proves difficult. You stumble into a small, unexpected clearing. In the center stands a weathered stone statue, its features eroded by time and covered in thick moss. It depicts a forgotten deity or hero.</p>`, options: [ { text: "Examine the statue closely for clues or markings (Intelligence Check)", check: { stat: 'intelligence', dc: 13, onFailure: 71 }, next: 70 }, { text: "Ignore the statue and try to find the main path again", next: 72 }, { text: "Leave a small offering (if you have something suitable)", next: 73 } ], illustration: "forest-clearing-mossy-statue-weathered-stone" },
321
- "8": { title: "Hidden Game Trail", content: `<p>Your sharp eyes spot a faint trail, almost invisible to the untrained observer, diverging from the main path. It looks like a route used by deer or other forest creatures. Following it, you soon arrive at the edge of a deep ravine spanned by a single, rickety rope bridge.</p><p>(+20 XP)</p>`, options: [ { text: "Risk crossing the rope bridge (Dexterity Check)", check: { stat: 'dexterity', dc: 10, onFailure: 81 }, next: 80 }, { text: "Search along the ravine edge for another way across", next: 82 } ], illustration: "narrow-game-trail-forest-rope-bridge-ravine", reward: { xp: 20 } },
322
- // Forest Encounters/Events
323
- "10": { title: "Goblin Ambush!", content: `<p>Suddenly, two scraggly goblins, clad in mismatched leather scraps and wielding crude, sharp spears, leap out from behind large toadstools! Their beady eyes fix on you with malicious intent.</p>`, options: [ { text: "Fight the goblins!", next: 12 }, { text: "Attempt to dodge past them and flee down the path (Dexterity Check)", check: { stat: 'dexterity', dc: 13, onFailure: 10 }, next: 13 } ], illustration: "two-goblins-ambush-forest-path-spears" }, // TODO: Implement combat
324
- "11": { title: "Hidden Evasion", content: `<p>Quickly and silently, you melt into the deep shadows beneath a large, ancient tree. The two goblins blunder past, bickering in their guttural tongue, completely oblivious to your presence.</p><p>(+30 XP)</p>`, options: [ { text: "Continue cautiously down the path once they are gone", next: 14 } ], illustration: "forest-shadows-hiding-goblins-walking-past", reward: { xp: 30 } },
325
- "12": { title: "Ambush Victory!", content: `<p>Though caught by surprise, you react swiftly. After a brief, vicious skirmish, the goblins lie defeated at your feet. Searching their meagre belongings, you find a single, Crude Dagger.</p><p>(+50 XP)</p>`, options: [ { text: "Wipe your blade clean and press onward", next: 14 } ], illustration: "defeated-goblins-forest-path-loot", reward: { xp: 50, addItem: "Crude Dagger" } },
326
- "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 } },
327
- "70": { title: "Statue's Secret", content:"<p>Running your fingers over the mossy stone, you find a small, almost invisible seam near the base. Applying pressure, a hidden compartment clicks open! Inside is a Scout's Pouch.</p><p>(+40 XP)</p>", options: [{text:"Take the pouch and press on", next: 72}], illustration: "forest-clearing-mossy-statue-hidden-compartment", reward:{xp: 40, addItem: "Scout's Pouch"}},
328
- "71": { title: "Just an Old Statue", content:"<p>Despite a careful examination, the statue appears to be just that – an old, weathered stone figure of no special significance that you can discern.</p>", options: [{text:"Ignore the statue and press on", next: 72}], illustration: "forest-clearing-mossy-statue-weathered-stone"},
329
- "72": { title: "Back to the Thicket", content:"<p>Leaving the clearing and the statue behind, you push back into the dense undergrowth, eventually relocating the main forest path.</p>", options: [{text:"Continue along the main path", next: 6}], illustration:"pushing-through-forest-undergrowth"},
330
- "73": { title: "A Small Offering", content:"<p>You place a small, simple offering at the statue's base (a ration, a coin, or perhaps just a moment of respect). You feel a subtle sense of approval or peace before turning to leave.</p>", options: [{text:"Try to find the main path again", next: 72}], illustration:"forest-clearing-mossy-statue-offering"}, // TODO: Check inventory for offering? Grant minor boon?
331
- "80": { title: "Across the Ravine", content:"<p>Taking a deep breath, you step onto the swaying rope bridge. With careful, deliberate steps, testing each plank before putting your weight on it, you make your way across the chasm to the other side.</p><p>(+25 XP)</p>", options: [{text:"Continue following the game trail", next: 14}], illustration:"character-crossing-rope-bridge-safely", reward:{xp:25}}, // Rejoins main path for now
332
- "81": { title: "Bridge Collapse!", content:"<p>Halfway across, a frayed rope snaps! The bridge lurches violently, sending you plunging into the ravine below! You lose 10 HP. Luckily, the bottom is covered in soft moss and mud, cushioning your fall.</p>", options: [{text:"Climb out and find another way", next: 82}], illustration:"rope-bridge-snapping-character-falling", hpLoss: 10},
333
- "82": { title: "Ravine Detour", content:"<p>Searching along the ravine's edge, you eventually find a place where the chasm narrows, and a fallen log provides a much safer, if longer, way across.</p>", options: [{text:"Cross the log bridge and continue", next: 14}], illustration:"fallen-log-crossing-ravine"}, // Rejoins main path for now
334
- // Forest Path Continues
335
- "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" },
336
- "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" },
337
- "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" },
338
- // Leaving the Forest
339
- "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" },
340
- // Foothills Path Branches
341
- "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" },
342
- "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 } },
343
- // Foothills Encounters/Events
344
- "19": { title: "Blocked Pass", content: `<p>As you round a sharp bend, your way is completely blocked by a recent rockslide! Huge boulders and debris choke the path, making further progress impossible along this route.</p>`, options: [ { text: "Try to climb over the unstable rockslide (Strength Check)", check: { stat: 'strength', dc: 14, onFailure: 191 }, next: 190 }, { text: "Search the surrounding cliffs for another way around", next: 192 } ], illustration: "rockslide-blocking-mountain-path-boulders" },
345
- "190": { title: "Over the Rocks", content:"<p>Summoning your strength, you find handholds and footholds, scrambling and pulling yourself up and over the precarious rockslide. It's exhausting work, but you make it past the blockage.</p><p>(+35 XP)</p>", options: [{text:"Continue up the now clear path", next: 22}], illustration:"character-climbing-over-boulders", reward: {xp:35} },
346
- "191": { title: "Climb Fails", content:"<p>The boulders are too large, too smooth, or too unstable. You try several approaches, but cannot safely climb over the rockslide. This way is blocked.</p>", options: [{text:"Search the surrounding cliffs for another way around", next: 192}], illustration:"character-slipping-on-rockslide-boulders"},
347
- "192": { title: "Detour Found", content:"<p>After considerable searching along the cliff face, you find a rough, overgrown path leading steeply up and around the rockslide area. It eventually rejoins the main trail further up the mountain.</p>", options: [{text:"Follow the detour path", next: 22}], illustration:"rough-detour-path-around-rockslide"},
348
- "20": { title: "Goat Trail", content: `<p>Your thorough search pays off! Partially hidden behind a cluster of hardy mountain shrubs, you discover a narrow trail, barely wide enough for a single person (or perhaps a mountain goat). It seems to bypass the main path, heading upwards towards the fortress.</p><p>(+40 XP)</p>`, options: [ { text: "Follow the precarious goat trail", next: 22 } ], illustration: "narrow-goat-trail-mountainside-fortress-view", reward: { xp: 40 } },
349
- "21": { title: "Western Ridge", content:"<p>The path along the western ridge is dangerously narrow and exposed. Loose gravel shifts underfoot, and strong gusts of wind whip around you, threatening to push you off the edge into the dizzying drop below.</p>", options: [{text:"Proceed carefully along the ridge (Dexterity Check)", check:{stat:'dexterity', dc: 14, onFailure: 211}, next: 22 } ], illustration:"narrow-windy-mountain-ridge-path" },
350
- "211": {title:"Lost Balance", content:"<p>A particularly strong gust of wind catches you at a bad moment! You lose your balance and stumble, tumbling down a steep, rocky slope before managing to arrest your fall. You lose 10 HP.</p>", options:[{text:"Climb back up and reconsider the main path", next: 17}], illustration:"character-falling-off-windy-ridge", hpLoss: 10},
351
- // Approaching/At the Fortress
352
- "22": { title: "Fortress Approach", content:"<p>You've navigated the treacherous paths and finally stand near the imposing outer walls of the dark mountain fortress. Stern-faced guards patrol the battlements, their eyes scanning the approaches. The main gate looks heavily fortified.</p>", options: [
353
- {text:"Search for a less obvious entrance (Wisdom Check)", check:{stat:'wisdom', dc: 16, onFailure: 221}, next: 220}, // TODO: Success leads to secret entrance page
354
- {text:"Attempt to bluff your way past the gate guards (Charisma Check)", check:{stat:'charisma', dc: 15, onFailure: 222}, next: 223}, // TODO: Success/Failure pages for bluff
355
- {text:"Try to sneak past the gate guards (Dexterity Check)", check:{stat:'dexterity', dc: 17, onFailure: 222}, next: 224}, // TODO: Success/Failure pages for sneak
356
- {text:"Retreat for now", next: 16} // Option to go back
357
- ], illustration:"approaching-dark-fortress-walls-guards"},
358
- "220": { title: "Secret Passage?", content:"<p>Your careful search reveals loose stones near the base of the wall, potentially hiding a passage!</p>", options: [{text:"Investigate the loose stones", next: 99}], illustration:"approaching-dark-fortress-walls-guards"}, // TODO: Expand secret passage
359
- "221": { title: "No Obvious Weakness", content:"<p>The fortress walls look solid and well-maintained. You find no obvious weak points or hidden entrances from this vantage point.</p>", options: [{text:"Reconsider your approach", next: 22}], illustration:"approaching-dark-fortress-walls-guards"},
360
- "222": { title: "Caught!", content:"<p>Your attempt fails miserably! The guards spot you immediately and raise the alarm! You are captured.</p>", options: [{text:"To the dungeons...", next: 99}], illustration:"approaching-dark-fortress-walls-guards"}, // TODO: Lead to prisoner cell?
361
- "223": { title: "Bluff Success?", content:"<p>Amazingly, your story seems plausible enough for the guards to let you pass through the gate!</p>", options: [{text:"Enter the fortress courtyard", next: 99}], illustration:"approaching-dark-fortress-walls-guards"}, // TODO: Expand fortress interior
362
- "224": { title: "Sneak Success?", content:"<p>Moving like a shadow, you manage to slip past the gate guards unnoticed!</p>", options: [{text:"Enter the fortress courtyard", next: 99}], illustration:"approaching-dark-fortress-walls-guards"}, // TODO: Expand fortress interior
363
-
364
- // --- Game Over / Error State ---
365
  "99": {
366
  title: "Game Over / To Be Continued...",
367
- content: "<p>Your adventure ends here... for now. You can sell unwanted items for gold before starting again.</p>", // Modified content
368
- // Options will be dynamically generated in renderPageInternal for selling
369
- options: [
370
- // The restart button will be added last by renderPageInternal logic
371
- ],
372
  illustration: "game-over-generic",
373
- gameOver: true, // Mark this page specifically for game over logic
374
- allowSell: true // Add a flag to enable selling on this page
375
  }
376
  };
377
 
378
-
379
  // ========================================
380
- // Game State
381
  // ========================================
382
- // Define default character state - used for first launch
383
  const defaultCharacterState = {
384
  name: "Hero", race: "Human", alignment: "Neutral Good", class: "Adventurer",
385
- level: 1, xp: 0, xpToNextLevel: 100, gold: 0, // Added gold
386
  stats: { strength: 8, intelligence: 10, wisdom: 10, dexterity: 10, constitution: 10, charisma: 8, hp: 12, maxHp: 12 },
387
  inventory: []
388
  };
389
-
390
- // Initialize gameState
391
  let gameState = {
392
  currentPageId: 1,
393
- // Deep copy default state initially to avoid modification issues
394
- character: JSON.parse(JSON.stringify(defaultCharacterState)),
395
- // Store sell feedback message temporarily
396
- lastSellMessage: ""
397
  };
398
 
399
-
400
  // ========================================
401
- // Game Logic Functions
402
  // ========================================
403
 
404
- // Function to start a brand new game (resets everything)
405
  function startNewGame() {
406
  console.log("Starting brand new game...");
407
- // Reset state completely using the default
408
  gameState = {
409
  currentPageId: 1,
410
- character: JSON.parse(JSON.stringify(defaultCharacterState)),
411
- lastSellMessage: ""
412
  };
413
- renderPage(gameState.currentPageId);
414
  }
415
 
416
- // Function to restart, keeping character progress ("New Game Plus")
417
  function restartGamePlus() {
418
  console.log("Restarting game (keeping progress)...");
419
- gameState.currentPageId = 1; // Only reset the page ID
420
- gameState.lastSellMessage = ""; // Clear any sell messages
421
- renderPage(gameState.currentPageId);
422
  }
423
 
424
  function handleChoiceClick(choiceData) {
425
  console.log("Choice clicked:", choiceData);
 
426
 
427
- // --- Special Actions (Sell, Restart) ---
428
  if (choiceData.action === 'restart_plus') {
429
  restartGamePlus();
430
  return;
431
  }
432
  if (choiceData.action === 'sell_item') {
433
- handleSellItem(choiceData.item);
 
 
 
434
  return;
435
  }
436
 
@@ -438,20 +430,17 @@
438
  const optionNextPageId = parseInt(choiceData.next);
439
  const itemToAdd = choiceData.addItem;
440
  let nextPageId = optionNextPageId;
441
- let rollResultMessage = gameState.lastSellMessage || ""; // Carry over sell message if any
442
- gameState.lastSellMessage = ""; // Clear sell message after use
443
  const check = choiceData.check;
444
 
445
- // --- Basic Input Validation ---
446
  if (isNaN(optionNextPageId) && !check) {
447
  console.error("Invalid choice data: Missing 'next' page ID and no check defined.", choiceData);
448
- const currentPageData = gameData[gameState.currentPageId] || gameData[99];
449
- renderPageInternal(gameState.currentPageId, currentPageData , "<p><em>Error: Invalid choice data encountered! Cannot proceed.</em></p>");
450
  choicesElement.querySelectorAll('button').forEach(b => b.disabled = true);
451
  return;
452
  }
453
 
454
- // --- Skill Check Logic ---
455
  if (check) {
456
  const statValue = gameState.character.stats[check.stat] || 10;
457
  const modifier = Math.floor((statValue - 10) / 2);
@@ -460,17 +449,16 @@
460
  const dc = check.dc;
461
  const statName = check.stat.charAt(0).toUpperCase() + check.stat.slice(1);
462
  console.log(`Check: ${statName} (DC ${dc}) | Roll: ${roll} + Mod: ${modifier} = ${totalResult}`);
463
-
464
- if (totalResult >= dc) { // Success
465
  nextPageId = optionNextPageId;
466
- rollResultMessage += `<p class="roll-success"><em>${statName} Check Success! (Rolled ${roll} + ${modifier} = ${totalResult} vs DC ${dc})</em></p>`;
467
- } else { // Failure
468
  nextPageId = parseInt(check.onFailure);
469
- rollResultMessage += `<p class="roll-failure"><em>${statName} Check Failed! (Rolled ${roll} + ${modifier} = ${totalResult} vs DC ${dc})</em></p>`;
470
  if (isNaN(nextPageId)) {
471
  console.error("Invalid onFailure ID:", check.onFailure);
472
  nextPageId = 99;
473
- rollResultMessage += "<p><em>Error: Invalid failure path defined!</em></p>";
474
  }
475
  }
476
  }
@@ -479,11 +467,11 @@
479
  const targetPageData = gameData[nextPageId];
480
  if (!targetPageData) {
481
  console.error(`Data for target page ${nextPageId} not found!`);
482
- renderPageInternal(99, gameData[99] || { title: "Error", content: "<p>Page Data Missing!</p>", illustration: "error", gameOver: true }, "<p><em>Error: Next page data missing! Cannot continue.</em></p>");
 
483
  return;
484
  }
485
 
486
- // Apply consequences/rewards defined on the *target* page
487
  let hpLostThisTurn = 0;
488
  if (targetPageData.hpLoss) {
489
  hpLostThisTurn = targetPageData.hpLoss;
@@ -496,23 +484,20 @@
496
  console.log(`Gained ${hpGained} HP.`);
497
  }
498
 
499
- // Check for death *after* applying HP changes
500
  if (gameState.character.stats.hp <= 0) {
501
  gameState.character.stats.hp = 0;
502
  console.log("Player died!");
503
- nextPageId = 99; // Force navigation to game over page
504
- rollResultMessage += `<p><em>You have succumbed to your injuries!${hpLostThisTurn > 0 ? ` (-${hpLostThisTurn} HP)` : ''}</em></p>`;
505
- const gameOverPageData = gameData[nextPageId];
506
- renderPageInternal(nextPageId, gameOverPageData, rollResultMessage);
507
- return; // Stop processing
508
  }
509
 
510
- // Apply other rewards if alive
511
  if (targetPageData.reward) {
512
  if (targetPageData.reward.xp) {
513
  gameState.character.xp += targetPageData.reward.xp;
514
  console.log(`Gained ${targetPageData.reward.xp} XP! Total: ${gameState.character.xp}`);
515
- // checkLevelUp(); // TODO
516
  }
517
  if (targetPageData.reward.statIncrease) {
518
  const stat = targetPageData.reward.statIncrease.stat;
@@ -524,131 +509,141 @@
524
  }
525
  }
526
  if (targetPageData.reward.addItem && !gameState.character.inventory.includes(targetPageData.reward.addItem)) {
527
- gameState.character.inventory.push(targetPageData.reward.addItem);
528
- console.log(`Found item: ${targetPageData.reward.addItem}`);
529
- rollResultMessage += `<p><em>Item acquired: ${targetPageData.reward.addItem}</em></p>`;
 
 
 
 
 
 
 
530
  }
531
  }
532
  if (itemToAdd && !gameState.character.inventory.includes(itemToAdd)) {
533
- gameState.character.inventory.push(itemToAdd);
534
- console.log("Added item:", itemToAdd);
535
- rollResultMessage += `<p><em>Item acquired: ${itemToAdd}</em></p>`;
 
 
 
 
 
536
  }
537
 
538
- // --- Update Game State ---
539
  gameState.currentPageId = nextPageId;
540
  recalculateMaxHp();
541
  gameState.character.stats.hp = Math.min(gameState.character.stats.hp, gameState.character.stats.maxHp);
542
 
543
  console.log("Transitioning to page:", nextPageId, " New state:", JSON.stringify(gameState));
544
- renderPageInternal(nextPageId, gameData[nextPageId], rollResultMessage);
545
  }
546
 
 
547
  function handleSellItem(itemName) {
548
  console.log("Attempting to sell:", itemName);
549
  const itemIndex = gameState.character.inventory.indexOf(itemName);
550
  const itemInfo = itemsData[itemName];
551
-
552
- if (itemIndex !== -1 && itemInfo && itemInfo.goldValue > 0) {
553
- const value = itemInfo.goldValue;
554
- gameState.character.gold += value;
555
- gameState.character.inventory.splice(itemIndex, 1); // Remove item
556
- gameState.lastSellMessage = `<p class="sell-feedback">Sold ${itemName} for ${value} Gold.</p>`;
557
- console.log(`Sold ${itemName} for ${value} gold. Current gold: ${gameState.character.gold}`);
558
- } else {
559
- console.warn("Could not sell item:", itemName, " - Item not found, no value, or invalid.");
560
- gameState.lastSellMessage = `<p class="roll-failure">Cannot sell ${itemName}.</p>`; // Use failure style for error feedback
561
- }
562
- // Re-render the current page (which should be the Game Over page '99')
563
- renderPageInternal(gameState.currentPageId, gameData[gameState.currentPageId], gameState.lastSellMessage);
 
 
 
 
 
 
 
 
 
 
 
564
  }
565
 
566
 
567
- function recalculateMaxHp() {
568
- const baseHp = 10; // Base HP for level 1 adventurer
569
  const conModifier = Math.floor((gameState.character.stats.constitution - 10) / 2);
570
- gameState.character.stats.maxHp = Math.max(1, baseHp + conModifier * gameState.character.level); // Ensure HP is at least 1
571
  }
572
 
573
- function renderPageInternal(pageId, pageData, message = "") {
574
  if (!pageData) {
575
  console.error(`Render Error: No data for page ${pageId}`);
576
- pageData = gameData[99] || { title: "Error", content: "<p>Render Error! Critical page data missing.</p>", illustration: "error", gameOver: true };
577
- message += "<p><em>Render Error: Page data was missing! Cannot proceed.</em></p>";
578
  pageId = 99;
579
  }
580
  console.log(`Rendering page ${pageId}: "${pageData.title}"`);
581
 
582
  storyTitleElement.textContent = pageData.title || "Untitled Page";
583
- // Inject message first, then page content
584
  storyContentElement.innerHTML = message + (pageData.content || "<p>...</p>");
585
 
586
  updateStatsDisplay();
587
  updateInventoryDisplay();
588
- choicesElement.innerHTML = ''; // Clear previous choices
589
 
590
  const options = pageData.options || [];
591
- const isGameOverPage = pageData.gameOver === true; // Specifically check the gameOver flag
592
 
593
- // --- Generate Sell Buttons (Only on Game Over page if allowSell is true) ---
594
  if (isGameOverPage && pageData.allowSell === true) {
595
  const sellableItems = gameState.character.inventory.filter(itemName => {
596
  const itemInfo = itemsData[itemName];
597
- return itemInfo && itemInfo.goldValue > 0 && itemInfo.type !== 'quest'; // Only sell non-quest items with value
 
598
  });
599
 
600
  if (sellableItems.length > 0) {
601
- choicesElement.innerHTML += `<h3>Sell Items:</h3>`; // Add a header for sell options
602
  sellableItems.forEach(itemName => {
 
603
  const itemInfo = itemsData[itemName];
 
 
604
  const sellButton = document.createElement('button');
605
- sellButton.classList.add('choice-button', 'sell-button'); // Add specific class
606
  sellButton.textContent = `Sell ${itemName} (${itemInfo.goldValue} Gold)`;
607
  sellButton.onclick = () => handleChoiceClick({ action: 'sell_item', item: itemName });
608
  choicesElement.appendChild(sellButton);
609
  });
610
- choicesElement.innerHTML += `<hr style="border-color: #555; margin: 10px 0;">`; // Separator
611
  }
612
  }
613
 
614
-
615
- // --- Generate Standard Choices / Restart Button ---
616
- if (!isGameOverPage && options.length > 0) { // Normal page with navigation choices
617
  options.forEach(option => {
618
  const button = document.createElement('button');
619
  button.classList.add('choice-button');
620
  button.textContent = option.text;
621
  let requirementMet = true;
622
  let requirementText = [];
623
-
624
- if (option.requireItem) {
625
- if (!gameState.character.inventory.includes(option.requireItem)) {
626
- requirementMet = false; requirementText.push(`Requires: ${option.requireItem}`);
627
- }
628
- }
629
- if (option.requireStat) {
630
- const currentStat = gameState.character.stats[option.requireStat.stat] || 0;
631
- if (currentStat < option.requireStat.value) {
632
- requirementMet = false; requirementText.push(`Requires: ${option.requireStat.stat.charAt(0).toUpperCase() + option.requireStat.stat.slice(1)} ${option.requireStat.value}`);
633
- }
634
- }
635
-
636
  button.disabled = !requirementMet;
637
  if (!requirementMet) button.title = requirementText.join(', ');
638
- else {
639
- const choiceData = { next: option.next, addItem: option.addItem, check: option.check };
640
- button.onclick = () => handleChoiceClick(choiceData);
641
- }
642
  choicesElement.appendChild(button);
643
  });
644
- } else if (isGameOverPage) { // Game Over page needs restart button
645
  const restartButton = document.createElement('button');
646
  restartButton.classList.add('choice-button');
647
  restartButton.textContent = "Restart Adventure (Keep Progress)";
648
- // Use the specific restart_plus action
649
  restartButton.onclick = () => handleChoiceClick({ action: 'restart_plus' });
650
  choicesElement.appendChild(restartButton);
651
- } else { // End of a branch (not game over page 99), offer restart
652
  choicesElement.insertAdjacentHTML('beforeend', '<p><i>There are no further paths from here.</i></p>');
653
  const restartButton = document.createElement('button');
654
  restartButton.classList.add('choice-button');
@@ -662,13 +657,12 @@
662
 
663
  function renderPage(pageId) { renderPageInternal(pageId, gameData[pageId]); }
664
 
665
- function updateStatsDisplay() {
666
  const char=gameState.character;
667
- // Added Gold display
668
  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>`;
669
  }
670
 
671
- function updateInventoryDisplay() { // Unchanged
672
  let h='<strong>Inventory:</strong> ';
673
  if(gameState.character.inventory.length === 0){
674
  h+='<em>Empty</em>';
@@ -683,36 +677,33 @@
683
  inventoryElement.innerHTML = h;
684
  }
685
 
686
- // --- Scene Update and Lighting --- (Unchanged updateScene, adjustLighting)
 
687
  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(); }
688
  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); }
689
 
690
- // --- Potential Future Improvements Comment --- (Unchanged)
691
  /* [Keep comment block here] */
692
 
693
  // ========================================
694
- // Initialization
695
  // ========================================
696
  document.addEventListener('DOMContentLoaded', () => {
697
  console.log("DOM Ready. Initializing game...");
698
- try {
699
- initThreeJS();
700
- if (scene && camera && renderer) {
701
- // Use startNewGame for the very first load
702
- startNewGame();
703
- console.log("Game Started Successfully.");
704
- } else {
705
- throw new Error("Three.js initialization failed or did not complete.");
706
- }
707
- } catch (error) {
708
- console.error("Initialization failed:", error);
709
- storyTitleElement.textContent = "Initialization Error";
710
- storyContentElement.innerHTML = `<p>A critical error occurred during setup. The adventure cannot begin. Please check the console (F12) for technical details.</p><pre>${error.stack || error}</pre>`;
711
- choicesElement.innerHTML = '<p style="color: #f77;">Cannot proceed due to initialization error.</p>';
712
- if (sceneContainer) {
713
- sceneContainer.innerHTML = '<p style="color: #f77; padding: 20px; font-size: 1.2em; text-align: center;">3D Scene Failed to Load</p>';
714
- }
715
- scene = null; camera = null; renderer = null;
716
  }
717
  });
718
  </script>
 
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
  #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; }
66
+ #story-content .feedback-message { font-style: italic; margin-top: 1em; padding-left: 8px; border-left: 3px solid #888; } /* Generic feedback */
67
+ #story-content .feedback-success { color: #9f9; border-left-color: #4a4;} /* Success feedback */
68
+ #story-content .feedback-error { color: #f99; border-left-color: #a44;} /* Error feedback */
69
+
70
 
71
  #stats-inventory-container {
72
  margin-bottom: 20px;
 
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
  .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; }
 
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');
 
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
  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() {
 
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 }) {
 
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; }
 
294
  function createHiddenCoveAssembly() { const group = new THREE.Group(); group.add(createGroundPlane(sandMaterial, 15)); const caveGeo = new THREE.BoxGeometry(3, 2.5, 3); const caveMat = new THREE.MeshStandardMaterial({ color: 0x111111 }); group.add(createMesh(caveGeo, caveMat, { z: -6, y: 1.25 })); const rockGeo = new THREE.SphereGeometry(0.5, 6, 6); const rockMat = wetStoneMaterial.clone(); for (let i = 0; i < 15; i++) { group.add(createMesh(rockGeo, rockMat, { x: (Math.random() - 0.5) * 12, y: 0.25, z: (Math.random() - 0.5) * 12 }, { y: Math.random() * Math.PI })); } const seaweedGeo = new THREE.ConeGeometry(0.2, 1.2, 6); const seaweedMat = leafMaterial.clone().set({ color: 0x1E4D2B }); for (let i = 0; i < 10; i++) { group.add(createMesh(seaweedGeo, seaweedMat, { x: (Math.random() - 0.5) * 10, y: 0.6, z: (Math.random() - 0.5) * 10 + 2 }, { x: (Math.random() - 0.5) * 0.2, z: (Math.random() - 0.5) * 0.2 })); } return group; }
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},
 
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" },
320
+ "4": { title: "Hill Path Overlook", content: `<p>The path crests a hill, offering a panoramic view. To the east, the hills gradually give way to rugged, barren badlands. Nearby, nestled amongst wildflowers, you spot a small, ancient-looking shrine, heavily overgrown with vines.</p>`, options: [ { text: "Investigate the overgrown shrine", next: 40 }, { text: "Continue east towards the badlands", next: 41 } ], illustration: "hilltop-view-overgrown-shrine-wildflowers" },
321
+ "5": { title: "Shadowwood Entrance", content: `<p>The air grows cool and damp as you step beneath the dense canopy of the Shadowwood. Sunlight struggles to pierce the gloom, illuminating gnarled roots that writhe across the forest floor. A narrow, overgrown path leads deeper into the woods.</p>`, options: [ { text: "Follow the main, albeit overgrown, path", next: 6 }, { text: "Try to navigate through the lighter undergrowth beside the path", next: 7 }, { text: "Look for animal trails or signs of passage (Wisdom Check)", check: { stat: 'wisdom', dc: 10, onFailure: 6 }, next: 8 } ], illustration: "dark-forest-entrance-gnarled-roots-filtered-light" },
322
+ "6": { title: "Overgrown Forest Path", content: `<p>The path is barely visible beneath a thick layer of fallen leaves and creeping vines. Strange, faintly glowing fungi cling to rotting logs. You push deeper into the oppressive silence when suddenly, you hear a twig snap nearby!</p>`, options: [ { text: "Ready your weapon and investigate the sound", next: 10 }, { text: "Attempt to hide quietly amongst the ferns (Dexterity Check)", check: { stat: 'dexterity', dc: 11, onFailure: 10 }, next: 11 }, { text: "Call out cautiously, 'Who's there?'", next: 10 } ], illustration: "overgrown-forest-path-glowing-fungi-vines" },
323
+ "7": { title: "Tangled Undergrowth", content: `<p>Pushing through thick ferns and thorny bushes proves difficult. You stumble into a small, unexpected clearing. In the center stands a weathered stone statue, its features eroded by time and covered in thick moss. It depicts a forgotten deity or hero.</p>`, options: [ { text: "Examine the statue closely for clues or markings (Intelligence Check)", check: { stat: 'intelligence', dc: 13, onFailure: 71 }, next: 70 }, { text: "Ignore the statue and try to find the main path again", next: 72 }, { text: "Leave a small offering (if you have something suitable)", next: 73 } ], illustration: "forest-clearing-mossy-statue-weathered-stone" },
324
+ "8": { title: "Hidden Game Trail", content: `<p>Your sharp eyes spot a faint trail, almost invisible to the untrained observer, diverging from the main path. It looks like a route used by deer or other forest creatures. Following it, you soon arrive at the edge of a deep ravine spanned by a single, rickety rope bridge.</p><p>(+20 XP)</p>`, options: [ { text: "Risk crossing the rope bridge (Dexterity Check)", check: { stat: 'dexterity', dc: 10, onFailure: 81 }, next: 80 }, { text: "Search along the ravine edge for another way across", next: 82 } ], illustration: "narrow-game-trail-forest-rope-bridge-ravine", reward: { xp: 20 } },
325
+ "10": { title: "Goblin Ambush!", content: `<p>Suddenly, two scraggly goblins, clad in mismatched leather scraps and wielding crude, sharp spears, leap out from behind large toadstools! Their beady eyes fix on you with malicious intent.</p>`, options: [ { text: "Fight the goblins!", next: 12 }, { text: "Attempt to dodge past them and flee down the path (Dexterity Check)", check: { stat: 'dexterity', dc: 13, onFailure: 10 }, next: 13 } ], illustration: "two-goblins-ambush-forest-path-spears" },
326
+ "11": { title: "Hidden Evasion", content: `<p>Quickly and silently, you melt into the deep shadows beneath a large, ancient tree. The two goblins blunder past, bickering in their guttural tongue, completely oblivious to your presence.</p><p>(+30 XP)</p>`, options: [ { text: "Continue cautiously down the path once they are gone", next: 14 } ], illustration: "forest-shadows-hiding-goblins-walking-past", reward: { xp: 30 } },
327
+ "12": { title: "Ambush Victory!", content: `<p>Though caught by surprise, you react swiftly. After a brief, vicious skirmish, the goblins lie defeated at your feet. Searching their meagre belongings, you find a single, Crude Dagger.</p><p>(+50 XP)</p>`, options: [ { text: "Wipe your blade clean and press onward", next: 14 } ], illustration: "defeated-goblins-forest-path-loot", reward: { xp: 50, addItem: "Crude Dagger" } },
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 } },
335
+ "19": { title: "Blocked Pass", content: `<p>As you round a sharp bend, your way is completely blocked by a recent rockslide! Huge boulders and debris choke the path, making further progress impossible along this route.</p>`, options: [ { text: "Try to climb over the unstable rockslide (Strength Check)", check: { stat: 'strength', dc: 14, onFailure: 191 }, next: 190 }, { text: "Search the surrounding cliffs for another way around", next: 192 } ], illustration: "rockslide-blocking-mountain-path-boulders" },
336
+ "20": { title: "Goat Trail", content: `<p>Your thorough search pays off! Partially hidden behind a cluster of hardy mountain shrubs, you discover a narrow trail, barely wide enough for a single person (or perhaps a mountain goat). It seems to bypass the main path, heading upwards towards the fortress.</p><p>(+40 XP)</p>`, options: [ { text: "Follow the precarious goat trail", next: 22 } ], illustration: "narrow-goat-trail-mountainside-fortress-view", reward: { xp: 40 } },
337
+ "21": { title: "Western Ridge", content:"<p>The path along the western ridge is dangerously narrow and exposed. Loose gravel shifts underfoot, and strong gusts of wind whip around you, threatening to push you off the edge into the dizzying drop below.</p>", options: [{text:"Proceed carefully along the ridge (Dexterity Check)", check:{stat:'dexterity', dc: 14, onFailure: 211}, next: 22 } ], illustration:"narrow-windy-mountain-ridge-path" },
338
+ "22": { title: "Fortress Approach", content:"<p>You've navigated the treacherous paths and finally stand near the imposing outer walls of the dark mountain fortress. Stern-faced guards patrol the battlements, their eyes scanning the approaches. The main gate looks heavily fortified.</p>", options: [ {text:"Search for a less obvious entrance (Wisdom Check)", check:{stat:'wisdom', dc: 16, onFailure: 221}, next: 220}, {text:"Attempt to bluff your way past the gate guards (Charisma Check)", check:{stat:'charisma', dc: 15, onFailure: 222}, next: 223}, {text:"Try to sneak past the gate guards (Dexterity Check)", check:{stat:'dexterity', dc: 17, onFailure: 222}, next: 224}, {text:"Retreat for now", next: 16} ], illustration:"approaching-dark-fortress-walls-guards"},
339
+ "30": { title: "Hidden Cove", content: `<p>Your careful descent, whether via the main path or hidden steps, brings you safely to a secluded, sandy cove sheltered by the towering cliffs. The air smells strongly of salt and seaweed. Half-hidden in the shadows at the back of the cove is the dark, foreboding entrance to a sea cave.</p><p>(+25 XP)</p>`, options: [ { text: "Explore the dark cave", next: 35 } ], illustration: "hidden-cove-beach-dark-cave-entrance", reward: { xp: 25 } },
340
+ "31": { title: "Tumbled Down", content: `<p>You lose your footing on the steep, treacherous path! You tumble and slide the last few feet, landing hard on the sandy cove floor. You take 5 points of damage from the fall. Shaking your head to clear it, you see the dark entrance to a sea cave nearby.</p>`, options: [ { text: "Gingerly get up and explore the dark cave", next: 35 } ], illustration: "character-fallen-at-bottom-of-cliff-path-cove", hpLoss: 5 },
341
+ "32": { title: "No Easier Path", content: `<p>You scan the towering cliffs intently, searching for any alternative routes down. Despite your efforts, you find no obviously easier or safer paths than the precarious one directly before you.</p>`, options: [ { text: "Attempt the precarious descent again (Dexterity Check)", check: { stat: 'dexterity', dc: 12, onFailure: 31 }, next: 30 } ], illustration: "scanning-sea-cliffs-no-other-paths-visible" },
342
+ "33": { title: "Smuggler's Steps?", content: `<p>Your keen eyes spot what others might miss: a series of barely visible handholds and footholds carved into the rock face, slightly hidden by an overhang. They look old but might offer a slightly less treacherous descent.</p><p>(+15 XP)</p>`, options: [ { text: "Use the hidden steps (Easier Dex Check)", check: { stat: 'dexterity', dc: 8, onFailure: 31 }, next: 30 } ], illustration: "close-up-handholds-carved-in-cliff-face", reward: { xp: 15 } },
343
+ "35": { title: "Dark Cave", content: `<p>You cautiously enter the sea cave. The air inside is heavy with the smell of salt, damp rock, and something else... decay. Water drips rhythmically from unseen stalactites somewhere deeper within the oppressive darkness.</p>`, options: [{ text: "Press deeper into the darkness (Requires Light Source?)", next: 99 } ], illustration: "dark-cave-entrance-dripping-water" },
344
+ "40": { title: "Overgrown Shrine", content: `<p>Pushing aside thick vines reveals a small stone shrine dedicated to a forgotten nature deity. Intricate carvings, though worn, are still visible beneath the moss and grime. A sense of ancient peace emanates from the stones. Wildflowers grow in profusion around its base.</p>`, options: [{ text: "Examine the carvings for meaning (Intelligence Check)", check:{stat:'intelligence', dc:11, onFailure: 401}, next: 400 }, {text: "Leave the shrine undisturbed", next: 4}, {text: "Say a quiet prayer for guidance", next: 402}], illustration: "overgrown-stone-shrine-wildflowers-close" },
345
+ "41": { title: "Rocky Badlands", content: `<p>The gentle green hills give way abruptly to cracked, sun-baked earth and jagged rock formations. The air is hot and still under a harsh, unforgiving sun. This land looks hostile and sparsely populated.</p>`, options: [{ text: "Scout ahead cautiously", next: 99 } ], illustration: "rocky-badlands-cracked-earth-harsh-sun" },
346
+ "70": { title: "Statue's Secret", content:"<p>Running your fingers over the mossy stone, you find a small, almost invisible seam near the base. Applying pressure, a hidden compartment clicks open! Inside is a Scout's Pouch.</p><p>(+40 XP)</p>", options: [{text:"Take the pouch and press on", next: 72}], illustration: "forest-clearing-mossy-statue-hidden-compartment", reward:{xp: 40, addItem: "Scout's Pouch"}},
347
+ "71": { title: "Just an Old Statue", content:"<p>Despite a careful examination, the statue appears to be just that – an old, weathered stone figure of no special significance that you can discern.</p>", options: [{text:"Ignore the statue and press on", next: 72}], illustration: "forest-clearing-mossy-statue-weathered-stone"},
348
+ "72": { title: "Back to the Thicket", content:"<p>Leaving the clearing and the statue behind, you push back into the dense undergrowth, eventually relocating the main forest path.</p>", options: [{text:"Continue along the main path", next: 6}], illustration:"pushing-through-forest-undergrowth"},
349
+ "73": { title: "A Small Offering", content:"<p>You place a small, simple offering at the statue's base (a ration, a coin, or perhaps just a moment of respect). You feel a subtle sense of approval or peace before turning to leave.</p>", options: [{text:"Try to find the main path again", next: 72}], illustration:"forest-clearing-mossy-statue-offering"},
350
+ "80": { title: "Across the Ravine", content:"<p>Taking a deep breath, you step onto the swaying rope bridge. With careful, deliberate steps, testing each plank before putting your weight on it, you make your way across the chasm to the other side.</p><p>(+25 XP)</p>", options: [{text:"Continue following the game trail", next: 14}], illustration:"character-crossing-rope-bridge-safely", reward:{xp:25}},
351
+ "81": { title: "Bridge Collapse!", content:"<p>Halfway across, a frayed rope snaps! The bridge lurches violently, sending you plunging into the ravine below! You lose 10 HP. Luckily, the bottom is covered in soft moss and mud, cushioning your fall.</p>", options: [{text:"Climb out and find another way", next: 82}], illustration:"rope-bridge-snapping-character-falling", hpLoss: 10},
352
+ "82": { title: "Ravine Detour", content:"<p>Searching along the ravine's edge, you eventually find a place where the chasm narrows, and a fallen log provides a much safer, if longer, way across.</p>", options: [{text:"Cross the log bridge and continue", next: 14}], illustration:"fallen-log-crossing-ravine"},
353
+ "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" },
354
+ "190": { title: "Over the Rocks", content:"<p>Summoning your strength, you find handholds and footholds, scrambling and pulling yourself up and over the precarious rockslide. It's exhausting work, but you make it past the blockage.</p><p>(+35 XP)</p>", options: [{text:"Continue up the now clear path", next: 22}], illustration:"character-climbing-over-boulders", reward: {xp:35} },
355
+ "191": { title: "Climb Fails", content:"<p>The boulders are too large, too smooth, or too unstable. You try several approaches, but cannot safely climb over the rockslide. This way is blocked.</p>", options: [{text:"Search the surrounding cliffs for another way around", next: 192}], illustration:"character-slipping-on-rockslide-boulders"},
356
+ "192": { title: "Detour Found", content:"<p>After considerable searching along the cliff face, you find a rough, overgrown path leading steeply up and around the rockslide area. It eventually rejoins the main trail further up the mountain.</p>", options: [{text:"Follow the detour path", next: 22}], illustration:"rough-detour-path-around-rockslide"},
357
+ "211": {title:"Lost Balance", content:"<p>A particularly strong gust of wind catches you at a bad moment! You lose your balance and stumble, tumbling down a steep, rocky slope before managing to arrest your fall. You lose 10 HP.</p>", options:[{text:"Climb back up and reconsider the main path", next: 17}], illustration:"character-falling-off-windy-ridge", hpLoss: 10},
358
+ "220": { title: "Secret Passage?", content:"<p>Your careful search reveals loose stones near the base of the wall, potentially hiding a passage!</p>", options: [{text:"Investigate the loose stones", next: 99}], illustration:"approaching-dark-fortress-walls-guards"},
359
+ "221": { title: "No Obvious Weakness", content:"<p>The fortress walls look solid and well-maintained. You find no obvious weak points or hidden entrances from this vantage point.</p>", options: [{text:"Reconsider your approach", next: 22}], illustration:"approaching-dark-fortress-walls-guards"},
360
+ "222": { title: "Caught!", content:"<p>Your attempt fails miserably! The guards spot you immediately and raise the alarm! You are captured.</p>", options: [{text:"To the dungeons...", next: 99}], illustration:"approaching-dark-fortress-walls-guards"},
361
+ "223": { title: "Bluff Success?", content:"<p>Amazingly, your story seems plausible enough for the guards to let you pass through the gate!</p>", options: [{text:"Enter the fortress courtyard", next: 99}], illustration:"approaching-dark-fortress-walls-guards"},
362
+ "224": { title: "Sneak Success?", content:"<p>Moving like a shadow, you manage to slip past the gate guards unnoticed!</p>", options: [{text:"Enter the fortress courtyard", next: 99}], illustration:"approaching-dark-fortress-walls-guards"},
363
+ "400": { title: "Shrine Insights", content:"<p>The carvings depict cycles of growth and renewal. You feel a sense of calm wash over you, slightly restoring your vitality. (+1 HP)</p>", options: [{text:"Continue towards the badlands", next: 41}], illustration: "overgrown-stone-shrine-wildflowers-close", reward: {hpGain: 1}},
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",
383
+ level: 1, xp: 0, xpToNextLevel: 100, gold: 0,
384
  stats: { strength: 8, intelligence: 10, wisdom: 10, dexterity: 10, constitution: 10, charisma: 8, hp: 12, maxHp: 12 },
385
  inventory: []
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
 
 
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);
 
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
  }
 
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;
 
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;
 
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');
 
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>';
 
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>`;
703
+ choicesElement.innerHTML = '<p style="color: #f77;">Cannot proceed due to initialization error.</p>';
704
+ if (sceneContainer) {
705
+ sceneContainer.innerHTML = '<p style="color: #f77; padding: 20px; font-size: 1.2em; text-align: center;">3D Scene Failed to Load</p>';
706
+ }
 
 
 
 
707
  }
708
  });
709
  </script>