awacke1 commited on
Commit
9c3a2d1
·
verified ·
1 Parent(s): 1aff277

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +348 -324
index.html CHANGED
@@ -3,38 +3,33 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Wacky D&D Shapes Adventure!</title>
7
  <style>
8
- /* --- Base Styles --- */
9
  body { font-family: 'Verdana', sans-serif; background-color: #2a3a4a; color: #f0f0f0; margin: 0; padding: 0; overflow: hidden; display: flex; flex-direction: column; height: 100vh; }
10
  #game-container { display: flex; flex-grow: 1; overflow: hidden; }
11
  #scene-container { flex-grow: 3; position: relative; border-right: 3px solid #506070; min-width: 250px; background-color: #101820; height: 100%; box-sizing: border-box; overflow: hidden; cursor: default; }
12
  #ui-container { flex-grow: 2; padding: 25px; overflow-y: auto; background-color: #3a4a5a; min-width: 320px; height: 100%; box-sizing: border-box; display: flex; flex-direction: column; }
13
  #scene-container canvas { display: block; }
14
-
15
- /* --- UI Elements --- */
16
  #story-title { color: #ffd700; margin: 0 0 15px 0; padding-bottom: 10px; border-bottom: 2px solid #506070; font-size: 1.8em; font-weight: bold; text-shadow: 1px 1px 2px #111; }
17
  #story-content { margin-bottom: 25px; line-height: 1.7; flex-grow: 1; font-size: 1.1em; color: #e8e8e8;}
18
  #stats-inventory-container { margin-bottom: 25px; padding: 15px; border: 1px solid #506070; border-radius: 5px; background-color: #4a5a6a; font-size: 0.95em; }
19
  #stats-display, #inventory-display { margin-bottom: 10px; line-height: 1.8; }
20
- #stats-display span { display: inline-block; background-color: #5a6a7a; padding: 4px 10px; border-radius: 15px; margin: 0 8px 5px 0; border: 1px solid #7a8a9a; white-space: nowrap; box-shadow: inset 0 1px 2px rgba(0,0,0,0.2); }
21
- #inventory-display .item-tag { display: inline-block; background-color: #5a6a7a; padding: 4px 10px; border-radius: 15px; margin: 0 8px 5px 0; border: 1px solid #7a8a9a; white-space: nowrap; box-shadow: inset 0 1px 2px rgba(0,0,0,0.2); cursor: default; }
22
  #stats-display strong, #inventory-display strong { color: #e0e0e0; margin-right: 6px; font-weight:bold; }
23
  #inventory-display em { color: #aabbcc; font-style: normal; }
24
  .item-tool { background-color: #a0522d; border-color: #cd853f; color: #fff;}
25
  .item-key { background-color: #ffd700; border-color: #f0e68c; color: #333;}
26
  .item-treasure { background-color: #20b2aa; border-color: #48d1cc; color: #fff;}
27
  .item-food { background-color: #ff6347; border-color: #fa8072; color: #fff;}
28
- .item-quest { background-color: #da70d6; border-color: #ee82ee; color: #fff;} /* Orchid/Violet */
29
  .item-unknown { background-color: #778899; border-color: #b0c4de;}
30
-
31
- /* --- Choices & Messages --- */
32
  #choices-container { margin-top: auto; padding-top: 20px; border-top: 2px solid #506070; }
33
  #choices-container h3 { margin-top: 0; margin-bottom: 12px; color: #e0e0e0; font-size: 1.2em; font-weight: bold;}
34
- #choices { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 15px;} /* Grid for nav */
35
- #action-choices { display: flex; flex-direction: column; gap: 12px; margin-top: 15px; border-top: 1px dashed #607080; padding-top: 15px;} /* Separate area for actions */
36
  .choice-button { display: block; width: 100%; padding: 13px 16px; margin-bottom: 0; background-color: #607080; color: #fff; border: 1px solid #8090a0; border-radius: 5px; cursor: pointer; text-align: center; font-family: 'Verdana', sans-serif; font-size: 1.0em; font-weight: bold; transition: background-color 0.2s, transform 0.1s; box-sizing: border-box; letter-spacing: 0.5px; }
37
- .choice-button.action { text-align: left; grid-column: span 2;} /* Action buttons span full width */
38
  .choice-button:hover:not(:disabled) { background-color: #ffc107; color: #222; border-color: #ffca2c; transform: translateY(-1px); }
39
  .choice-button:disabled { background-color: #4e5a66; color: #8a9aab; cursor: not-allowed; border-color: #607080; opacity: 0.6; transform: none; box-shadow: none;}
40
  .choice-button[title]:disabled::after { content: ' (' attr(title) ')'; font-style: italic; font-size: 0.9em; margin-left: 5px; }
@@ -48,8 +43,6 @@
48
  .message-combat { color: #f98; border-left-color: #c64; font-weight: bold;}
49
  .combat-button { background-color: #a33; border-color: #c66; color: #fff; font-weight: bold; text-align: center;}
50
  .combat-button:hover:not(:disabled) { background-color: #d44; border-color: #f88;}
51
-
52
- /* --- Action Info --- */
53
  #action-info { position: absolute; bottom: 10px; left: 10px; background-color: rgba(0,0,0,0.7); color: #ffcc66; padding: 5px 10px; border-radius: 3px; font-size: 0.9em; display: block; z-index: 10;}
54
  </style>
55
  </head>
@@ -60,7 +53,7 @@
60
  </div>
61
  <div id="ui-container">
62
  <h2 id="story-title">Initializing...</h2>
63
- <div id="story-content"><p>Loading world...</p></div>
64
  <div id="stats-inventory-container">
65
  <div id="stats-display">Loading Stats...</div>
66
  <div id="inventory-display">Inventory: ...</div>
@@ -82,9 +75,9 @@
82
 
83
  <script type="module">
84
  import * as THREE from 'three';
 
85
  import { FontLoader } from 'three/addons/loaders/FontLoader.js';
86
  import { TextGeometry } from 'three/addons/geometries/TextGeometry.js';
87
- // Orbit Controls removed again for simplicity focus
88
 
89
  console.log("Script module execution started.");
90
 
@@ -92,8 +85,8 @@
92
  const sceneContainer = document.getElementById('scene-container');
93
  const storyTitleElement = document.getElementById('story-title');
94
  const storyContentElement = document.getElementById('story-content');
95
- const choicesElement = document.getElementById('choices'); // Nav buttons grid
96
- const actionChoicesElement = document.getElementById('action-choices'); // Action buttons list
97
  const statsElement = document.getElementById('stats-display');
98
  const inventoryElement = document.getElementById('inventory-display');
99
  const actionInfoElement = document.getElementById('action-info');
@@ -101,11 +94,12 @@
101
  console.log("DOM elements obtained.");
102
 
103
  // --- Core Three.js Variables ---
104
- let scene, camera, renderer, clock;
105
  let currentSceneGroup = null;
106
  let currentLights = [];
107
  let threeFont = null;
108
  let currentMessage = "";
 
109
 
110
  // --- Materials ---
111
  const MAT = {
@@ -126,8 +120,8 @@
126
  sky_blue: new THREE.MeshStandardMaterial({ color: 0x03a9f4, roughness: 0.5 }),
127
  town_wood: new THREE.MeshStandardMaterial({ color: 0xae8a63, roughness: 0.7 }),
128
  town_roof: new THREE.MeshStandardMaterial({ color: 0x8b4513, roughness: 0.8 }),
129
- goblin_skin: new THREE.MeshStandardMaterial({ color: 0x8FBC8F, roughness: 0.8 }), // DarkSeaGreen
130
- text_material: new THREE.MeshBasicMaterial({ color: 0xffddaa }),
131
  };
132
 
133
  // --- Game State ---
@@ -140,16 +134,18 @@
140
  "Goblin's Favorite Sock": {type:"quest", description:"Smells... unique."},
141
  "Shiny Rock": {type:"treasure", description:"Distractingly shiny."},
142
  "Suspicious Mushroom": {type:"food", description:"Maybe... just maybe..."},
 
143
  };
144
 
145
  // --- Enemy Data ---
146
  const enemyData = {
147
  'goblin': { name: "Grumpy Goblin", hp: 12, defense: 12, attackBonus: 1, damageDice: 4, xp: 25, drops: ["Goblin's Favorite Sock", "Shiny Rock"] },
148
- // Add more enemies later
 
149
  };
150
 
151
- // --- Procedural Assembly Shapes ---
152
- const SHAPE_GENERATORS = {
153
  'pointy_cone': (size) => new THREE.ConeGeometry(size * 0.5, size * 1.2, 6 + Math.floor(Math.random() * 5)),
154
  'round_blob': (size) => new THREE.SphereGeometry(size * 0.6, 12, 8),
155
  'spinny_torus': (size) => new THREE.TorusGeometry(size * 0.5, size * 0.15, 8, 16),
@@ -172,134 +168,139 @@
172
  };
173
  const shapeKeys = Object.keys(SHAPE_GENERATORS);
174
 
175
- // --- Game Data (Page-Based, Expanded Story) ---
176
- const gameData = {
177
- 1: {
178
  title: "Snoring Meadows",
179
- content: "<p>You wake up in a field of gently snoring grass. Yup, snoring. To the North are some Wiggly Woods. East leads to the Clanky Caves entrance.</p>",
180
  options: [
181
- { text: "Enter Wiggly Woods (North)", next: 2 },
182
- { text: "Approach Clanky Caves (East)", next: 5 },
183
- { text: "Poke the snoring grass (Wisdom Check DC 10)", check: { stat: 'wisdom', dc: 10, next: 10, onFailure: 11 } }
184
  ],
185
  assemblyParams: { baseShape: 'ground_grass', baseSize: 30, mainShapes: ['squashed_ball', 'pointy_cone'], accents: [], count: 30, scaleRange: [0.5, 1.2], colorTheme: [MAT.grass, MAT.leaf_green, MAT.sky_blue], arrangement: 'scatter' }
186
  },
187
- 2: {
188
  title: "Wiggly Woods",
189
- content: "<p>These trees really do wiggle! It's making you a bit dizzy. A grumpy-looking goblin blocks the path deeper into the woods (South?).</p>",
190
  options: [
191
- { text: "Talk to the Goblin", next: 3 },
192
- { text: "Try to sneak past (Dexterity Check DC 12)", check: { stat: 'dexterity', dc: 12, next: 4, onFailure: 3 } },
193
- { text: "Go Back (South)", next: 1 }
194
  ],
195
  assemblyParams: { baseShape: 'ground_dirt', baseSize: 25, mainShapes: ['tall_cylinder', 'knotty_thing'], accents: ['leaf_autumn'], count: 20, scaleRange: [1.0, 3.0], colorTheme: [MAT.wood_dark, MAT.leaf_autumn, MAT.deep_purple], arrangement: 'cluster', clusterRadius: 10 }
196
  },
197
- 3: {
198
- title: "Grumpy Goblin",
199
- content: "<p>'Oi! You wanna pass?' grunts the goblin, pointing a wobbly spear. 'Dis my wood! Whatcha gonna do?'</p>",
200
  options: [
201
- { text: "Fight the Grumpy Goblin!", action: 'triggerCombat', enemy: 'goblin', nextOnWin: 4, nextOnLoss: 98 },
202
- { text: "Offer him a Shiny Rock (Requires Shiny Rock)", requires: "Shiny Rock", consume: true, next: 4, reward: {xp: 10} },
203
- { text: "Try to reason with him (Charisma Check DC 14)", check: { stat: 'charisma', dc: 14, next: 4, onFailure: 12 } },
204
- { text: "Run Away!", next: 2 }
205
  ],
206
- assemblyParams: { // Similar to woods, maybe add goblin representation
207
- baseShape: 'ground_dirt', baseSize: 25,
208
- mainShapes: ['tall_cylinder', 'knotty_thing'], accents: ['leaf_autumn', 'basic_tetra'],
209
- count: 20, scaleRange: [1.0, 3.0],
210
- colorTheme: [MAT.wood_dark, MAT.leaf_autumn, MAT.deep_purple, MAT.goblin_skin],
211
- arrangement: 'cluster', clusterRadius: 10
212
- }
213
  },
214
- 4: {
215
- title: "Past the Goblin!",
216
- content: "<p>You somehow got past the goblin! The path continues North to a weirdly Square Clearing. Or you can go back South.</p>",
217
  options: [
218
- { text: "Go to Square Clearing (North)", next: 7 },
219
- { text: "Go Back (South)", next: 2 }
220
  ],
221
- assemblyParams: { baseShape: 'ground_dirt', baseSize: 25, mainShapes: ['tall_cylinder'], accents: ['leaf_green'], count: 15, scaleRange: [1.0, 2.5], colorTheme: [MAT.wood_light, MAT.leaf_green], arrangement: 'scatter' }
222
  },
223
- 5: {
224
  title: "Clanky Caves Entrance",
225
- content: "<p>The entrance to the cave clanks and whirs softly. It smells like old metal and damp socks. A sign reads 'Beware of... Clanks?'</p>",
226
- options: [ { text: "Enter the Caves", next: 8 }, { text: "Go Back West", next: 1 } ],
227
- assemblyParams: { baseShape: 'stone_grey', baseSize: 20, mainShapes: ['spiky_ball', 'spinny_torus'], accents: ['metal_rusty', 'basic_tetra'], count: 25, scaleRange: [0.5, 1.8], colorTheme: [MAT.stone_grey, MAT.metal_rusty], arrangement: 'cluster', clusterRadius: 8 }
228
  },
229
- 7: { // Added North from page 4
 
 
 
 
 
 
230
  title: "The Square Clearing",
231
- content: "<p>This clearing is unnervingly square. Even the rocks look cubic! In the center is a perfectly square chest with a wobbly-looking lock.</p>",
232
  options: [
233
- { text: "Try the Wobbly Key (Requires Wobbly Key)", requires: "Wobbly Key", consume: true, next: 13 },
234
- { text: "Whack it with a Stick (Requires Sturdy Stick)", requires: "Sturdy Stick", next: 14 },
235
- { text: "Go Back South", next: 4 }
236
  ],
237
- assemblyParams: { baseShape: 'ground_grass', baseSize: 20, mainShapes: ['boxy_chunk', 'holed_box'], accents: [], count: 15, scaleRange: [0.8, 1.5], colorTheme: [MAT.stone_brown, MAT.grass], arrangement: 'patch', patchRadius: 8 }
238
  },
239
  8: { // Inside Clanky Caves
240
  title: "Clanky Caves - Junction",
241
- content: "<p>Clank! Whirr! Sproing! The cave is full of noisy, spinning metal things. Passages lead West and East.</p>",
242
- options: [ { text: "Go West (Deeper?)", next: 9 }, { text: "Go East (Exit?)", next: 5 } ],
243
- assemblyParams: { baseShape: 'stone_grey', baseSize: 25, mainShapes: ['spinny_torus', 'holed_box'], accents: ['metal_shiny', 'basic_tetra'], count: 30, scaleRange: [0.6, 2.0], colorTheme: [MAT.stone_grey, MAT.metal_shiny, MAT.metal_rusty, MAT.bright_yellow], arrangement: 'cluster', clusterRadius: 10 }
244
  },
245
- 9: { // Deeper Cave
246
  title: "Sprocket Stash",
247
- content: "<p>You found a pile of shiny sprockets! And maybe some suspicious mushrooms growing nearby.</p>",
248
- options: [ { text: "Take a Sprocket!", reward: { addItem: "Shiny Sprocket", xp: 10 }, next: 8 }, { text: "Eat a Mushroom?", next: 15 }, { text: "Go Back East", next: 8 } ],
249
- assemblyParams: { baseShape: 'stone_grey', baseSize: 15, mainShapes: ['gem_shape'], accents: ['spinny_torus', 'metal_shiny'], count: 20, scaleRange: [0.4, 1.0], colorTheme: [MAT.metal_shiny, MAT.bright_yellow, MAT.deep_purple], arrangement: 'patch', patchRadius: 4 }
250
  },
251
  10: { // Success poking grass
252
  title: "Grass Tickled!",
253
- content: "<p>You gently poke the snoring grass. It giggles and coughs up a Wobbly Key! Weird.</p>",
254
- options: [ { text: "Grab the Key!", reward: { addItem: "Wobbly Key", xp: 5 }, next: 1 } ],
255
  assemblyParams: { baseShape: 'ground_grass', baseSize: 30, mainShapes: ['squashed_ball', 'pointy_cone'], accents: [], count: 30, scaleRange: [0.5, 1.2], colorTheme: [MAT.grass, MAT.leaf_green, MAT.sky_blue], arrangement: 'scatter' }
256
  },
257
  11: { // Fail poking grass
258
- title: "Grass Bites Back!",
259
- content: "<p>Ouch! You poked too hard. The grass blade snapped back like a tiny whip! (-1 HP)</p>",
260
- options: [ { text: "Okay, fine, leave it alone.", next: 1 } ],
261
- hpLoss: 1,
262
  assemblyParams: { baseShape: 'ground_grass', baseSize: 30, mainShapes: ['squashed_ball', 'pointy_cone'], accents: [], count: 30, scaleRange: [0.5, 1.2], colorTheme: [MAT.grass, MAT.leaf_green, MAT.sky_blue], arrangement: 'scatter' }
263
  },
264
  12: { // Fail charisma check on goblin
265
- title: "Goblin Unimpressed",
266
- content: "<p>'Nice try, smooth-talker!' the goblin sneers, waving his spear more menacingly. 'Now what?'</p>",
267
  options: [
268
- { text: "Fight him!", action: 'triggerCombat', enemy: 'goblin', nextOnWin: 4, nextOnLoss: 98 },
269
- { text: "Offer a Shiny Rock (If you have one)", requires: "Shiny Rock", consume: true, next: 4, reward: {xp: 10} },
270
- { text: "Run Away!", next: 2 }
271
  ],
272
  assemblyParams: { baseShape: 'ground_dirt', baseSize: 25, mainShapes: ['tall_cylinder', 'knotty_thing'], accents: ['leaf_autumn', 'basic_tetra'], count: 20, scaleRange: [1.0, 3.0], colorTheme: [MAT.wood_dark, MAT.leaf_autumn, MAT.deep_purple, MAT.goblin_skin], arrangement: 'cluster', clusterRadius: 10 }
273
  },
274
  13: { // Open chest with key
275
- title: "Chest Opens!",
276
- content: "<p>Click! The wobbly key fits the wobbly lock! Inside you find... another Shiny Rock?</p>",
277
- options: [ { text: "Take the rock!", reward: { addItem: "Shiny Rock", xp: 15 }, next: 7 } ],
278
- assemblyParams: { baseShape: 'ground_grass', baseSize: 20, mainShapes: ['boxy_chunk', 'holed_box'], accents: [], count: 15, scaleRange: [0.8, 1.5], colorTheme: [MAT.stone_brown, MAT.grass], arrangement: 'patch', patchRadius: 8 }
 
 
 
 
 
 
279
  },
280
- 14: { // Whack chest
281
- title: "Thwack!",
282
- content: "<p>You whack the square chest with your Sturdy Stick. It makes a dull *thud*. Nothing happens, but the stick feels slightly less sturdy.</p>",
283
- options: [ { text: "Try the Wobbly Key (Requires Wobbly Key)", requires: "Wobbly Key", consume: true, next: 13 }, { text: "Give up and go back", next: 7 } ],
284
- // Maybe add consequence: Sturdy Stick breaks? Requires more state tracking.
285
- assemblyParams: { baseShape: 'ground_grass', baseSize: 20, mainShapes: ['boxy_chunk', 'holed_box'], accents: [], count: 15, scaleRange: [0.8, 1.5], colorTheme: [MAT.stone_brown, MAT.grass], arrangement: 'patch', patchRadius: 8 }
286
  },
287
- 15: { // Eat Mushroom
288
- title: "Gulp!",
289
- content: "<p>You bravely (or foolishly?) eat the Suspicious Mushroom. Your vision goes purple for a second, then... you feel... bouncy! (+5 Max HP! HP Restored!)</p>",
290
- options: [ { text: "Weird! Go back.", next: 8 } ],
291
- reward: { statGain: { maxHp: 5 }, hpGain: 5, xp: 5 }, // Example stat gain
292
  assemblyParams: { baseShape: 'stone_grey', baseSize: 15, mainShapes: ['gem_shape'], accents: ['spinny_torus', 'metal_shiny'], count: 20, scaleRange: [0.4, 1.0], colorTheme: [MAT.metal_shiny, MAT.bright_yellow, MAT.deep_purple], arrangement: 'patch', patchRadius: 4 }
293
  },
294
  98: { // Lose combat
295
- title: "Oof!",
296
- content: "<p>You were defeated! You wake up back where you started, feeling dizzy.</p>",
297
- options: [ { text: "Okay...", next: 1 } ],
298
- assemblyParams: { baseShape: 'ground_grass', count: 5, mainShapes:['round_blob'], colorTheme:[MAT.grass]} // Simple scene
299
  },
300
  99: { // Generic End/TBC
301
- title: "Adventure Paused!",
302
- content: "<p>That's as far as this path goes for now. What an adventure!</p>",
303
  options: [ { text: "Start Over?", next: 1 } ],
304
  assemblyParams: { baseShape: 'flat_plate', baseSize: 10, mainShapes: ['basic_tetra'], count: 5, scaleRange: [1, 2], colorTheme: [MAT.sky_blue, MAT.bright_yellow], arrangement: 'center_stack', stackHeight: 3 }
305
  }
@@ -327,11 +328,11 @@
327
  renderer.shadowMap.type = THREE.PCFSoftShadowMap;
328
  sceneContainer.appendChild(renderer.domElement);
329
 
330
- // OrbitControls removed for focus on page-based nav
331
  // controls = new OrbitControls(camera, renderer.domElement);
332
 
333
  window.addEventListener('resize', onWindowResize, false);
334
- renderer.domElement.addEventListener('click', onMouseClick, false); // For pickup
335
  setTimeout(onWindowResize, 100);
336
  animate();
337
  console.log("initThreeJS finished.");
@@ -340,14 +341,15 @@
340
  function loadFontAndStart() {
341
  console.log("Loading font...");
342
  const loader = new FontLoader();
 
343
  loader.load('https://unpkg.com/[email protected]/examples/fonts/helvetiker_regular.typeface.json', function (font) {
344
  threeFont = font;
345
  console.log("Font loaded.");
346
  startGame();
347
  }, undefined, function (error) {
348
  console.error('Font loading failed:', error);
349
- threeFont = null; // Ensure font is null if failed
350
- startGame(); // Start anyway
351
  });
352
  }
353
 
@@ -364,11 +366,10 @@
364
  }
365
 
366
  function onMouseClick( event ) {
367
- // Calculate mouse position in normalized device coordinates (-1 to +1)
368
  const rect = renderer.domElement.getBoundingClientRect();
369
  mouse.x = ( (event.clientX - rect.left) / rect.width ) * 2 - 1;
370
  mouse.y = - ( (event.clientY - rect.top) / rect.height ) * 2 + 1;
371
- pickupItem(); // Only pickup is enabled via click now
372
  }
373
 
374
 
@@ -376,8 +377,7 @@
376
  requestAnimationFrame(animate);
377
  const delta = clock.getDelta();
378
  const time = clock.getElapsedTime();
379
-
380
- // controls?.update(); // OrbitControls removed
381
 
382
  if (currentSceneGroup) {
383
  currentSceneGroup.traverse(obj => {
@@ -397,23 +397,23 @@
397
  return mesh;
398
  }
399
 
400
- function setupLighting(type = 'default') {
401
  currentLights.forEach(light => { if (light.parent) light.parent.remove(light); if(scene.children.includes(light)) scene.remove(light); });
402
  currentLights = [];
403
 
404
- const ambientLight = new THREE.AmbientLight(0xffffff, 0.7); // Brighter ambient for kid-friendly
405
  scene.add(ambientLight);
406
  currentLights.push(ambientLight);
407
 
408
- const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2); // Brighter directional
409
- directionalLight.position.set(8, 12, 10);
410
  directionalLight.castShadow = true;
411
  directionalLight.shadow.mapSize.set(1024, 1024);
412
- directionalLight.shadow.camera.near = 0.5; directionalLight.shadow.camera.far = 50;
413
  const sb = 25;
414
  directionalLight.shadow.camera.left = -sb; directionalLight.shadow.camera.right = sb;
415
  directionalLight.shadow.camera.top = sb; directionalLight.shadow.camera.bottom = -sb;
416
- directionalLight.shadow.bias = -0.0005;
417
  scene.add(directionalLight);
418
  currentLights.push(directionalLight);
419
  }
@@ -439,19 +439,19 @@
439
  baseMesh = new THREE.Mesh(groundGeo, groundMat);
440
  baseMesh.rotation.x = -Math.PI / 2; baseMesh.position.y = 0;
441
  baseMesh.receiveShadow = true; baseMesh.castShadow = false;
442
- baseMesh.userData.isGround = true; // Mark for placement/raycasting
443
  group.add(baseMesh);
444
  } else {
445
  const baseGeoFunc = SHAPE_GENERATORS[baseShape] || SHAPE_GENERATORS['flat_plate'];
446
  const baseGeo = baseGeoFunc(baseSize);
447
  baseMesh = createMesh(baseGeo, colorTheme[0] || MAT.stone_grey, {y:0.1});
448
  baseMesh.receiveShadow = true; baseMesh.castShadow = false;
449
- baseMesh.userData.isGround = true; // Allow placing on non-plane bases
450
  group.add(baseMesh);
451
  }
452
 
453
  const allShapes = [...mainShapes, ...accents];
454
- let lastY = 0.1; // Start stacking slightly above base
455
  let stackCount = 0;
456
 
457
  for (let i = 0; i < count; i++) {
@@ -462,68 +462,61 @@
462
 
463
  const scaleFactor = scaleRange[0] + Math.random() * (scaleRange[1] - scaleRange[0]);
464
  const geometry = geoFunc(scaleFactor);
 
 
465
  const material = colorTheme[Math.floor(Math.random() * colorTheme.length)];
466
  const mesh = createMesh(geometry, material);
467
 
468
- geometry.computeBoundingBox(); // Needed for height calculation
 
 
 
469
  let height = 0;
470
  if (geometry.boundingBox) {
471
  height = (geometry.boundingBox.max.y - geometry.boundingBox.min.y) * mesh.scale.y;
472
  } else {
473
- console.warn("Geometry missing boundingBox", geometry);
474
- height = scaleFactor; // Estimate height if bbox failed
475
  }
476
- height = Math.max(0.1, height); // Ensure minimum height
477
 
478
 
479
  let position = { x:0, y:0, z:0 };
480
- let isValidPosition = false; // Flag to check if position calculation succeeded
481
 
482
  switch(arrangement) {
483
- case 'stack':
484
- if (stackCount < stackHeight) {
485
- position.y = lastY + height / 2;
486
- position.x = (Math.random() - 0.5) * 0.5 * scaleFactor;
487
- position.z = (Math.random() - 0.5) * 0.5 * scaleFactor;
488
- lastY += height * 0.9;
489
- stackCount++;
490
- isValidPosition = true;
491
- }
492
- break;
493
- case 'center_stack':
494
  if (stackCount < stackHeight) {
495
- position.y = lastY + height / 2;
496
- lastY += height * 0.95;
497
- stackCount++;
498
- isValidPosition = true;
499
- }
500
- break;
501
- case 'cluster':
502
- const angle = Math.random() * Math.PI * 2;
503
- const radius = Math.random() * clusterRadius;
504
- position.x = Math.cos(angle) * radius;
505
- position.z = Math.sin(angle) * radius;
506
- position.y = height / 2;
507
- isValidPosition = true;
508
- break;
509
- case 'patch':
510
- const pAngle = Math.random() * Math.PI * 2;
511
- const pRadius = Math.random() * patchRadius;
512
- position.x = patchPos.x + Math.cos(pAngle) * pRadius;
513
- position.z = patchPos.z + Math.sin(pAngle) * pRadius;
514
- position.y = (patchPos.y || 0) + height / 2;
515
- isValidPosition = true;
516
- break;
517
- case 'scatter':
518
- default:
519
- position.x = (Math.random() - 0.5) * baseSize * 0.8;
520
- position.z = (Math.random() - 0.5) * baseSize * 0.8;
521
- position.y = height / 2;
522
- isValidPosition = true;
523
- break;
524
  }
525
 
526
- if (!isValidPosition) continue; // Skip if arrangement logic failed (e.g., stack full)
527
 
528
  mesh.position.set(position.x, position.y, position.z);
529
  mesh.rotation.set(
@@ -544,22 +537,26 @@
544
  }
545
  group.add(mesh);
546
  }
547
- console.log(`Assembly created with ${group.children.length -1} procedural objects.`); // -1 for base
548
  return group;
549
  }
550
 
551
  function updateScene(assemblyParams) {
552
  console.log("updateScene called");
 
 
 
553
  if (currentSceneGroup) {
554
- currentSceneGroup.traverse(child => {
555
- if (child.isMesh) {
556
- if(child.geometry) child.geometry.dispose();
557
- }
558
- });
559
- scene.remove(currentSceneGroup);
 
560
  currentSceneGroup = null;
561
  }
562
- setupLighting('default'); // Use default lighting for simplicity now
563
 
564
  if (!assemblyParams) {
565
  console.warn("No assemblyParams provided, creating default scene.");
@@ -572,10 +569,10 @@
572
  console.log("New scene group added.");
573
  } catch (error) {
574
  console.error("Error creating procedural assembly:", error);
575
- // Add error indicator?
576
  }
577
  }
578
 
 
579
  function startGame() {
580
  console.log("startGame called.");
581
  const defaultChar = {
@@ -586,59 +583,64 @@
586
  gameState = {
587
  currentPageId: 1,
588
  character: JSON.parse(JSON.stringify(defaultChar)),
589
- combat: null // Ensure combat is null at start
590
  };
591
  renderPage(gameState.currentPageId);
592
  console.log("startGame finished.");
593
  }
594
 
595
- function handleChoiceClick(choiceData) {
596
- console.log("Choice clicked:", choiceData);
597
  currentMessage = "";
598
 
599
- let nextPageId = parseInt(choiceData.next);
600
- const currentPageData = gameData[gameState.currentPageId];
601
- const option = currentPageData?.options?.find(opt => opt.next === nextPageId || (opt.action === choiceData.action && opt.enemy === choiceData.enemy)); // Find the specific option
 
 
 
602
 
603
- // 1. Check Requirements
604
- if (option && option.requires) {
605
- if (!gameState.character.inventory.includes(option.requires)) {
606
- currentMessage = `<p class="message message-warning">You need a ${option.requires} for that!</p>`;
607
- renderPage(gameState.currentPageId); // Re-render current page with message
608
- return;
609
- }
610
- if (option.consume) {
611
- gameState.character.inventory = gameState.character.inventory.filter(i => i !== option.requires);
612
- currentMessage += `<p class="message message-item">Used your ${option.requires}.</p>`;
613
- }
614
  }
615
 
616
- // 2. Handle Actions (Combat, Checks) before transitioning
617
- if (option && option.action === 'triggerCombat' && option.enemy) {
618
  startCombat(option.enemy, option.nextOnWin, option.nextOnLoss);
619
- return; // Combat handles the next transition
620
- }
621
- if (option && option.check) {
622
  performSkillCheck(option.check, option.next, option.onFailure);
623
- return; // Skill check handles the next transition
624
- }
 
 
 
 
 
 
 
 
 
625
 
626
- // 3. Apply Rewards (if navigating directly)
627
- if (option && option.reward) {
628
- applyReward(option.reward);
629
- }
630
 
631
- // 4. Transition to Next Page (if not handled by action/check)
632
- const targetPageData = gameData[nextPageId];
633
- if (!targetPageData) {
634
- console.error(`Invalid next page ID: ${nextPageId}`);
635
- currentMessage += `<p class="message message-warning">That path is mysteriously blocked!</p>`;
636
- nextPageId = gameState.currentPageId;
637
- }
638
 
639
- gameState.currentPageId = nextPageId;
640
- renderPage(nextPageId);
 
 
 
641
  }
 
642
 
643
  function performSkillCheck(checkData, successPageId, failurePageId) {
644
  const {stat, dc} = checkData;
@@ -646,11 +648,11 @@
646
  const modifier = Math.floor((baseStat - 10) / 2);
647
  const roll = Math.floor(Math.random() * 20) + 1;
648
  const total = roll + modifier;
649
- const success = total >= dc;
650
 
651
  console.log(`Skill Check ${stat}: Roll ${roll} + Mod ${modifier} = ${total} vs DC ${dc}`);
652
  currentMessage += `<p class="message message-info"><em>Rolling ${stat}... (Rolled ${total} vs DC ${dc})</em></p>`;
653
- displayDiceRoll(roll, success); // Show the roll
654
 
655
  if (success) {
656
  currentMessage += `<p class="message message-success"><em>Success!</em></p>`;
@@ -659,111 +661,131 @@
659
  currentMessage += `<p class="message message-failure"><em>Failed!</em></p>`;
660
  gameState.currentPageId = failurePageId;
661
  }
662
- // Add short delay before rendering next page to see dice roll
663
- setTimeout(() => renderPage(gameState.currentPageId), 1500);
 
664
  }
665
 
666
  function applyReward(rewardData) {
 
 
667
  if(rewardData.addItem && itemsData[rewardData.addItem]) {
668
  if (!gameState.character.inventory.includes(rewardData.addItem)) {
669
  gameState.character.inventory.push(rewardData.addItem);
670
- currentMessage += `<p class="message message-item">You found a ${rewardData.addItem}!</p>`;
671
  } else {
672
- currentMessage += `<p class="message message-info">You found another ${rewardData.addItem}, but can't carry more.</p>`;
673
  }
674
  }
675
  if(rewardData.xp) {
676
  gameState.character.stats.xp += rewardData.xp;
677
- currentMessage += `<p class="message message-xp">You gained ${rewardData.xp} XP!</p>`;
678
  }
679
  if(rewardData.hpGain) {
680
  gameState.character.stats.hp = Math.min(gameState.character.stats.maxHp, gameState.character.stats.hp + rewardData.hpGain);
681
- currentMessage += `<p class="message message-success">You feel better! (+${rewardData.hpGain} HP)</p>`;
682
  }
683
- if(rewardData.statGain) { // Example: { statGain: { maxHp: 5 } }
684
  for (const stat in rewardData.statGain) {
685
  if (gameState.character.stats.hasOwnProperty(stat)) {
686
- gameState.character.stats[stat] += rewardData.statGain[stat];
687
- currentMessage += `<p class="message message-success">Your ${stat} increased by ${rewardData.statGain[stat]}!</p>`;
688
- // If maxHp increases, maybe heal too?
689
- if (stat === 'maxHp' && rewardData.statGain[stat] > 0) {
690
- gameState.character.stats.hp += rewardData.statGain[stat];
691
  }
 
692
  }
693
  }
694
  }
695
  }
696
 
697
-
698
  function renderPage(pageId) {
699
  console.log(`Rendering page ${pageId}`);
700
  const pageData = gameData[pageId];
701
 
702
- if (!pageData) { /* Error handling as before */ return; }
703
 
704
  storyTitleElement.textContent = pageData.title || "An Unnamed Place";
705
  storyContentElement.innerHTML = currentMessage + (pageData.content || "<p>...</p>");
706
  choicesElement.innerHTML = '';
707
- actionChoicesElement.innerHTML = ''; // Clear action choices too
708
 
709
- // Setup Navigation Buttons (Always show, disable if unavailable)
710
- const neighbors = getZoneNeighbors(gameState.currentZoneId || getZoneId(1,1)); // Use placeholder if needed
711
- const directions = {'north': 'North', 'south': 'South', 'east': 'East', 'west': 'West'};
712
- for(const dir in directions) {
713
- const neighborId = neighbors[dir]; // Note: This uses grid logic, pageData uses .next
714
  const button = document.createElement('button');
715
  button.classList.add('choice-button');
716
- button.textContent = `Go ${directions[dir]}`;
717
- // For now, navigation uses pageData.options, not grid neighbors
718
- button.disabled = true; // Disable all grid nav by default
 
 
 
 
 
 
 
 
 
 
719
  choicesElement.appendChild(button);
720
- }
721
-
722
 
723
- // Add options from pageData to Action Choices area
724
- if (pageData.options && pageData.options.length > 0) {
725
- pageData.options.forEach(option => {
726
- const button = document.createElement('button');
727
- button.classList.add('choice-button', 'action'); // Mark as action
728
- button.textContent = option.text;
729
 
730
- // Disable if required item is missing
731
- let requirementMet = true;
732
- if (option.requires && !gameState.character.inventory.includes(option.requires)) {
733
- requirementMet = false;
734
- button.title = `Requires: ${option.requires}`;
735
- }
736
- button.disabled = !requirementMet;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
737
 
738
- button.onclick = () => handleChoiceClick(option); // Pass full option data
739
- actionChoicesElement.appendChild(button);
740
- });
741
- } else {
742
- actionChoicesElement.innerHTML = '<p><i>Nothing else to do here right now.</i></p>';
 
 
743
  }
744
 
 
745
  updateStatsDisplay();
746
  updateInventoryDisplay();
747
  updateActionInfo();
748
  updateScene(pageData.assemblyParams);
749
  }
750
- window.handleChoiceClick = handleChoiceClick; // Make global
751
-
752
 
753
  function updateStatsDisplay() {
754
- if (!gameState.character || !statsElement) return;
755
- const stats = gameState.character.stats;
756
- const hpColor = stats.hp / stats.maxHp < 0.3 ? '#f88' : (stats.hp / stats.maxHp < 0.6 ? '#fd5' : '#8f8');
757
- const statBonus = (statVal) => {
758
- const mod = Math.floor((statVal - 10) / 2);
759
- return `${statVal} (${mod >= 0 ? '+' : ''}${mod})`;
760
- };
761
-
762
- statsElement.innerHTML = `<strong>Stats:</strong>
763
- <span style="color:${hpColor}">HP: ${stats.hp}/${stats.maxHp}</span> <span>XP: ${stats.xp}</span><br>
764
- <span>Str: ${statBonus(stats.strength)}</span> <span>Dex: ${statBonus(stats.dexterity)}</span> <span>Con: ${statBonus(stats.constitution)}</span>
765
- <span>Int: ${statBonus(stats.intelligence)}</span> <span>Wis: ${statBonus(stats.wisdom)}</span> <span>Cha: ${statBonus(stats.charisma)}</span>`;
766
- }
767
 
768
  function updateInventoryDisplay() {
769
  if (!gameState.character || !inventoryElement) return;
@@ -786,25 +808,25 @@
786
  actionInfoElement.textContent = `Location: ${gameData[gameState.currentPageId]?.title || 'Unknown'} | ${mode}`;
787
  }
788
 
789
- // --- Combat ---
790
  function startCombat(enemyTypeId, nextOnWin, nextOnLoss) {
791
  const enemyBase = enemyData[enemyTypeId];
792
  if (!enemyBase) { console.error("Unknown enemy type:", enemyTypeId); return; }
793
  gameState.combat = {
794
  active: true, enemyId: enemyTypeId, enemyName: enemyBase.name,
795
  enemyHp: enemyBase.hp, enemyMaxHp: enemyBase.hp,
796
- enemyDefense: enemyBase.defense, enemyAttackBonus: 2, // Simplified bonus
797
  enemyDamageDice: enemyBase.damageDice || 4, enemyXp: enemyBase.xp,
798
  enemyDrops: enemyBase.drops || [],
799
  nextOnWin: nextOnWin, nextOnLoss: nextOnLoss
800
  };
801
- currentMessage = `<p class="message message-combat">A ${enemyBase.name} blocks your path!</p>`;
802
- renderCurrentPageUI(); // Update UI to show combat mode
803
  }
804
 
805
  function handleCombatAction(action) {
806
  if (!gameState.combat?.active) return;
807
- currentMessage = ""; // Clear previous messages
808
 
809
  if (action === 'attack') {
810
  // Player Attack
@@ -812,33 +834,32 @@
812
  const playerAtkBonus = Math.floor((gameState.character.stats.strength - 10) / 2);
813
  const playerTotalAttack = playerRoll + playerAtkBonus;
814
  const playerHit = playerRoll === 20 || (playerRoll !== 1 && playerTotalAttack >= gameState.combat.enemyDefense);
815
-
816
- displayDiceRoll(playerRoll, playerHit); // Show roll
817
 
818
  if (playerHit) {
819
  const weapon = gameState.character.inventory.find(i => itemsData[i]?.type === 'weapon');
820
- const baseDamage = itemsData[weapon]?.baseDamage || 2; // Unarmed = 1d2 ?
821
- const damageRoll = Math.max(1, Math.floor(Math.random() * baseDamage) + 1 + playerAtkBonus);
822
  gameState.combat.enemyHp -= damageRoll;
823
- currentMessage += `<p class="message message-success">You hit the ${gameState.combat.enemyName} for ${damageRoll} damage! (Rolled ${playerTotalAttack} vs AC ${gameState.combat.enemyDefense})</p>`;
824
  } else {
825
- currentMessage += `<p class="message message-failure">You missed the ${gameState.combat.enemyName}. (Rolled ${playerTotalAttack} vs AC ${gameState.combat.enemyDefense})</p>`;
826
  }
827
 
828
  // Check Enemy Defeat
829
  if (gameState.combat.enemyHp <= 0) {
830
  currentMessage += `<p class="message message-success"><b>You defeated the ${gameState.combat.enemyName}!</b></p>`;
831
- applyReward({ xp: gameState.combat.enemyXp });
832
- // Handle Drops (Simplified: drop one random item from list)
833
  if (gameState.combat.enemyDrops.length > 0) {
834
  const droppedItemName = gameState.combat.enemyDrops[Math.floor(Math.random() * gameState.combat.enemyDrops.length)];
835
  dropItemInScene(droppedItemName, new THREE.Vector3(Math.random()*2-1, 0.2, Math.random()*2-1));
836
  currentMessage += `<p class="message message-item"><em>The ${gameState.combat.enemyName} dropped a ${droppedItemName}! Click it to pick up.</em></p>`;
837
  }
838
  const winPage = gameState.combat.nextOnWin;
839
- gameState.combat = null; // End combat
840
- gameState.currentPageId = winPage; // Go to win page
841
- renderPage(gameState.currentPageId);
842
  return;
843
  }
844
 
@@ -848,8 +869,7 @@
848
  const playerAC = 10 + Math.floor((gameState.character.stats.dexterity - 10) / 2);
849
  const enemyHit = enemyRoll === 20 || (enemyRoll !== 1 && enemyTotalAttack >= playerAC);
850
 
851
- // Add slight delay for enemy roll display maybe?
852
- setTimeout(() => displayDiceRoll(enemyRoll, enemyHit), 500); // Display enemy roll slightly later
853
 
854
  if (enemyHit) {
855
  const damageRoll = Math.max(1, Math.floor(Math.random() * gameState.combat.enemyDamageDice) + 1);
@@ -858,44 +878,44 @@
858
  if (gameState.character.stats.hp <= 0) {
859
  currentMessage += `<p class="message message-failure"><b>You have been defeated!</b></p>`;
860
  const lossPage = gameState.combat.nextOnLoss;
861
- gameState.combat = null; // End combat
862
- gameState.currentPageId = lossPage; // Go to loss page
863
- setTimeout(() => renderPage(gameState.currentPageId), 1500); // Delay transition after showing roll
864
  return;
865
  }
866
  } else {
867
  currentMessage += `<p class="message message-info">The ${gameState.combat.enemyName} misses you. (Rolled ${enemyTotalAttack} vs AC ${playerAC})</p>`;
868
  }
869
- setTimeout(() => renderPage(gameState.currentPageId), 1500); // Re-render UI after delay
870
  }
871
  }
872
- window.handleCombatAction = handleCombatAction; // Make global
873
 
874
- function displayDiceRoll(result, success) {
875
- if (!threeFont) { console.warn("Font not loaded for dice roll."); return; }
876
  activeTimeouts.forEach(timeoutId => clearTimeout(timeoutId)); activeTimeouts = [];
877
- scene.children.filter(c => c.userData?.isDiceRoll).forEach(c => scene.remove(c)); // Remove old rolls from main scene
878
 
879
  const textGeo = new TextGeometry(result.toString(), { font: threeFont, size: 1.0, height: 0.1, curveSegments: 4 });
880
- textGeo.computeBoundingBox();
881
- const textWidth = textGeo.boundingBox.max.x - textGeo.boundingBox.min.x;
882
  const textMat = MAT.text_material.clone();
883
  textMat.color.setHex(success ? 0x88ff88 : 0xff8888);
884
- textMat.transparent = true; // Needed for opacity fade
885
 
886
  const textMesh = new THREE.Mesh(textGeo, textMat);
887
  textMesh.userData.isDiceRoll = true;
888
 
889
- const distance = 6;
890
- const textPos = camera.position.clone().add(camera.getWorldDirection(new THREE.Vector3()).multiplyScalar(distance));
 
891
  textMesh.position.copy(textPos);
892
- textMesh.position.y += 1.0; // Adjust vertical position
893
- textMesh.position.x -= textWidth / 2 * textMesh.scale.x; // Center horizontally
894
- textMesh.quaternion.copy(camera.quaternion);
895
 
896
  scene.add(textMesh);
897
 
898
- const duration = 2.5; // seconds
 
899
  const startTime = clock.getElapsedTime();
900
  textMesh.userData.startTime = startTime;
901
  textMesh.userData.update = (time) => {
@@ -903,19 +923,23 @@
903
  if (elapsed >= duration) {
904
  if (textMesh.parent) textMesh.parent.remove(textMesh);
905
  delete textMesh.userData.update;
 
906
  } else {
907
- textMesh.position.y += 0.01;
908
- textMesh.material.opacity = 1.0 - (elapsed / duration);
 
 
909
  }
910
  };
911
- }
 
912
 
913
  function dropItemInScene(itemName, positionOffset = new THREE.Vector3(0, 0, 0)) {
914
- const currentGroup = currentSceneGroup; // Items are dropped relative to the current scene group origin
915
  if (!currentGroup || !itemsData[itemName]) return;
916
 
917
  const itemDef = itemsData[itemName];
918
- const itemGeo = new THREE.BoxGeometry(0.4, 0.4, 0.4); // Simple representation
919
  const itemMat = MAT.simple.clone();
920
  if(itemDef.type === 'weapon') itemMat.color.setHex(0xcc6666);
921
  else if(itemDef.type === 'consumable') itemMat.color.setHex(0xcc9966);
@@ -929,15 +953,15 @@
929
  console.log(`Dropped ${itemName} in scene`);
930
  }
931
 
932
- function pickupItem() { // Re-enabled pickup
933
- if (gameState.combat?.active) return; // No pickup during combat
934
 
935
  raycaster.setFromCamera(mouse, camera);
936
  const currentGroup = currentSceneGroup;
937
  if (!currentGroup) return;
938
 
939
  const pickupables = [];
940
- currentGroup.traverseVisible(child => { // Check only visible objects in current group
941
  if (child.userData.isPickupable) pickupables.push(child);
942
  });
943
 
@@ -954,20 +978,18 @@
954
  if (!gameState.character.inventory.includes(itemName)) {
955
  gameState.character.inventory.push(itemName);
956
  } else {
957
- currentMessage += `<p class="message message-info"><em>(Cannot carry more ${itemName})</em></p>`;
958
  }
959
 
960
- // Remove object fully after pickup
961
  if(clickedObject.parent) clickedObject.parent.remove(clickedObject);
962
  if(clickedObject.geometry) clickedObject.geometry.dispose();
963
- // We assume material is shared from MAT, so don't dispose
964
 
965
- renderCurrentPageUI(); // Update inventory and messages
966
  }
967
  }
968
  }
969
 
970
-
971
  document.addEventListener('DOMContentLoaded', () => {
972
  console.log("DOM Ready - Initializing Wacky D&D Shapes Adventure!");
973
  try {
@@ -980,8 +1002,10 @@
980
  storyTitleElement.textContent = "Initialization Error";
981
  storyContentElement.innerHTML = `<p style="color:red;">Failed to start game:</p><pre style="color:red; white-space: pre-wrap;">${error.stack || error}</pre><p style="color:yellow;">Check console (F12) for details.</p>`;
982
  if(sceneContainer) sceneContainer.innerHTML = '<p style="color:red; padding: 20px;">3D Scene Failed</p>';
983
- document.getElementById('stats-inventory-container').style.display = 'none';
984
- document.getElementById('choices-container').style.display = 'none';
 
 
985
  }
986
  });
987
 
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Wacky D&D Shapes Adventure! (Var Fix)</title>
7
  <style>
8
+ /* --- Base Styles (Same as before) --- */
9
  body { font-family: 'Verdana', sans-serif; background-color: #2a3a4a; color: #f0f0f0; margin: 0; padding: 0; overflow: hidden; display: flex; flex-direction: column; height: 100vh; }
10
  #game-container { display: flex; flex-grow: 1; overflow: hidden; }
11
  #scene-container { flex-grow: 3; position: relative; border-right: 3px solid #506070; min-width: 250px; background-color: #101820; height: 100%; box-sizing: border-box; overflow: hidden; cursor: default; }
12
  #ui-container { flex-grow: 2; padding: 25px; overflow-y: auto; background-color: #3a4a5a; min-width: 320px; height: 100%; box-sizing: border-box; display: flex; flex-direction: column; }
13
  #scene-container canvas { display: block; }
 
 
14
  #story-title { color: #ffd700; margin: 0 0 15px 0; padding-bottom: 10px; border-bottom: 2px solid #506070; font-size: 1.8em; font-weight: bold; text-shadow: 1px 1px 2px #111; }
15
  #story-content { margin-bottom: 25px; line-height: 1.7; flex-grow: 1; font-size: 1.1em; color: #e8e8e8;}
16
  #stats-inventory-container { margin-bottom: 25px; padding: 15px; border: 1px solid #506070; border-radius: 5px; background-color: #4a5a6a; font-size: 0.95em; }
17
  #stats-display, #inventory-display { margin-bottom: 10px; line-height: 1.8; }
18
+ #stats-display span, #inventory-display .item-tag { display: inline-block; background-color: #5a6a7a; padding: 4px 10px; border-radius: 15px; margin: 0 8px 5px 0; border: 1px solid #7a8a9a; white-space: nowrap; box-shadow: inset 0 1px 2px rgba(0,0,0,0.2); }
 
19
  #stats-display strong, #inventory-display strong { color: #e0e0e0; margin-right: 6px; font-weight:bold; }
20
  #inventory-display em { color: #aabbcc; font-style: normal; }
21
  .item-tool { background-color: #a0522d; border-color: #cd853f; color: #fff;}
22
  .item-key { background-color: #ffd700; border-color: #f0e68c; color: #333;}
23
  .item-treasure { background-color: #20b2aa; border-color: #48d1cc; color: #fff;}
24
  .item-food { background-color: #ff6347; border-color: #fa8072; color: #fff;}
25
+ .item-quest { background-color: #da70d6; border-color: #ee82ee; color: #fff;}
26
  .item-unknown { background-color: #778899; border-color: #b0c4de;}
 
 
27
  #choices-container { margin-top: auto; padding-top: 20px; border-top: 2px solid #506070; }
28
  #choices-container h3 { margin-top: 0; margin-bottom: 12px; color: #e0e0e0; font-size: 1.2em; font-weight: bold;}
29
+ #choices { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 15px;}
30
+ #action-choices { display: flex; flex-direction: column; gap: 12px; margin-top: 15px; border-top: 1px dashed #607080; padding-top: 15px;}
31
  .choice-button { display: block; width: 100%; padding: 13px 16px; margin-bottom: 0; background-color: #607080; color: #fff; border: 1px solid #8090a0; border-radius: 5px; cursor: pointer; text-align: center; font-family: 'Verdana', sans-serif; font-size: 1.0em; font-weight: bold; transition: background-color 0.2s, transform 0.1s; box-sizing: border-box; letter-spacing: 0.5px; }
32
+ .choice-button.action { text-align: left; grid-column: span 2;}
33
  .choice-button:hover:not(:disabled) { background-color: #ffc107; color: #222; border-color: #ffca2c; transform: translateY(-1px); }
34
  .choice-button:disabled { background-color: #4e5a66; color: #8a9aab; cursor: not-allowed; border-color: #607080; opacity: 0.6; transform: none; box-shadow: none;}
35
  .choice-button[title]:disabled::after { content: ' (' attr(title) ')'; font-style: italic; font-size: 0.9em; margin-left: 5px; }
 
43
  .message-combat { color: #f98; border-left-color: #c64; font-weight: bold;}
44
  .combat-button { background-color: #a33; border-color: #c66; color: #fff; font-weight: bold; text-align: center;}
45
  .combat-button:hover:not(:disabled) { background-color: #d44; border-color: #f88;}
 
 
46
  #action-info { position: absolute; bottom: 10px; left: 10px; background-color: rgba(0,0,0,0.7); color: #ffcc66; padding: 5px 10px; border-radius: 3px; font-size: 0.9em; display: block; z-index: 10;}
47
  </style>
48
  </head>
 
53
  </div>
54
  <div id="ui-container">
55
  <h2 id="story-title">Initializing...</h2>
56
+ <div id="story-content"><p>Loading assets...</p></div>
57
  <div id="stats-inventory-container">
58
  <div id="stats-display">Loading Stats...</div>
59
  <div id="inventory-display">Inventory: ...</div>
 
75
 
76
  <script type="module">
77
  import * as THREE from 'three';
78
+ import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; // Re-added controls
79
  import { FontLoader } from 'three/addons/loaders/FontLoader.js';
80
  import { TextGeometry } from 'three/addons/geometries/TextGeometry.js';
 
81
 
82
  console.log("Script module execution started.");
83
 
 
85
  const sceneContainer = document.getElementById('scene-container');
86
  const storyTitleElement = document.getElementById('story-title');
87
  const storyContentElement = document.getElementById('story-content');
88
+ const choicesElement = document.getElementById('choices');
89
+ const actionChoicesElement = document.getElementById('action-choices');
90
  const statsElement = document.getElementById('stats-display');
91
  const inventoryElement = document.getElementById('inventory-display');
92
  const actionInfoElement = document.getElementById('action-info');
 
94
  console.log("DOM elements obtained.");
95
 
96
  // --- Core Three.js Variables ---
97
+ let scene, camera, renderer, clock, controls, raycaster, mouse; // Added controls, raycaster, mouse back
98
  let currentSceneGroup = null;
99
  let currentLights = [];
100
  let threeFont = null;
101
  let currentMessage = "";
102
+ let activeTimeouts = []; // Clear timeouts on scene change
103
 
104
  // --- Materials ---
105
  const MAT = {
 
120
  sky_blue: new THREE.MeshStandardMaterial({ color: 0x03a9f4, roughness: 0.5 }),
121
  town_wood: new THREE.MeshStandardMaterial({ color: 0xae8a63, roughness: 0.7 }),
122
  town_roof: new THREE.MeshStandardMaterial({ color: 0x8b4513, roughness: 0.8 }),
123
+ goblin_skin: new THREE.MeshStandardMaterial({ color: 0x8FBC8F, roughness: 0.8 }),
124
+ text_material: new THREE.MeshBasicMaterial({ color: 0xffddaa, transparent: true }), // Make text transparent for fade
125
  };
126
 
127
  // --- Game State ---
 
134
  "Goblin's Favorite Sock": {type:"quest", description:"Smells... unique."},
135
  "Shiny Rock": {type:"treasure", description:"Distractingly shiny."},
136
  "Suspicious Mushroom": {type:"food", description:"Maybe... just maybe..."},
137
+ "Cave Crystal": {type:"unknown", description:"A faintly glowing crystal shard."}
138
  };
139
 
140
  // --- Enemy Data ---
141
  const enemyData = {
142
  'goblin': { name: "Grumpy Goblin", hp: 12, defense: 12, attackBonus: 1, damageDice: 4, xp: 25, drops: ["Goblin's Favorite Sock", "Shiny Rock"] },
143
+ 'skeleton': { name: "Clattering Skeleton", hp: 10, defense: 13, attackBonus: 2, damageDice: 4, xp: 20, drops: ["Ancient Coin"] }, // Added skeleton
144
+ 'spider': { name: "Hairy Spider", hp: 15, defense: 11, attackBonus: 2, damageDice: 3, xp: 25, drops: ["Cave Crystal"] } // Adjusted spider
145
  };
146
 
147
+ // --- Procedural Shapes ---
148
+ const SHAPE_GENERATORS = {
149
  'pointy_cone': (size) => new THREE.ConeGeometry(size * 0.5, size * 1.2, 6 + Math.floor(Math.random() * 5)),
150
  'round_blob': (size) => new THREE.SphereGeometry(size * 0.6, 12, 8),
151
  'spinny_torus': (size) => new THREE.TorusGeometry(size * 0.5, size * 0.15, 8, 16),
 
168
  };
169
  const shapeKeys = Object.keys(SHAPE_GENERATORS);
170
 
171
+ // --- Game Data (More Pages) ---
172
+ const gameData = {
173
+ 1: { // Snoring Meadows (Start)
174
  title: "Snoring Meadows",
175
+ content: "<p>You wake with a start! Not from a nightmare, but because the grass around you is... snoring softly? Weird. A path leads North into the Wiggly Woods and East towards some noisy-looking Clanky Caves.</p>",
176
  options: [
177
+ { text: "Venture into Wiggly Woods", next: 2 },
178
+ { text: "Investigate Clanky Caves", next: 5 },
179
+ { text: "Poke the snoring grass? (WIS Check DC 10)", check: { stat: 'wisdom', dc: 10, next: 10, onFailure: 11 } }
180
  ],
181
  assemblyParams: { baseShape: 'ground_grass', baseSize: 30, mainShapes: ['squashed_ball', 'pointy_cone'], accents: [], count: 30, scaleRange: [0.5, 1.2], colorTheme: [MAT.grass, MAT.leaf_green, MAT.sky_blue], arrangement: 'scatter' }
182
  },
183
+ 2: { // Wiggly Woods Entrance
184
  title: "Wiggly Woods",
185
+ content: "<p>Whoa! These trees are doing the wiggle! It's quite groovy, but makes walking tricky. A particularly grumpy-looking Goblin blocks the path deeper (North).</p>",
186
  options: [
187
+ { text: "Politely ask the Goblin to move", next: 3 },
188
+ { text: "Try to wiggle past (DEX Check DC 12)", check: { stat: 'dexterity', dc: 12, next: 4, onFailure: 3 } }, // Fail leads to talk
189
+ { text: "Wiggle back to the Meadows (South)", next: 1 }
190
  ],
191
  assemblyParams: { baseShape: 'ground_dirt', baseSize: 25, mainShapes: ['tall_cylinder', 'knotty_thing'], accents: ['leaf_autumn'], count: 20, scaleRange: [1.0, 3.0], colorTheme: [MAT.wood_dark, MAT.leaf_autumn, MAT.deep_purple], arrangement: 'cluster', clusterRadius: 10 }
192
  },
193
+ 3: { // Confront Goblin
194
+ title: "Grumpy Goblin Guard",
195
+ content: "<p>'Oi! Wiggler!' the Goblin shouts, adjusting his single, smelly sock. 'Dis MY patch o' wiggles! Whatcha want?' He doesn't look like he enjoys conversation. Or baths.</p>",
196
  options: [
197
+ { text: "'Just passing through!' (CHA Check DC 14)", check: { stat: 'charisma', dc: 14, next: 4, onFailure: 12 } },
198
+ { text: "Offer 'Shiny Rock' as tribute", requires: "Shiny Rock", consume: true, next: 4, reward: {xp: 15} },
199
+ { text: "Prepare for a Wobbly Tussle!", action: 'triggerCombat', enemy: 'goblin', nextOnWin: 4, nextOnLoss: 98 },
200
+ { text: "'Nevermind!' (Retreat)", next: 2 }
201
  ],
202
+ assemblyParams: { baseShape: 'ground_dirt', baseSize: 25, mainShapes: ['tall_cylinder', 'knotty_thing'], accents: ['leaf_autumn', 'basic_tetra'], count: 20, scaleRange: [1.0, 3.0], colorTheme: [MAT.wood_dark, MAT.leaf_autumn, MAT.deep_purple, MAT.goblin_skin], arrangement: 'cluster', clusterRadius: 10 }
 
 
 
 
 
 
203
  },
204
+ 4: { // Past Goblin
205
+ title: "Further Into the Woods",
206
+ content: "<p>You made it past the grumpy guardian! The woods get thicker here, but you see a strange, perfectly Square Clearing to the North.</p>",
207
  options: [
208
+ { text: "Investigate the Square Clearing (North)", next: 7 },
209
+ { text: "Go Back Past Goblin Spot (South)", next: 2 } // Assume goblin is gone/pacified
210
  ],
211
+ assemblyParams: { baseShape: 'ground_dirt', baseSize: 25, mainShapes: ['tall_cylinder', 'round_blob'], accents: ['leaf_green'], count: 25, scaleRange: [1.0, 2.8], colorTheme: [MAT.wood_light, MAT.leaf_green, MAT.stone_brown], arrangement: 'scatter' }
212
  },
213
+ 5: { // Cave Entrance
214
  title: "Clanky Caves Entrance",
215
+ content: "<p>CLANK! WHOOSH! A rusty metal sign hangs crookedly: 'Clanky Caves - Enter At Yer Own Risk & Volume Level'. It smells faintly of oil and... ozone?</p>",
216
+ options: [ { text: "Bravely Enter!", next: 8 }, { text: "Nope Out (West)", next: 1 } ],
217
+ assemblyParams: { baseShape: 'stone_grey', baseSize: 20, mainShapes: ['spiky_ball', 'spinny_torus', 'holed_box'], accents: ['metal_rusty'], count: 25, scaleRange: [0.5, 1.8], colorTheme: [MAT.stone_grey, MAT.metal_rusty, MAT.metal_shiny], arrangement: 'cluster', clusterRadius: 8 }
218
  },
219
+ 6: { // Placeholder - Reached from old page 3, needs rewrite
220
+ title: "Gem Patch Revisited",
221
+ content: "<p>You return to the shiny spot. The remaining gems twinkle invitingly.</p>",
222
+ options: [ { text: "Go Back to Meadow Path", next: 1 } ], // Simple exit
223
+ assemblyParams: { baseShape: 'ground_grass', baseSize: 15, mainShapes: ['gem_shape', 'basic_tetra'], accents: ['pointy_cone'], count: 30, scaleRange: [0.2, 0.7], colorTheme: [MAT.crystal_blue, MAT.crystal_red, MAT.stone_grey], arrangement: 'patch', patchPos: {x:0, y:0.1, z:0}, patchRadius: 5 }
224
+ },
225
+ 7: { // Square Clearing
226
  title: "The Square Clearing",
227
+ content: "<p>Wow, they weren't kidding. Everything is suspiciously square - the rocks, the flowers... In the center sits a locked chest, also square, with a distinctly wobbly keyhole.</p>",
228
  options: [
229
+ { text: "Try Wobbly Key", requires: "Wobbly Key", consume: true, next: 13 },
230
+ { text: "Try Sturdy Stick (Strength Check DC 13 to pry)", requires: "Sturdy Stick", check: { stat: 'strength', dc: 13, next: 14, onFailure: 14 } }, // Fail still goes to 14 but maybe adds message
231
+ { text: "Leave the Creepy Clearing (South)", next: 4 }
232
  ],
233
+ assemblyParams: { baseShape: 'ground_grass', baseSize: 20, mainShapes: ['boxy_chunk', 'holed_box'], accents: ['basic_tetra'], count: 15, scaleRange: [0.8, 1.5], colorTheme: [MAT.stone_brown, MAT.grass, MAT.stone_grey], arrangement: 'patch', patchRadius: 8 }
234
  },
235
  8: { // Inside Clanky Caves
236
  title: "Clanky Caves - Junction",
237
+ content: "<p>Clank! Whirr! Sproing! It's a cacophony! Gears spin wildly, pistons pump uselessly. Passages lead West (Clankier?) and East (Quieter?).</p>",
238
+ options: [ { text: "Go West (More Clanks!)", next: 9 }, { text: "Go East (Less Clanky?)", next: 5 } ],
239
+ assemblyParams: { baseShape: 'stone_grey', baseSize: 25, mainShapes: ['spinny_torus', 'holed_box', 'tall_cylinder'], accents: ['metal_shiny', 'basic_tetra'], count: 30, scaleRange: [0.6, 2.0], colorTheme: [MAT.stone_grey, MAT.metal_shiny, MAT.metal_rusty, MAT.bright_yellow], arrangement: 'cluster', clusterRadius: 10 }
240
  },
241
+ 9: { // Deeper Cave / Sprocket Stash
242
  title: "Sprocket Stash",
243
+ content: "<p>Jackpot! A huge pile of Shiny Sprockets! They spin hypnotically. Also, some weird purple mushrooms pulse nearby.</p>",
244
+ options: [ { text: "Grab a Sprocket!", reward: { addItem: "Shiny Sprocket", xp: 10 }, next: 9 }, { text: "Lick a Mushroom? (CON Check DC 11)", check: {stat:'constitution', dc: 11, next: 15, onFailure: 16 } }, { text: "Go Back East", next: 8 } ],
245
+ assemblyParams: { baseShape: 'stone_grey', baseSize: 15, mainShapes: ['gem_shape', 'pointy_cone'], accents: ['spinny_torus', 'metal_shiny'], count: 25, scaleRange: [0.4, 1.0], colorTheme: [MAT.metal_shiny, MAT.bright_yellow, MAT.deep_purple], arrangement: 'patch', patchRadius: 4 }
246
  },
247
  10: { // Success poking grass
248
  title: "Grass Tickled!",
249
+ content: "<p>Hehe! The blade of grass giggles (yes, really!) and spits out a very Wobbly Key!</p>",
250
+ options: [ { text: "Nab the Key!", reward: { addItem: "Wobbly Key", xp: 5 }, next: 1 } ],
251
  assemblyParams: { baseShape: 'ground_grass', baseSize: 30, mainShapes: ['squashed_ball', 'pointy_cone'], accents: [], count: 30, scaleRange: [0.5, 1.2], colorTheme: [MAT.grass, MAT.leaf_green, MAT.sky_blue], arrangement: 'scatter' }
252
  },
253
  11: { // Fail poking grass
254
+ title: "Grass Annoyed!",
255
+ content: "<p>The snoring grass grumbles and rolls over, hiding whatever might have been there. Maybe try again later?</p>",
256
+ options: [ { text: "Okay, okay, sorry!", next: 1 } ],
 
257
  assemblyParams: { baseShape: 'ground_grass', baseSize: 30, mainShapes: ['squashed_ball', 'pointy_cone'], accents: [], count: 30, scaleRange: [0.5, 1.2], colorTheme: [MAT.grass, MAT.leaf_green, MAT.sky_blue], arrangement: 'scatter' }
258
  },
259
  12: { // Fail charisma check on goblin
260
+ title: "Goblin Scowls",
261
+ content: "<p>'Bah! Words are cheap!' The goblin tightens his grip on the spear. 'Try again, maybe with shiny stuff?'</p>",
262
  options: [
263
+ { text: "Attack!", action: 'triggerCombat', enemy: 'goblin', nextOnWin: 4, nextOnLoss: 98 },
264
+ { text: "Offer 'Shiny Rock'", requires: "Shiny Rock", consume: true, next: 4, reward: {xp: 10} },
265
+ { text: "Flee!", next: 2 }
266
  ],
267
  assemblyParams: { baseShape: 'ground_dirt', baseSize: 25, mainShapes: ['tall_cylinder', 'knotty_thing'], accents: ['leaf_autumn', 'basic_tetra'], count: 20, scaleRange: [1.0, 3.0], colorTheme: [MAT.wood_dark, MAT.leaf_autumn, MAT.deep_purple, MAT.goblin_skin], arrangement: 'cluster', clusterRadius: 10 }
268
  },
269
  13: { // Open chest with key
270
+ title: "Wobbly Chest Opens!",
271
+ content: "<p>Wiggle, jiggle... click! The key works! Inside is... a single, slightly Suspicious Mushroom. Huh.</p>",
272
+ options: [ { text: "Take the Mushroom", reward: { addItem: "Suspicious Mushroom", xp: 15 }, next: 7 } ],
273
+ assemblyParams: { baseShape: 'ground_grass', baseSize: 20, mainShapes: ['boxy_chunk', 'holed_box'], accents: [], count: 15, scaleRange: [0.8, 1.5], colorTheme: [MAT.stone_brown, MAT.grass, MAT.stone_grey], arrangement: 'patch', patchRadius: 8 }
274
+ },
275
+ 14: { // Whack chest / Fail Str check
276
+ title: "Thwack! Ow.",
277
+ content: "<p>You whack the chest. It remains stubbornly square and closed. Your stick might have a splinter. The lock still wobbles mockingly.</p>",
278
+ options: [ { text: "Try Wobbly Key", requires: "Wobbly Key", consume: true, next: 13 }, { text: "Leave it", next: 7 } ],
279
+ assemblyParams: { baseShape: 'ground_grass', baseSize: 20, mainShapes: ['boxy_chunk', 'holed_box'], accents: [], count: 15, scaleRange: [0.8, 1.5], colorTheme: [MAT.stone_brown, MAT.grass, MAT.stone_grey], arrangement: 'patch', patchRadius: 8 }
280
  },
281
+ 15: { // Success licking Mushroom
282
+ title: "Tingly!",
283
+ content: "<p>You lick the mushroom. It tastes like purple and static! You feel... stronger! (+1 Strength!)</p>",
284
+ options: [ { text: "Whoa! Go Back.", next: 8 } ],
285
+ reward: { statGain: { strength: 1 }, xp: 5 }, // Example stat gain
286
+ assemblyParams: { baseShape: 'stone_grey', baseSize: 15, mainShapes: ['gem_shape'], accents: ['spinny_torus', 'metal_shiny'], count: 20, scaleRange: [0.4, 1.0], colorTheme: [MAT.metal_shiny, MAT.bright_yellow, MAT.deep_purple], arrangement: 'patch', patchRadius: 4 }
287
  },
288
+ 16: { // Fail licking Mushroom
289
+ title: "Blech!",
290
+ content: "<p>You lick the mushroom. It tastes like old socks and regret. Your tongue feels fuzzy. (-1 Charisma temporarily? TBC)</p>",
291
+ options: [ { text: "Ugh! Go Back.", next: 8 } ],
292
+ // Add temporary effect later if needed
293
  assemblyParams: { baseShape: 'stone_grey', baseSize: 15, mainShapes: ['gem_shape'], accents: ['spinny_torus', 'metal_shiny'], count: 20, scaleRange: [0.4, 1.0], colorTheme: [MAT.metal_shiny, MAT.bright_yellow, MAT.deep_purple], arrangement: 'patch', patchRadius: 4 }
294
  },
295
  98: { // Lose combat
296
+ title: "Bonked!",
297
+ content: "<p>Ouch! That hurt. You wake up back in the Snoring Meadows, feeling rather silly.</p>",
298
+ options: [ { text: "Try again?", next: 1 } ],
299
+ assemblyParams: { baseShape: 'ground_grass', count: 5, mainShapes:['round_blob'], colorTheme:[MAT.grass]}
300
  },
301
  99: { // Generic End/TBC
302
+ title: "To Be Continued... Maybe!",
303
+ content: "<p>That's the end of this particular wacky path. Was there a point? Who knows! Adventure!</p>",
304
  options: [ { text: "Start Over?", next: 1 } ],
305
  assemblyParams: { baseShape: 'flat_plate', baseSize: 10, mainShapes: ['basic_tetra'], count: 5, scaleRange: [1, 2], colorTheme: [MAT.sky_blue, MAT.bright_yellow], arrangement: 'center_stack', stackHeight: 3 }
306
  }
 
328
  renderer.shadowMap.type = THREE.PCFSoftShadowMap;
329
  sceneContainer.appendChild(renderer.domElement);
330
 
331
+ // Controls removed for page-based navigation focus
332
  // controls = new OrbitControls(camera, renderer.domElement);
333
 
334
  window.addEventListener('resize', onWindowResize, false);
335
+ renderer.domElement.addEventListener('click', onMouseClick, false);
336
  setTimeout(onWindowResize, 100);
337
  animate();
338
  console.log("initThreeJS finished.");
 
341
  function loadFontAndStart() {
342
  console.log("Loading font...");
343
  const loader = new FontLoader();
344
+ // Ensure the path to the font is correct or use a reliable CDN
345
  loader.load('https://unpkg.com/[email protected]/examples/fonts/helvetiker_regular.typeface.json', function (font) {
346
  threeFont = font;
347
  console.log("Font loaded.");
348
  startGame();
349
  }, undefined, function (error) {
350
  console.error('Font loading failed:', error);
351
+ threeFont = null;
352
+ startGame();
353
  });
354
  }
355
 
 
366
  }
367
 
368
  function onMouseClick( event ) {
 
369
  const rect = renderer.domElement.getBoundingClientRect();
370
  mouse.x = ( (event.clientX - rect.left) / rect.width ) * 2 - 1;
371
  mouse.y = - ( (event.clientY - rect.top) / rect.height ) * 2 + 1;
372
+ pickupItem(); // Only use click for pickup now
373
  }
374
 
375
 
 
377
  requestAnimationFrame(animate);
378
  const delta = clock.getDelta();
379
  const time = clock.getElapsedTime();
380
+ // controls?.update(); // Controls removed
 
381
 
382
  if (currentSceneGroup) {
383
  currentSceneGroup.traverse(obj => {
 
397
  return mesh;
398
  }
399
 
400
+ function setupLighting(type = 'default') { // Simplified lighting
401
  currentLights.forEach(light => { if (light.parent) light.parent.remove(light); if(scene.children.includes(light)) scene.remove(light); });
402
  currentLights = [];
403
 
404
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.7);
405
  scene.add(ambientLight);
406
  currentLights.push(ambientLight);
407
 
408
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2);
409
+ directionalLight.position.set(8, 15, 10);
410
  directionalLight.castShadow = true;
411
  directionalLight.shadow.mapSize.set(1024, 1024);
412
+ directionalLight.shadow.camera.near = 0.5; directionalLight.shadow.camera.far = 50;
413
  const sb = 25;
414
  directionalLight.shadow.camera.left = -sb; directionalLight.shadow.camera.right = sb;
415
  directionalLight.shadow.camera.top = sb; directionalLight.shadow.camera.bottom = -sb;
416
+ directionalLight.shadow.bias = -0.001; // Adjusted bias slightly
417
  scene.add(directionalLight);
418
  currentLights.push(directionalLight);
419
  }
 
439
  baseMesh = new THREE.Mesh(groundGeo, groundMat);
440
  baseMesh.rotation.x = -Math.PI / 2; baseMesh.position.y = 0;
441
  baseMesh.receiveShadow = true; baseMesh.castShadow = false;
442
+ baseMesh.userData.isGround = true;
443
  group.add(baseMesh);
444
  } else {
445
  const baseGeoFunc = SHAPE_GENERATORS[baseShape] || SHAPE_GENERATORS['flat_plate'];
446
  const baseGeo = baseGeoFunc(baseSize);
447
  baseMesh = createMesh(baseGeo, colorTheme[0] || MAT.stone_grey, {y:0.1});
448
  baseMesh.receiveShadow = true; baseMesh.castShadow = false;
449
+ baseMesh.userData.isGround = true;
450
  group.add(baseMesh);
451
  }
452
 
453
  const allShapes = [...mainShapes, ...accents];
454
+ let lastY = 0.1;
455
  let stackCount = 0;
456
 
457
  for (let i = 0; i < count; i++) {
 
462
 
463
  const scaleFactor = scaleRange[0] + Math.random() * (scaleRange[1] - scaleRange[0]);
464
  const geometry = geoFunc(scaleFactor);
465
+ if (!geometry) { console.warn(`Geometry creation failed for ${shapeKey}`); continue; } // Check geometry creation
466
+
467
  const material = colorTheme[Math.floor(Math.random() * colorTheme.length)];
468
  const mesh = createMesh(geometry, material);
469
 
470
+ try {
471
+ geometry.computeBoundingBox();
472
+ } catch(e) { console.error("Error computing bounding box", e, geometry); continue; }
473
+
474
  let height = 0;
475
  if (geometry.boundingBox) {
476
  height = (geometry.boundingBox.max.y - geometry.boundingBox.min.y) * mesh.scale.y;
477
  } else {
478
+ height = scaleFactor;
 
479
  }
480
+ height = Math.max(0.1, height);
481
 
482
 
483
  let position = { x:0, y:0, z:0 };
484
+ let isValidPosition = false;
485
 
486
  switch(arrangement) {
487
+ case 'stack':
 
 
 
 
 
 
 
 
 
 
488
  if (stackCount < stackHeight) {
489
+ position.y = lastY + height / 2;
490
+ position.x = (Math.random() - 0.5) * 0.5 * scaleFactor;
491
+ position.z = (Math.random() - 0.5) * 0.5 * scaleFactor;
492
+ lastY += height * 0.9;
493
+ stackCount++; isValidPosition = true;
494
+ } break;
495
+ case 'center_stack':
496
+ if (stackCount < stackHeight) {
497
+ position.y = lastY + height / 2;
498
+ lastY += height * 0.95;
499
+ stackCount++; isValidPosition = true;
500
+ } break;
501
+ case 'cluster':
502
+ const angle = Math.random() * Math.PI * 2;
503
+ const radius = Math.random() * clusterRadius;
504
+ position.x = Math.cos(angle) * radius;
505
+ position.z = Math.sin(angle) * radius;
506
+ position.y = height / 2; isValidPosition = true; break;
507
+ case 'patch':
508
+ const pAngle = Math.random() * Math.PI * 2;
509
+ const pRadius = Math.random() * patchRadius;
510
+ position.x = patchPos.x + Math.cos(pAngle) * pRadius;
511
+ position.z = patchPos.z + Math.sin(pAngle) * pRadius;
512
+ position.y = (patchPos.y || 0) + height / 2; isValidPosition = true; break;
513
+ case 'scatter': default:
514
+ position.x = (Math.random() - 0.5) * baseSize * 0.8;
515
+ position.z = (Math.random() - 0.5) * baseSize * 0.8;
516
+ position.y = height / 2; isValidPosition = true; break;
 
517
  }
518
 
519
+ if (!isValidPosition) continue;
520
 
521
  mesh.position.set(position.x, position.y, position.z);
522
  mesh.rotation.set(
 
537
  }
538
  group.add(mesh);
539
  }
540
+ console.log(`Assembly created with ${group.children.length -1} procedural objects.`);
541
  return group;
542
  }
543
 
544
  function updateScene(assemblyParams) {
545
  console.log("updateScene called");
546
+ activeTimeouts.forEach(id => clearTimeout(id)); // Clear pending timeouts
547
+ activeTimeouts = [];
548
+
549
  if (currentSceneGroup) {
550
+ scene.remove(currentSceneGroup); // Remove group cleanly
551
+ // Dispose geometries in the removed group
552
+ currentSceneGroup.traverse(child => {
553
+ if (child.isMesh && child.geometry) {
554
+ child.geometry.dispose();
555
+ }
556
+ });
557
  currentSceneGroup = null;
558
  }
559
+ setupLighting('default');
560
 
561
  if (!assemblyParams) {
562
  console.warn("No assemblyParams provided, creating default scene.");
 
569
  console.log("New scene group added.");
570
  } catch (error) {
571
  console.error("Error creating procedural assembly:", error);
 
572
  }
573
  }
574
 
575
+
576
  function startGame() {
577
  console.log("startGame called.");
578
  const defaultChar = {
 
583
  gameState = {
584
  currentPageId: 1,
585
  character: JSON.parse(JSON.stringify(defaultChar)),
586
+ combat: null
587
  };
588
  renderPage(gameState.currentPageId);
589
  console.log("startGame finished.");
590
  }
591
 
592
+ function handleChoiceClick(option) { // Renamed parameter for clarity
593
+ console.log("Choice clicked:", option);
594
  currentMessage = "";
595
 
596
+ // Check requirements first
597
+ if (option.requires && !gameState.character.inventory.includes(option.requires)) {
598
+ currentMessage = `<p class="message message-warning">You need a ${option.requires} for that!</p>`;
599
+ renderPage(gameState.currentPageId); // Re-render current page with message
600
+ return;
601
+ }
602
 
603
+ // Consume item if required and successful (or if action doesn't fail)
604
+ let consumedItem = false;
605
+ if (option.requires && option.consume) {
606
+ gameState.character.inventory = gameState.character.inventory.filter(i => i !== option.requires);
607
+ currentMessage += `<p class="message message-item">Used your ${option.requires}.</p>`;
608
+ consumedItem = true; // Mark as consumed
 
 
 
 
 
609
  }
610
 
611
+ // Handle different action types
612
+ if (option.action === 'triggerCombat' && option.enemy) {
613
  startCombat(option.enemy, option.nextOnWin, option.nextOnLoss);
614
+ // Combat flow handles the next steps and UI render
615
+ } else if (option.check) {
 
616
  performSkillCheck(option.check, option.next, option.onFailure);
617
+ // Skill check flow handles the next steps and UI render
618
+ } else if (option.next) {
619
+ // Simple navigation or action leading to next page
620
+ let nextPageId = parseInt(option.next);
621
+ const targetPageData = gameData[nextPageId];
622
+
623
+ if (!targetPageData) {
624
+ console.error(`Invalid next page ID: ${nextPageId}`);
625
+ currentMessage += `<p class="message message-warning">That path is mysteriously blocked!</p>`;
626
+ nextPageId = gameState.currentPageId; // Stay on current page
627
+ }
628
 
629
+ // Apply rewards associated *with this specific choice*
630
+ if (option.reward) {
631
+ applyReward(option.reward);
632
+ }
633
 
634
+ gameState.currentPageId = nextPageId;
635
+ renderPage(nextPageId); // Render the next page
 
 
 
 
 
636
 
637
+ } else {
638
+ console.warn("Choice has no action or next page:", option);
639
+ currentMessage += `<p class="message message-info">Nothing seems to happen...</p>`;
640
+ renderPage(gameState.currentPageId); // Re-render current page
641
+ }
642
  }
643
+ window.handleChoiceClick = handleChoiceClick; // Expose for inline handlers
644
 
645
  function performSkillCheck(checkData, successPageId, failurePageId) {
646
  const {stat, dc} = checkData;
 
648
  const modifier = Math.floor((baseStat - 10) / 2);
649
  const roll = Math.floor(Math.random() * 20) + 1;
650
  const total = roll + modifier;
651
+ const success = roll === 20 || (roll !== 1 && total >= dc); // Nat 20 success, Nat 1 fail
652
 
653
  console.log(`Skill Check ${stat}: Roll ${roll} + Mod ${modifier} = ${total} vs DC ${dc}`);
654
  currentMessage += `<p class="message message-info"><em>Rolling ${stat}... (Rolled ${total} vs DC ${dc})</em></p>`;
655
+ displayDiceRoll(roll, success);
656
 
657
  if (success) {
658
  currentMessage += `<p class="message message-success"><em>Success!</em></p>`;
 
661
  currentMessage += `<p class="message message-failure"><em>Failed!</em></p>`;
662
  gameState.currentPageId = failurePageId;
663
  }
664
+
665
+ // Add delay before showing result page
666
+ activeTimeouts.push(setTimeout(() => renderPage(gameState.currentPageId), 2600)); // Longer delay
667
  }
668
 
669
  function applyReward(rewardData) {
670
+ // Refactored reward application
671
+ if(!rewardData) return;
672
  if(rewardData.addItem && itemsData[rewardData.addItem]) {
673
  if (!gameState.character.inventory.includes(rewardData.addItem)) {
674
  gameState.character.inventory.push(rewardData.addItem);
675
+ currentMessage += `<p class="message message-item">You got a ${rewardData.addItem}!</p>`;
676
  } else {
677
+ currentMessage += `<p class="message message-info">You found another ${rewardData.addItem}.</p>`;
678
  }
679
  }
680
  if(rewardData.xp) {
681
  gameState.character.stats.xp += rewardData.xp;
682
+ currentMessage += `<p class="message message-xp">Gained ${rewardData.xp} XP!</p>`;
683
  }
684
  if(rewardData.hpGain) {
685
  gameState.character.stats.hp = Math.min(gameState.character.stats.maxHp, gameState.character.stats.hp + rewardData.hpGain);
686
+ currentMessage += `<p class="message message-success">Recovered ${rewardData.hpGain} HP.</p>`;
687
  }
688
+ if(rewardData.statGain) {
689
  for (const stat in rewardData.statGain) {
690
  if (gameState.character.stats.hasOwnProperty(stat)) {
691
+ const increase = rewardData.statGain[stat];
692
+ gameState.character.stats[stat] += increase;
693
+ currentMessage += `<p class="message message-success">${stat.charAt(0).toUpperCase() + stat.slice(1)} increased by ${increase}!</p>`;
694
+ if (stat === 'maxHp' && increase > 0) { // Also heal when maxHP increases
695
+ gameState.character.stats.hp += increase;
696
  }
697
+ // Potentially recalculate maxHP if constitution changes (add recalculateMaxHp function if needed)
698
  }
699
  }
700
  }
701
  }
702
 
 
703
  function renderPage(pageId) {
704
  console.log(`Rendering page ${pageId}`);
705
  const pageData = gameData[pageId];
706
 
707
+ if (!pageData) { /* Error handling */ console.error(`No page data for ID: ${pageId}`); return; }
708
 
709
  storyTitleElement.textContent = pageData.title || "An Unnamed Place";
710
  storyContentElement.innerHTML = currentMessage + (pageData.content || "<p>...</p>");
711
  choicesElement.innerHTML = '';
712
+ actionChoicesElement.innerHTML = '';
713
 
714
+ // Navigation Buttons - Always show 4, disable if no exit
715
+ const neighbors = {}; // For page-based, neighbours aren't fixed grid, maybe infer from options? Or ignore for now.
716
+ const navOptions = pageData.options.filter(opt => opt.text.toLowerCase().includes("go ")); // Basic filter for nav text
717
+ ['North', 'South', 'East', 'West'].forEach(dir => {
 
718
  const button = document.createElement('button');
719
  button.classList.add('choice-button');
720
+ button.textContent = `Go ${dir}`;
721
+ // Find if an option corresponds to this direction (simplistic check)
722
+ const matchingOption = pageData.options.find(opt => opt.text.toLowerCase().includes(`(${dir.toLowerCase()})`) || opt.text.toLowerCase().includes(`go ${dir.toLowerCase()}`));
723
+ if (matchingOption) {
724
+ button.onclick = () => handleChoiceClick(matchingOption);
725
+ // Check requirements for this specific nav option
726
+ if (matchingOption.requires && !gameState.character.inventory.includes(matchingOption.requires)) {
727
+ button.disabled = true;
728
+ button.title = `Requires: ${matchingOption.requires}`;
729
+ }
730
+ } else {
731
+ button.disabled = true; // Disable if no matching option found
732
+ }
733
  choicesElement.appendChild(button);
734
+ });
 
735
 
 
 
 
 
 
 
736
 
737
+ // Action Buttons (Non-navigational)
738
+ if (pageData.options) {
739
+ pageData.options.filter(opt => !opt.text.toLowerCase().includes("go ")).forEach(option => { // Filter out nav buttons
740
+ const button = document.createElement('button');
741
+ button.classList.add('choice-button', 'action');
742
+ if (option.action === 'triggerCombat') button.classList.add('combat-button'); // Style combat triggers
743
+ button.textContent = option.text;
744
+
745
+ let requirementMet = true;
746
+ if (option.requires && !gameState.character.inventory.includes(option.requires)) {
747
+ requirementMet = false;
748
+ button.title = `Requires: ${option.requires}`;
749
+ }
750
+ button.disabled = !requirementMet;
751
+
752
+ button.onclick = () => handleChoiceClick(option);
753
+ actionChoicesElement.appendChild(button);
754
+ });
755
+ }
756
+ if (actionChoicesElement.innerHTML === '') {
757
+ actionChoicesElement.innerHTML = '<p><i>Nothing else seems possible here.</i></p>';
758
+ }
759
 
760
+ // Combat UI
761
+ if (gameState.combat?.active) {
762
+ actionChoicesElement.innerHTML += `
763
+ <div id="combat-ui">
764
+ <p class="message message-combat">Fighting ${gameState.combat.enemyName}! (Enemy HP: ${gameState.combat.enemyHp})</p>
765
+ <button class="choice-button combat-button action" onclick="handleCombatAction('attack')">Attack!</button>
766
+ </div>`;
767
  }
768
 
769
+
770
  updateStatsDisplay();
771
  updateInventoryDisplay();
772
  updateActionInfo();
773
  updateScene(pageData.assemblyParams);
774
  }
 
 
775
 
776
  function updateStatsDisplay() {
777
+ if (!gameState.character || !statsElement) return;
778
+ const stats = gameState.character.stats;
779
+ const hpColor = stats.hp / stats.maxHp < 0.3 ? '#f88' : (stats.hp / stats.maxHp < 0.6 ? '#fd5' : '#8f8');
780
+ const statBonus = (statVal) => {
781
+ const mod = Math.floor((statVal - 10) / 2);
782
+ return `${statVal} (${mod >= 0 ? '+' : ''}${mod})`;
783
+ };
784
+ statsElement.innerHTML = `<strong>Stats:</strong>
785
+ <span style="color:${hpColor}">HP: ${stats.hp}/${stats.maxHp}</span> <span>XP: ${stats.xp}</span><br>
786
+ <span>Str: ${statBonus(stats.strength)}</span> <span>Dex: ${statBonus(stats.dexterity)}</span> <span>Con: ${statBonus(stats.constitution)}</span>
787
+ <span>Int: ${statBonus(stats.intelligence)}</span> <span>Wis: ${statBonus(stats.wisdom)}</span> <span>Cha: ${statBonus(stats.charisma)}</span>`;
788
+ }
 
789
 
790
  function updateInventoryDisplay() {
791
  if (!gameState.character || !inventoryElement) return;
 
808
  actionInfoElement.textContent = `Location: ${gameData[gameState.currentPageId]?.title || 'Unknown'} | ${mode}`;
809
  }
810
 
811
+ // --- Combat & Item Functions ---
812
  function startCombat(enemyTypeId, nextOnWin, nextOnLoss) {
813
  const enemyBase = enemyData[enemyTypeId];
814
  if (!enemyBase) { console.error("Unknown enemy type:", enemyTypeId); return; }
815
  gameState.combat = {
816
  active: true, enemyId: enemyTypeId, enemyName: enemyBase.name,
817
  enemyHp: enemyBase.hp, enemyMaxHp: enemyBase.hp,
818
+ enemyDefense: enemyBase.defense, enemyAttackBonus: enemyBase.attackBonus || 2,
819
  enemyDamageDice: enemyBase.damageDice || 4, enemyXp: enemyBase.xp,
820
  enemyDrops: enemyBase.drops || [],
821
  nextOnWin: nextOnWin, nextOnLoss: nextOnLoss
822
  };
823
+ currentMessage = `<p class="message message-combat">Watch out! A ${enemyBase.name} attacks!</p>`;
824
+ renderPage(gameState.currentPageId); // Re-render to show combat UI
825
  }
826
 
827
  function handleCombatAction(action) {
828
  if (!gameState.combat?.active) return;
829
+ currentMessage = "";
830
 
831
  if (action === 'attack') {
832
  // Player Attack
 
834
  const playerAtkBonus = Math.floor((gameState.character.stats.strength - 10) / 2);
835
  const playerTotalAttack = playerRoll + playerAtkBonus;
836
  const playerHit = playerRoll === 20 || (playerRoll !== 1 && playerTotalAttack >= gameState.combat.enemyDefense);
837
+ displayDiceRoll(playerRoll, playerHit);
 
838
 
839
  if (playerHit) {
840
  const weapon = gameState.character.inventory.find(i => itemsData[i]?.type === 'weapon');
841
+ const baseDamage = itemsData[weapon]?.baseDamage || 2; // Use weapon or 1d2 for unarmed
842
+ const damageRoll = Math.max(1, Math.floor(Math.random() * baseDamage) + 1 + playerAtkBonus); // Add STR mod to damage
843
  gameState.combat.enemyHp -= damageRoll;
844
+ currentMessage += `<p class="message message-success">You hit for ${damageRoll} damage! (Rolled ${playerTotalAttack} vs AC ${gameState.combat.enemyDefense})</p>`;
845
  } else {
846
+ currentMessage += `<p class="message message-failure">You missed. (Rolled ${playerTotalAttack} vs AC ${gameState.combat.enemyDefense})</p>`;
847
  }
848
 
849
  // Check Enemy Defeat
850
  if (gameState.combat.enemyHp <= 0) {
851
  currentMessage += `<p class="message message-success"><b>You defeated the ${gameState.combat.enemyName}!</b></p>`;
852
+ applyReward({ xp: gameState.combat.enemyXp }); // Apply XP reward
853
+ // Handle Drops
854
  if (gameState.combat.enemyDrops.length > 0) {
855
  const droppedItemName = gameState.combat.enemyDrops[Math.floor(Math.random() * gameState.combat.enemyDrops.length)];
856
  dropItemInScene(droppedItemName, new THREE.Vector3(Math.random()*2-1, 0.2, Math.random()*2-1));
857
  currentMessage += `<p class="message message-item"><em>The ${gameState.combat.enemyName} dropped a ${droppedItemName}! Click it to pick up.</em></p>`;
858
  }
859
  const winPage = gameState.combat.nextOnWin;
860
+ gameState.combat = null;
861
+ gameState.currentPageId = winPage;
862
+ setTimeout(() => renderPage(gameState.currentPageId), 500); // Short delay after win message
863
  return;
864
  }
865
 
 
869
  const playerAC = 10 + Math.floor((gameState.character.stats.dexterity - 10) / 2);
870
  const enemyHit = enemyRoll === 20 || (enemyRoll !== 1 && enemyTotalAttack >= playerAC);
871
 
872
+ activeTimeouts.push( setTimeout(() => displayDiceRoll(enemyRoll, enemyHit), 600) ); // Display enemy roll slightly later
 
873
 
874
  if (enemyHit) {
875
  const damageRoll = Math.max(1, Math.floor(Math.random() * gameState.combat.enemyDamageDice) + 1);
 
878
  if (gameState.character.stats.hp <= 0) {
879
  currentMessage += `<p class="message message-failure"><b>You have been defeated!</b></p>`;
880
  const lossPage = gameState.combat.nextOnLoss;
881
+ gameState.combat = null;
882
+ gameState.currentPageId = lossPage;
883
+ activeTimeouts.push( setTimeout(() => renderPage(gameState.currentPageId), 2600) ); // Delay transition
884
  return;
885
  }
886
  } else {
887
  currentMessage += `<p class="message message-info">The ${gameState.combat.enemyName} misses you. (Rolled ${enemyTotalAttack} vs AC ${playerAC})</p>`;
888
  }
889
+ activeTimeouts.push( setTimeout(() => renderPage(gameState.currentPageId), 2600) ); // Re-render UI after delay
890
  }
891
  }
892
+ window.handleCombatAction = handleCombatAction;
893
 
894
+ function displayDiceRoll(result, success) {
895
+ if (!threeFont) { return; }
896
  activeTimeouts.forEach(timeoutId => clearTimeout(timeoutId)); activeTimeouts = [];
897
+ scene.children.filter(c => c.userData?.isDiceRoll).forEach(c => scene.remove(c));
898
 
899
  const textGeo = new TextGeometry(result.toString(), { font: threeFont, size: 1.0, height: 0.1, curveSegments: 4 });
900
+ textGeo.computeBoundingBox(); textGeo.center(); // Center geometry
 
901
  const textMat = MAT.text_material.clone();
902
  textMat.color.setHex(success ? 0x88ff88 : 0xff8888);
903
+ textMat.opacity = 1.0; // Start fully opaque
904
 
905
  const textMesh = new THREE.Mesh(textGeo, textMat);
906
  textMesh.userData.isDiceRoll = true;
907
 
908
+ const distance = 5; // Distance in front of camera
909
+ const cameraDirection = camera.getWorldDirection(new THREE.Vector3());
910
+ const textPos = camera.position.clone().add(cameraDirection.multiplyScalar(distance));
911
  textMesh.position.copy(textPos);
912
+ textMesh.position.y += 1.2; // Position slightly higher
913
+ textMesh.quaternion.copy(camera.quaternion); // Face camera
 
914
 
915
  scene.add(textMesh);
916
 
917
+ const duration = 2.5;
918
+ const fadeStart = 1.5; // Start fading after 1.5s
919
  const startTime = clock.getElapsedTime();
920
  textMesh.userData.startTime = startTime;
921
  textMesh.userData.update = (time) => {
 
923
  if (elapsed >= duration) {
924
  if (textMesh.parent) textMesh.parent.remove(textMesh);
925
  delete textMesh.userData.update;
926
+ // Remove from timeout tracking - not strictly needed as it stops updating
927
  } else {
928
+ textMesh.position.y += 0.015; // Float up
929
+ if (elapsed > fadeStart) {
930
+ textMesh.material.opacity = 1.0 - ((elapsed - fadeStart) / (duration - fadeStart));
931
+ }
932
  }
933
  };
934
+ }
935
+
936
 
937
  function dropItemInScene(itemName, positionOffset = new THREE.Vector3(0, 0, 0)) {
938
+ const currentGroup = currentSceneGroup; // Drop relative to current scene group
939
  if (!currentGroup || !itemsData[itemName]) return;
940
 
941
  const itemDef = itemsData[itemName];
942
+ const itemGeo = new THREE.BoxGeometry(0.4, 0.4, 0.4);
943
  const itemMat = MAT.simple.clone();
944
  if(itemDef.type === 'weapon') itemMat.color.setHex(0xcc6666);
945
  else if(itemDef.type === 'consumable') itemMat.color.setHex(0xcc9966);
 
953
  console.log(`Dropped ${itemName} in scene`);
954
  }
955
 
956
+ function pickupItem() {
957
+ if (gameState.combat?.active) return;
958
 
959
  raycaster.setFromCamera(mouse, camera);
960
  const currentGroup = currentSceneGroup;
961
  if (!currentGroup) return;
962
 
963
  const pickupables = [];
964
+ currentGroup.traverseVisible(child => { // Only check visible objects
965
  if (child.userData.isPickupable) pickupables.push(child);
966
  });
967
 
 
978
  if (!gameState.character.inventory.includes(itemName)) {
979
  gameState.character.inventory.push(itemName);
980
  } else {
981
+ currentMessage += `<p class="message message-info"><em>(You already have a ${itemName})</em></p>`;
982
  }
983
 
 
984
  if(clickedObject.parent) clickedObject.parent.remove(clickedObject);
985
  if(clickedObject.geometry) clickedObject.geometry.dispose();
 
986
 
987
+ renderCurrentPageUI();
988
  }
989
  }
990
  }
991
 
992
+ // --- Initialization ---
993
  document.addEventListener('DOMContentLoaded', () => {
994
  console.log("DOM Ready - Initializing Wacky D&D Shapes Adventure!");
995
  try {
 
1002
  storyTitleElement.textContent = "Initialization Error";
1003
  storyContentElement.innerHTML = `<p style="color:red;">Failed to start game:</p><pre style="color:red; white-space: pre-wrap;">${error.stack || error}</pre><p style="color:yellow;">Check console (F12) for details.</p>`;
1004
  if(sceneContainer) sceneContainer.innerHTML = '<p style="color:red; padding: 20px;">3D Scene Failed</p>';
1005
+ const statsInvContainer = document.getElementById('stats-inventory-container');
1006
+ const choicesCont = document.getElementById('choices-container');
1007
+ if (statsInvContainer) statsInvContainer.style.display = 'none';
1008
+ if (choicesCont) choicesCont.style.display = 'none';
1009
  }
1010
  });
1011