awacke1 commited on
Commit
6c1f7e8
·
verified ·
1 Parent(s): 0a69594

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +209 -223
index.html CHANGED
@@ -5,28 +5,26 @@
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>Choose Your Own Procedural Adventure</title>
7
  <style>
8
- /* --- Base Styles --- */
9
  body {
10
  font-family: 'Courier New', monospace;
11
- background-color: #222; /* Dark background */
12
- color: #eee; /* Light text */
13
  margin: 0;
14
  padding: 0;
15
- overflow: hidden; /* Prevent scrollbars */
16
  display: flex;
17
  flex-direction: column;
18
- height: 100vh; /* Full viewport height */
19
  }
20
 
21
- /* --- Layout --- */
22
  #game-container {
23
  display: flex;
24
- flex-grow: 1; /* Fill remaining vertical space */
25
  overflow: hidden;
26
  }
27
 
28
  #scene-container {
29
- flex-grow: 3; /* Give more space to 3D view */
30
  position: relative;
31
  border-right: 2px solid #555;
32
  min-width: 200px;
@@ -36,20 +34,19 @@
36
  }
37
 
38
  #ui-container {
39
- flex-grow: 2; /* Space for UI elements */
40
  padding: 20px;
41
- overflow-y: auto; /* Allow scrolling */
42
  background-color: #333;
43
  min-width: 280px;
44
  height: 100%;
45
  box-sizing: border-box;
46
- display: flex; /* Enable flex column layout */
47
  flex-direction: column;
48
  }
49
 
50
  #scene-container canvas { display: block; }
51
 
52
- /* --- UI Elements --- */
53
  #story-title {
54
  color: #ffcc66;
55
  margin-top: 0;
@@ -62,7 +59,7 @@
62
  #story-content {
63
  margin-bottom: 20px;
64
  line-height: 1.6;
65
- flex-grow: 1; /* Allow story content to expand */
66
  }
67
  #story-content p { margin-bottom: 1em; }
68
  #story-content p:last-child { margin-bottom: 0; }
@@ -90,7 +87,6 @@
90
  #stats-display strong, #inventory-display strong { color: #aaa; margin-right: 5px; }
91
  #inventory-display em { color: #888; font-style: normal; }
92
 
93
- /* Item Type Styling */
94
  #inventory-display .item-quest { background-color: #666030; border-color: #999048;}
95
  #inventory-display .item-weapon { background-color: #663030; border-color: #994848;}
96
  #inventory-display .item-armor { background-color: #306630; border-color: #489948;}
@@ -98,7 +94,7 @@
98
  #inventory-display .item-unknown { background-color: #555; border-color: #777;}
99
 
100
  #choices-container {
101
- margin-top: auto; /* Push choices to bottom if space allows */
102
  padding-top: 15px;
103
  border-top: 1px solid #555;
104
  }
@@ -116,31 +112,28 @@
116
  .choice-button:hover:not(:disabled) { background-color: #d4a017; color: #222; border-color: #b8860b; }
117
  .choice-button:disabled { background-color: #444; color: #888; cursor: not-allowed; border-color: #666; opacity: 0.7; }
118
 
 
 
 
 
119
  </style>
120
  </head>
121
  <body>
122
 
123
  <div id="game-container">
124
- <div id="scene-container">
125
- </div>
126
-
127
  <div id="ui-container">
128
  <h2 id="story-title">Loading Adventure...</h2>
129
  <div id="story-content">
130
  <p>Please wait while the adventure loads.</p>
131
  </div>
132
-
133
  <div id="stats-inventory-container">
134
- <div id="stats-display">
135
- </div>
136
- <div id="inventory-display">
137
- </div>
138
  </div>
139
-
140
  <div id="choices-container">
141
  <h3>What will you do?</h3>
142
- <div id="choices">
143
- </div>
144
  </div>
145
  </div>
146
  </div>
@@ -159,7 +152,6 @@
159
  // Optional: Add OrbitControls for debugging/viewing scene
160
  // import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
161
 
162
- // --- DOM Elements ---
163
  const sceneContainer = document.getElementById('scene-container');
164
  const storyTitleElement = document.getElementById('story-title');
165
  const storyContentElement = document.getElementById('story-content');
@@ -167,47 +159,33 @@
167
  const statsElement = document.getElementById('stats-display');
168
  const inventoryElement = document.getElementById('inventory-display');
169
 
170
- // --- Three.js Setup ---
171
  let scene, camera, renderer;
172
- let currentAssemblyGroup = null; // To hold the current scene objects
173
- // let controls; // Optional OrbitControls
174
 
175
- // --- Shared Materials ---
176
  const stoneMaterial = new THREE.MeshStandardMaterial({ color: 0x888888, roughness: 0.8, metalness: 0.1 });
177
  const woodMaterial = new THREE.MeshStandardMaterial({ color: 0x8B4513, roughness: 0.7, metalness: 0 });
178
  const darkWoodMaterial = new THREE.MeshStandardMaterial({ color: 0x5C3D20, roughness: 0.7, metalness: 0 });
179
  const leafMaterial = new THREE.MeshStandardMaterial({ color: 0x2E8B57, roughness: 0.6, metalness: 0 });
180
  const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x556B2F, roughness: 0.9, metalness: 0 });
181
  const metalMaterial = new THREE.MeshStandardMaterial({ color: 0xaaaaaa, metalness: 0.8, roughness: 0.3 });
182
- // Add other materials as needed by assemblies...
183
  const templeMaterial = new THREE.MeshStandardMaterial({ color: 0xA99B78, roughness: 0.7, metalness: 0.1 });
184
  const errorMaterial = new THREE.MeshStandardMaterial({ color: 0xffa500, roughness: 0.5 });
185
  const gameOverMaterial = new THREE.MeshStandardMaterial({ color: 0xff0000, roughness: 0.5 });
186
 
187
  function initThreeJS() {
188
- console.log("Initializing Three.js with procedural scenes...");
189
-
190
  if (!sceneContainer) { console.error("Scene container not found!"); return; }
191
-
192
  scene = new THREE.Scene();
193
  scene.background = new THREE.Color(0x222222);
194
-
195
  const width = sceneContainer.clientWidth;
196
  const height = sceneContainer.clientHeight;
197
- if (width === 0 || height === 0) { console.warn("Scene container has zero dimensions on init."); }
198
-
199
  camera = new THREE.PerspectiveCamera(75, (width / height) || 1, 0.1, 1000);
200
- camera.position.set(0, 2.5, 7); // Adjusted position for better assembly view
201
- camera.lookAt(0, 0.5, 0); // Look slightly down towards assembly center
202
-
203
  renderer = new THREE.WebGLRenderer({ antialias: true });
204
  renderer.setSize(width || 400, height || 300);
205
- renderer.shadowMap.enabled = true; // Enable shadows
206
  renderer.shadowMap.type = THREE.PCFSoftShadowMap;
207
  sceneContainer.appendChild(renderer.domElement);
208
- console.log("Renderer initialized.");
209
-
210
- // Lighting (Setup for shadows)
211
  const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
212
  scene.add(ambientLight);
213
  const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2);
@@ -217,18 +195,13 @@
217
  directionalLight.shadow.mapSize.height = 1024;
218
  directionalLight.shadow.camera.near = 0.5;
219
  directionalLight.shadow.camera.far = 50;
220
- const shadowCamSize = 15; // Adjust based on assembly size
221
  directionalLight.shadow.camera.left = -shadowCamSize; directionalLight.shadow.camera.right = shadowCamSize;
222
  directionalLight.shadow.camera.top = shadowCamSize; directionalLight.shadow.camera.bottom = -shadowCamSize;
223
  scene.add(directionalLight);
224
-
225
- // Remove the single cube creation - scene objects are handled by updateScene now
226
-
227
  window.addEventListener('resize', onWindowResize, false);
228
- setTimeout(onWindowResize, 100); // Initial resize call
229
-
230
  animate();
231
- console.log("Animation loop started.");
232
  }
233
 
234
  function onWindowResize() {
@@ -244,212 +217,190 @@
244
 
245
  function animate() {
246
  requestAnimationFrame(animate);
247
-
248
- // Optional: Add subtle animation to the entire assembly
249
- // if (currentAssemblyGroup) {
250
- // currentAssemblyGroup.rotation.y += 0.0005;
251
- // }
252
-
253
  if (renderer && scene && camera) {
254
  renderer.render(scene, camera);
255
  }
256
  }
257
 
258
- // --- Helper Functions ---
259
  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 }) {
260
  const mesh = new THREE.Mesh(geometry, material);
261
  mesh.position.set(position.x, position.y, position.z);
262
  mesh.rotation.set(rotation.x, rotation.y, rotation.z);
263
  mesh.scale.set(scale.x, scale.y, scale.z);
264
- mesh.castShadow = true;
265
- mesh.receiveShadow = true;
266
  return mesh;
267
  }
268
 
269
  function createGroundPlane(material = groundMaterial, size = 20) {
270
  const groundGeo = new THREE.PlaneGeometry(size, size);
271
  const ground = new THREE.Mesh(groundGeo, material);
272
- ground.rotation.x = -Math.PI / 2;
273
- ground.position.y = -0.05;
274
- ground.receiveShadow = true;
275
- ground.castShadow = false; // Ground itself doesn't usually cast shadows
276
  return ground;
277
  }
278
 
279
  // --- Procedural Generation Functions ---
280
- // (Using the simpler versions for now)
281
-
282
- function createDefaultAssembly() {
283
- const group = new THREE.Group();
284
- const sphereGeo = new THREE.SphereGeometry(0.5, 16, 16);
285
- group.add(createMesh(sphereGeo, stoneMaterial, { x: 0, y: 0.5, z: 0 }));
286
- group.add(createGroundPlane());
287
- return group;
288
- }
289
-
290
- function createCityGatesAssembly() {
291
- const group = new THREE.Group();
292
- const gateWallHeight = 4; const gateWallWidth = 1.5; const gateWallDepth = 0.8;
293
- const archHeight = 1; const archWidth = 3;
294
- const towerLeftGeo = new THREE.BoxGeometry(gateWallWidth, gateWallHeight, gateWallDepth);
295
- group.add(createMesh(towerLeftGeo, stoneMaterial, { x: -(archWidth / 2 + gateWallWidth / 2), y: gateWallHeight / 2, z: 0 }));
296
- const towerRightGeo = new THREE.BoxGeometry(gateWallWidth, gateWallHeight, gateWallDepth);
297
- group.add(createMesh(towerRightGeo, stoneMaterial, { x: (archWidth / 2 + gateWallWidth / 2), y: gateWallHeight / 2, z: 0 }));
298
- const archGeo = new THREE.BoxGeometry(archWidth, archHeight, gateWallDepth);
299
- group.add(createMesh(archGeo, stoneMaterial, { x: 0, y: gateWallHeight - archHeight / 2, z: 0 }));
300
- // Simplified crenellations
301
- const crenellationSize = 0.4; const crenGeo = new THREE.BoxGeometry(crenellationSize, crenellationSize, gateWallDepth * 1.1);
302
- for (let i = -1; i <= 1; i += 2) { group.add(createMesh(crenGeo.clone(), stoneMaterial, { x: -(archWidth / 2 + gateWallWidth / 2) + i * crenellationSize * 0.7, y: gateWallHeight + crenellationSize / 2, z: 0 })); group.add(createMesh(crenGeo.clone(), stoneMaterial, { x: (archWidth / 2 + gateWallWidth / 2) + i * crenellationSize * 0.7, y: gateWallHeight + crenellationSize / 2, z: 0 })); }
303
- group.add(createMesh(crenGeo.clone(), stoneMaterial, { x: 0, y: gateWallHeight + archHeight - crenellationSize / 2, z: 0 }));
304
-
305
- group.add(createGroundPlane(stoneMaterial));
306
- return group;
307
- }
308
-
309
- function createWeaponsmithAssembly() {
310
- // Simple Box Building + Chimney
311
- const group = new THREE.Group();
312
- const buildingWidth = 3; const buildingHeight = 2.5; const buildingDepth = 3.5;
313
- const buildingGeo = new THREE.BoxGeometry(buildingWidth, buildingHeight, buildingDepth);
314
- group.add(createMesh(buildingGeo, darkWoodMaterial, { x: 0, y: buildingHeight / 2, z: 0 }));
315
- const chimneyHeight = 3.5; const chimneyGeo = new THREE.CylinderGeometry(0.3, 0.4, chimneyHeight, 8);
316
- group.add(createMesh(chimneyGeo, stoneMaterial, { x: buildingWidth * 0.3, y: chimneyHeight / 2, z: -buildingDepth * 0.3 }));
317
- group.add(createGroundPlane());
318
- return group;
319
- }
320
-
321
- function createTempleAssembly() {
322
- // Simple Base + Columns + Roof Slab
323
- const group = new THREE.Group();
324
- const baseSize = 5; const baseHeight = 0.5; const columnHeight = 3; const columnRadius = 0.25; const roofHeight = 0.5;
325
- const baseGeo = new THREE.BoxGeometry(baseSize, baseHeight, baseSize); group.add(createMesh(baseGeo, templeMaterial, { x: 0, y: baseHeight / 2, z: 0 }));
326
- const colGeo = new THREE.CylinderGeometry(columnRadius, columnRadius, columnHeight, 12);
327
- const colPositions = [ { x: -baseSize / 3, z: -baseSize / 3 }, { x: baseSize / 3, z: -baseSize / 3 }, { x: -baseSize / 3, z: baseSize / 3 }, { x: baseSize / 3, z: baseSize / 3 }];
328
- colPositions.forEach(pos => group.add(createMesh(colGeo.clone(), templeMaterial, { x: pos.x, y: baseHeight + columnHeight / 2, z: pos.z })));
329
- const roofGeo = new THREE.BoxGeometry(baseSize * 0.9, roofHeight, baseSize * 0.9); group.add(createMesh(roofGeo, templeMaterial, { x: 0, y: baseHeight + columnHeight + roofHeight / 2, z: 0 }));
330
- group.add(createGroundPlane());
331
- return group;
332
- }
333
-
334
- function createResistanceMeetingAssembly() {
335
- // Simple table + stools
336
- const group = new THREE.Group();
337
- const tableWidth = 2; const tableHeight = 0.8; const tableDepth = 1; const tableThickness = 0.1;
338
- const tableTopGeo = new THREE.BoxGeometry(tableWidth, tableThickness, tableDepth); group.add(createMesh(tableTopGeo, woodMaterial, { x: 0, y: tableHeight - tableThickness / 2, z: 0 }));
339
- const legHeight = tableHeight - tableThickness; const legSize = 0.1; const legGeo = new THREE.BoxGeometry(legSize, legHeight, legSize); const legOffsetW = tableWidth / 2 - legSize * 1.5; const legOffsetD = tableDepth / 2 - legSize * 1.5; group.add(createMesh(legGeo, woodMaterial, { x: -legOffsetW, y: legHeight / 2, z: -legOffsetD })); group.add(createMesh(legGeo.clone(), woodMaterial, { x: legOffsetW, y: legHeight / 2, z: -legOffsetD })); group.add(createMesh(legGeo.clone(), woodMaterial, { x: -legOffsetW, y: legHeight / 2, z: legOffsetD })); group.add(createMesh(legGeo.clone(), woodMaterial, { x: legOffsetW, y: legHeight / 2, z: legOffsetD }));
340
- const stoolSize = 0.4; const stoolGeo = new THREE.BoxGeometry(stoolSize, stoolSize * 0.8, stoolSize); group.add(createMesh(stoolGeo, darkWoodMaterial, { x: -tableWidth * 0.6, y: stoolSize * 0.4, z: 0 })); group.add(createMesh(stoolGeo.clone(), darkWoodMaterial, { x: tableWidth * 0.6, y: stoolSize * 0.4, z: 0 }));
341
- group.add(createGroundPlane(stoneMaterial));
342
- return group;
343
- }
344
-
345
- function createForestAssembly(treeCount = 10, area = 10) {
346
- // Simple Trees (Cylinder + Sphere)
347
- const group = new THREE.Group();
348
- const createTree = (x, z) => { const treeGroup = new THREE.Group(); const trunkHeight = Math.random() * 1.5 + 2; const trunkRadius = Math.random() * 0.1 + 0.1; const trunkGeo = new THREE.CylinderGeometry(trunkRadius * 0.7, trunkRadius, trunkHeight, 8); treeGroup.add(createMesh(trunkGeo, woodMaterial, { x: 0, y: trunkHeight / 2, z: 0 })); const foliageRadius = trunkHeight * 0.4 + 0.2; const foliageGeo = new THREE.SphereGeometry(foliageRadius, 8, 6); treeGroup.add(createMesh(foliageGeo, leafMaterial, { x: 0, y: trunkHeight * 0.9, z: 0 })); treeGroup.position.set(x, 0, z); return treeGroup; };
349
- for (let i = 0; i < treeCount; i++) { const x = (Math.random() - 0.5) * area; const z = (Math.random() - 0.5) * area; if (Math.sqrt(x*x + z*z) > 1.0) group.add(createTree(x, z)); } // Avoid center
350
- group.add(createGroundPlane(groundMaterial, area * 1.1));
351
- return group;
352
- }
353
-
354
- function createRoadAmbushAssembly() { /* ... (calls simple createForestAssembly) ... */
355
- const group = new THREE.Group(); const area = 12;
356
- const forestGroup = createForestAssembly(8, area); group.add(forestGroup); // Fewer trees
357
- const roadWidth = 3; const roadLength = area * 1.2; const roadGeo = new THREE.PlaneGeometry(roadWidth, roadLength); const roadMaterial = new THREE.MeshStandardMaterial({ color: 0x966F33, roughness: 0.9 }); const road = createMesh(roadGeo, roadMaterial, {x: 0, y: 0.01, z: 0}, {x: -Math.PI / 2}); road.receiveShadow = true; group.add(road);
358
- const rockGeo = new THREE.SphereGeometry(0.5, 5, 4); const rockMaterial = new THREE.MeshStandardMaterial({ color: 0x666666, roughness: 0.8 }); group.add(createMesh(rockGeo, rockMaterial, {x: roadWidth * 0.7, y: 0.25, z: 1}, {y: Math.random() * Math.PI})); group.add(createMesh(rockGeo.clone().scale(0.8,0.8,0.8), rockMaterial, {x: -roadWidth * 0.8, y: 0.2, z: -2}, {y: Math.random() * Math.PI}));
359
- return group;
360
- }
361
- function createForestEdgeAssembly() { /* ... (calls simple createForestAssembly) ... */
362
- const group = new THREE.Group(); const area = 15;
363
- const forestGroup = createForestAssembly(15, area); // Generate full forest
364
- // Keep only trees on one side
365
- const treesToRemove = [];
366
- forestGroup.children.forEach(child => { if(child.type === 'Group' && child.position.x > 0) { treesToRemove.push(child); } }); // Mark trees on positive X
367
- treesToRemove.forEach(tree => forestGroup.remove(tree)); // Remove them
368
- group.add(forestGroup); // Add remaining trees and ground
369
- return group;
370
- }
371
- function createPrisonerCellAssembly() { /* ... (Simplified version) ... */
372
- const group = new THREE.Group(); const cellSize = 3; const wallHeight = 2.5; const wallThickness = 0.2; const barRadius = 0.05; const barSpacing = 0.25;
373
- const cellFloorMaterial = stoneMaterial.clone(); cellFloorMaterial.color.setHex(0x555555); group.add(createGroundPlane(cellFloorMaterial, cellSize));
374
- const wallBackGeo = new THREE.BoxGeometry(cellSize, wallHeight, wallThickness); group.add(createMesh(wallBackGeo, stoneMaterial, { x: 0, y: wallHeight / 2, z: -cellSize / 2 })); const wallSideGeo = new THREE.BoxGeometry(wallThickness, wallHeight, cellSize); group.add(createMesh(wallSideGeo, stoneMaterial, { x: -cellSize / 2, y: wallHeight / 2, z: 0 })); group.add(createMesh(wallSideGeo.clone(), stoneMaterial, { x: cellSize / 2, y: wallHeight / 2, z: 0 }));
375
- const barGeo = new THREE.CylinderGeometry(barRadius, barRadius, wallHeight, 8); const numBars = Math.floor(cellSize / barSpacing); for (let i = 0; i < numBars; i++) { const xPos = -cellSize / 2 + (i + 0.5) * barSpacing; group.add(createMesh(barGeo.clone(), metalMaterial, { x: xPos, y: wallHeight / 2, z: cellSize / 2 })); }
376
- return group;
377
- }
378
- function createGameOverAssembly() { /* ... (same as before) ... */
379
- const group = new THREE.Group(); const boxGeo = new THREE.BoxGeometry(2, 2, 2); group.add(createMesh(boxGeo, gameOverMaterial, { x: 0, y: 1, z: 0 })); group.add(createGroundPlane(stoneMaterial.clone().set({color: 0x333333}))); return group;
380
- }
381
- function createErrorAssembly() { /* ... (same as before) ... */
382
- const group = new THREE.Group(); const coneGeo = new THREE.ConeGeometry( 0.8, 1.5, 8 ); group.add(createMesh(coneGeo, errorMaterial, { x: 0, y: 0.75, z: 0 })); group.add(createGroundPlane()); return group;
383
- }
384
 
385
  // --- Game Data ---
 
386
  const gameData = {
387
- "1": { title: "The Beginning", content: `<p>...</p>`, options: [ { text: "Visit the local weaponsmith", next: 2 }, { text: "Seek wisdom at the temple", next: 3 }, { text: "Meet the resistance leader", next: 4 } ], illustration: "city-gates" },
388
- "2": { title: "The Weaponsmith", content: `<p>...</p>`, options: [ { text: "Take the Flaming Sword", next: 5, addItem: "Flaming Sword" }, { text: "Choose the Whispering Bow", next: 5, addItem: "Whispering Bow" }, { text: "Select the Guardian Shield", next: 5, addItem: "Guardian Shield" } ], illustration: "weaponsmith" },
389
- "3": { title: "The Ancient Temple", content: `<p>...</p>`, options: [ { text: "Learn Healing Light", next: 5, addItem: "Healing Light Spell" }, { text: "Master Shield of Faith", next: 5, addItem: "Shield of Faith Spell" }, { text: "Study Binding Runes", next: 5, addItem: "Binding Runes Scroll" } ], illustration: "temple" },
390
- "4": { title: "The Resistance Leader", content: `<p>...</p>`, options: [ { text: "Take the Secret Tunnel Map", next: 5, addItem: "Secret Tunnel Map" }, { text: "Accept Poison Daggers", next: 5, addItem: "Poison Daggers" }, { text: "Choose the Master Key", next: 5, addItem: "Master Key" } ], illustration: "resistance-meeting" },
391
- "5": { title: "The Journey Begins", content: `<p>...</p>`, options: [ { text: "Take the main road", next: 6 }, { text: "Follow the river path", next: 7 }, { text: "Brave the ruins shortcut", next: 8 } ], illustration: "shadowwood-forest" },
392
- "6": { title: "Ambush!", content: "<p>...</p>", options: [{ text: "Fight!", next: 9 }, { text: "Try to flee!", next: 10 }], illustration: "road-ambush" },
393
- "7": { title: "River Path", content: "<p>...</p>", options: [{ text: "Keep going", next: 15 }], illustration: "river-spirit" /* Uses default for now */ },
394
- "8": { title: "Ancient Ruins", content: "<p>...</p>", options: [{ text: "Search carefully", next: 15 }], illustration: "ancient-ruins" /* Uses default for now */ },
395
- "9": { title: "Victory!", content: "<p>...</p>", options: [{ text: "Proceed", next: 15 }], illustration: "forest-edge", reward: { statIncrease: { stat: "strength", amount: 1 } } }, // Example reward added back
396
- "10": { title: "Captured!", content: "<p>...</p>", options: [{ text: "Accept fate", next: 20 }], illustration: "prisoner-cell" },
397
- "15": { title: "Fortress Plains", content: "<p>...</p>", options: [{ text: "March onward", next: 99 }], illustration: "fortress-plains" /* Uses default */ },
398
- "20": { title: "Prison Cell", content: "<p>...</p>", options: [{ text: "Wait", next: 99 }], illustration: "prisoner-cell" },
399
- "99": { title: "Game Over", content: "<p>...</p>", options: [{ text: "Restart", next: 1 }], illustration: "game-over", gameOver: true }
400
- };
401
- const itemsData = {
402
- "Flaming Sword": { type: "weapon", description: "A fiery blade" }, "Whispering Bow": { type: "weapon", description: "A silent bow" }, "Guardian Shield": { type: "armor", description: "A protective shield" }, "Healing Light Spell": { type: "spell", description: "Mends minor wounds" }, "Shield of Faith Spell": { type: "spell", description: "Temporary shield" }, "Binding Runes Scroll": { type: "spell", description: "Binds an enemy" }, "Secret Tunnel Map": { type: "quest", description: "Shows a hidden path" }, "Poison Daggers": { type: "weapon", description: "Daggers with poison" }, "Master Key": { type: "quest", description: "Unlocks many doors" },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
403
  };
404
 
405
  // --- Game State ---
406
  let gameState = {
407
- currentPageId: 1, inventory: [],
408
- stats: { courage: 7, wisdom: 5, strength: 6, hp: 30, maxHp: 30 }
 
 
 
 
 
409
  };
410
 
411
  // --- Game Logic Functions ---
412
  function startGame() {
413
- gameState = { currentPageId: 1, inventory: [], stats: { courage: 7, wisdom: 5, strength: 6, hp: 30, maxHp: 30 } };
 
 
 
414
  renderPage(gameState.currentPageId);
415
  }
416
 
417
- function renderPage(pageId) {
418
- const page = gameData[pageId];
419
- if (!page) { console.error(`Page data error for ID: ${pageId}`); storyTitleElement.textContent = "Error"; storyContentElement.innerHTML = "<p>Could not load page data.</p>"; choicesElement.innerHTML = '<button class="choice-button" onclick="window.location.reload()">Restart</button>'; updateScene('error'); return; } // Changed restart to reload for simplicity here
420
- storyTitleElement.textContent = page.title || "Untitled Page"; storyContentElement.innerHTML = page.content || "<p>...</p>";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
421
  updateStatsDisplay(); updateInventoryDisplay();
422
  choicesElement.innerHTML = '';
423
- if (page.options && page.options.length > 0) {
424
- page.options.forEach(option => {
425
  const button = document.createElement('button'); button.classList.add('choice-button'); button.textContent = option.text; let requirementMet = true;
426
- if (option.requireItem && !gameState.inventory.includes(option.requireItem)) { requirementMet = false; button.title = `Requires: ${option.requireItem}`; button.disabled = true; }
427
- if (requirementMet) { const choiceData = { nextPage: option.next }; if (option.addItem) { choiceData.addItem = option.addItem; } button.onclick = () => handleChoiceClick(choiceData); } else { button.classList.add('disabled'); } choicesElement.appendChild(button); });
428
- } else { const button = document.createElement('button'); button.classList.add('choice-button'); button.textContent = page.gameOver ? "Restart Adventure" : "Continue (End)"; button.onclick = () => handleChoiceClick({ nextPage: page.gameOver ? 1 : 99 }); choicesElement.appendChild(button); }
429
- updateScene(page.illustration || 'default');
430
  }
431
 
432
- function handleChoiceClick(choiceData) {
433
- const nextPageId = parseInt(choiceData.nextPage); const itemToAdd = choiceData.addItem; if (isNaN(nextPageId)) { console.error("Invalid nextPageId:", choiceData.nextPage); return; }
434
- if (itemToAdd && !gameState.inventory.includes(itemToAdd)) { gameState.inventory.push(itemToAdd); console.log("Added item:", itemToAdd); }
435
- gameState.currentPageId = nextPageId; const nextPageData = gameData[nextPageId];
436
- if (nextPageData) {
437
- if (nextPageData.hpLoss) { gameState.stats.hp = Math.max(0, gameState.stats.hp - nextPageData.hpLoss); console.log(`Lost ${nextPageData.hpLoss} HP.`); if (gameState.stats.hp <= 0) { console.log("Player died!"); renderPage(99); return; } }
438
- if (nextPageData.reward && nextPageData.reward.statIncrease) { const stat = nextPageData.reward.statIncrease.stat; const amount = nextPageData.reward.statIncrease.amount; if (gameState.stats.hasOwnProperty(stat)) { gameState.stats[stat] += amount; console.log(`Stat ${stat} increased by ${amount}.`); } }
439
- // Add item reward processing if needed from reward object
440
- if (nextPageData.reward && nextPageData.reward.addItem && !gameState.inventory.includes(nextPageData.reward.addItem)) { gameState.inventory.push(nextPageData.reward.addItem); console.log(`Found item: ${nextPageData.reward.addItem}`); }
441
- }
442
- else { console.error(`Data for page ${nextPageId} not found!`); renderPage(99); return; }
443
- renderPage(nextPageId); // Render the target page
444
- }
445
 
446
- function updateStatsDisplay() { statsElement.innerHTML = `<strong>Stats:</strong> <span>HP: ${gameState.stats.hp}/${gameState.stats.maxHp}</span> <span>Str: ${gameState.stats.strength}</span> <span>Wis: ${gameState.stats.wisdom}</span> <span>Cor: ${gameState.stats.courage}</span>`; }
447
- function updateInventoryDisplay() { let inventoryHTML = '<strong>Inventory:</strong> '; if (gameState.inventory.length === 0) { inventoryHTML += '<em>Empty</em>'; } else { gameState.inventory.forEach(item => { const itemInfo = itemsData[item] || { type: 'unknown', description: '???' }; const itemClass = `item-${itemInfo.type || 'unknown'}`; inventoryHTML += `<span class="${itemClass}" title="${itemInfo.description}">${item}</span>`; }); } inventoryElement.innerHTML = inventoryHTML; }
448
 
449
- // Scene Update Function (using procedural assemblies)
450
  function updateScene(illustrationKey) {
451
- console.log(`Updating scene to: ${illustrationKey}`);
452
- if (currentAssemblyGroup) { scene.remove(currentAssemblyGroup); /* TODO: Proper disposal? */ }
453
  currentAssemblyGroup = null; let assemblyFunction;
454
  switch (illustrationKey) {
455
  case 'city-gates': assemblyFunction = createCityGatesAssembly; break;
@@ -460,19 +411,54 @@
460
  case 'road-ambush': assemblyFunction = createRoadAmbushAssembly; break;
461
  case 'forest-edge': assemblyFunction = createForestEdgeAssembly; break;
462
  case 'prisoner-cell': assemblyFunction = createPrisonerCellAssembly; break;
463
- case 'game-over': assemblyFunction = createGameOverAssembly; break;
464
  case 'error': assemblyFunction = createErrorAssembly; break;
465
- // Add cases for other keys, falling back to default
466
- case 'river-spirit': case 'ancient-ruins': case 'fortress-plains': // Fall through
467
- default: console.warn(`No specific assembly for key: "${illustrationKey}". Using default.`); assemblyFunction = createDefaultAssembly; break;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
468
  }
469
  try { currentAssemblyGroup = assemblyFunction(); scene.add(currentAssemblyGroup); } catch (error) { console.error(`Error creating assembly for ${illustrationKey}:`, error); currentAssemblyGroup = createErrorAssembly(); scene.add(currentAssemblyGroup); }
470
  }
471
 
472
- // --- Initialization ---
473
  document.addEventListener('DOMContentLoaded', () => {
474
- console.log("DOM Ready. Initializing...");
475
- try { initThreeJS(); startGame(); } catch (error) { console.error("Initialization failed:", error); storyTitleElement.textContent = "Error"; storyContentElement.innerHTML = `<p>Initialization Error. Check console (F12).</p><pre>${error}</pre>`; }
476
  });
477
 
478
  </script>
 
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>Choose Your Own Procedural Adventure</title>
7
  <style>
 
8
  body {
9
  font-family: 'Courier New', monospace;
10
+ background-color: #222;
11
+ color: #eee;
12
  margin: 0;
13
  padding: 0;
14
+ overflow: hidden;
15
  display: flex;
16
  flex-direction: column;
17
+ height: 100vh;
18
  }
19
 
 
20
  #game-container {
21
  display: flex;
22
+ flex-grow: 1;
23
  overflow: hidden;
24
  }
25
 
26
  #scene-container {
27
+ flex-grow: 3;
28
  position: relative;
29
  border-right: 2px solid #555;
30
  min-width: 200px;
 
34
  }
35
 
36
  #ui-container {
37
+ flex-grow: 2;
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;
45
  flex-direction: column;
46
  }
47
 
48
  #scene-container canvas { display: block; }
49
 
 
50
  #story-title {
51
  color: #ffcc66;
52
  margin-top: 0;
 
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; }
 
87
  #stats-display strong, #inventory-display strong { color: #aaa; margin-right: 5px; }
88
  #inventory-display em { color: #888; font-style: normal; }
89
 
 
90
  #inventory-display .item-quest { background-color: #666030; border-color: #999048;}
91
  #inventory-display .item-weapon { background-color: #663030; border-color: #994848;}
92
  #inventory-display .item-armor { background-color: #306630; border-color: #489948;}
 
94
  #inventory-display .item-unknown { background-color: #555; border-color: #777;}
95
 
96
  #choices-container {
97
+ margin-top: auto;
98
  padding-top: 15px;
99
  border-top: 1px solid #555;
100
  }
 
112
  .choice-button:hover:not(:disabled) { background-color: #d4a017; color: #222; border-color: #b8860b; }
113
  .choice-button:disabled { background-color: #444; color: #888; cursor: not-allowed; border-color: #666; opacity: 0.7; }
114
 
115
+ /* Optional Roll Result Styling */
116
+ .roll-success { color: #7f7; border-left: 3px solid #4a4; padding-left: 8px; margin-bottom: 1em; font-size: 0.9em; }
117
+ .roll-failure { color: #f77; border-left: 3px solid #a44; padding-left: 8px; margin-bottom: 1em; font-size: 0.9em; }
118
+
119
  </style>
120
  </head>
121
  <body>
122
 
123
  <div id="game-container">
124
+ <div id="scene-container"></div>
 
 
125
  <div id="ui-container">
126
  <h2 id="story-title">Loading Adventure...</h2>
127
  <div id="story-content">
128
  <p>Please wait while the adventure loads.</p>
129
  </div>
 
130
  <div id="stats-inventory-container">
131
+ <div id="stats-display"></div>
132
+ <div id="inventory-display"></div>
 
 
133
  </div>
 
134
  <div id="choices-container">
135
  <h3>What will you do?</h3>
136
+ <div id="choices"></div>
 
137
  </div>
138
  </div>
139
  </div>
 
152
  // Optional: Add OrbitControls for debugging/viewing scene
153
  // import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
154
 
 
155
  const sceneContainer = document.getElementById('scene-container');
156
  const storyTitleElement = document.getElementById('story-title');
157
  const storyContentElement = document.getElementById('story-content');
 
159
  const statsElement = document.getElementById('stats-display');
160
  const inventoryElement = document.getElementById('inventory-display');
161
 
 
162
  let scene, camera, renderer;
163
+ let currentAssemblyGroup = null;
 
164
 
 
165
  const stoneMaterial = new THREE.MeshStandardMaterial({ color: 0x888888, roughness: 0.8, metalness: 0.1 });
166
  const woodMaterial = new THREE.MeshStandardMaterial({ color: 0x8B4513, roughness: 0.7, metalness: 0 });
167
  const darkWoodMaterial = new THREE.MeshStandardMaterial({ color: 0x5C3D20, roughness: 0.7, metalness: 0 });
168
  const leafMaterial = new THREE.MeshStandardMaterial({ color: 0x2E8B57, roughness: 0.6, metalness: 0 });
169
  const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x556B2F, roughness: 0.9, metalness: 0 });
170
  const metalMaterial = new THREE.MeshStandardMaterial({ color: 0xaaaaaa, metalness: 0.8, roughness: 0.3 });
 
171
  const templeMaterial = new THREE.MeshStandardMaterial({ color: 0xA99B78, roughness: 0.7, metalness: 0.1 });
172
  const errorMaterial = new THREE.MeshStandardMaterial({ color: 0xffa500, roughness: 0.5 });
173
  const gameOverMaterial = new THREE.MeshStandardMaterial({ color: 0xff0000, roughness: 0.5 });
174
 
175
  function initThreeJS() {
 
 
176
  if (!sceneContainer) { console.error("Scene container not found!"); return; }
 
177
  scene = new THREE.Scene();
178
  scene.background = new THREE.Color(0x222222);
 
179
  const width = sceneContainer.clientWidth;
180
  const height = sceneContainer.clientHeight;
 
 
181
  camera = new THREE.PerspectiveCamera(75, (width / height) || 1, 0.1, 1000);
182
+ camera.position.set(0, 2.5, 7);
183
+ camera.lookAt(0, 0.5, 0);
 
184
  renderer = new THREE.WebGLRenderer({ antialias: true });
185
  renderer.setSize(width || 400, height || 300);
186
+ renderer.shadowMap.enabled = true;
187
  renderer.shadowMap.type = THREE.PCFSoftShadowMap;
188
  sceneContainer.appendChild(renderer.domElement);
 
 
 
189
  const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
190
  scene.add(ambientLight);
191
  const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2);
 
195
  directionalLight.shadow.mapSize.height = 1024;
196
  directionalLight.shadow.camera.near = 0.5;
197
  directionalLight.shadow.camera.far = 50;
198
+ const shadowCamSize = 15;
199
  directionalLight.shadow.camera.left = -shadowCamSize; directionalLight.shadow.camera.right = shadowCamSize;
200
  directionalLight.shadow.camera.top = shadowCamSize; directionalLight.shadow.camera.bottom = -shadowCamSize;
201
  scene.add(directionalLight);
 
 
 
202
  window.addEventListener('resize', onWindowResize, false);
203
+ setTimeout(onWindowResize, 100);
 
204
  animate();
 
205
  }
206
 
207
  function onWindowResize() {
 
217
 
218
  function animate() {
219
  requestAnimationFrame(animate);
220
+ // if (currentAssemblyGroup) { currentAssemblyGroup.rotation.y += 0.0005; } // Optional rotation
 
 
 
 
 
221
  if (renderer && scene && camera) {
222
  renderer.render(scene, camera);
223
  }
224
  }
225
 
 
226
  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 }) {
227
  const mesh = new THREE.Mesh(geometry, material);
228
  mesh.position.set(position.x, position.y, position.z);
229
  mesh.rotation.set(rotation.x, rotation.y, rotation.z);
230
  mesh.scale.set(scale.x, scale.y, scale.z);
231
+ mesh.castShadow = true; mesh.receiveShadow = true;
 
232
  return mesh;
233
  }
234
 
235
  function createGroundPlane(material = groundMaterial, size = 20) {
236
  const groundGeo = new THREE.PlaneGeometry(size, size);
237
  const ground = new THREE.Mesh(groundGeo, material);
238
+ ground.rotation.x = -Math.PI / 2; ground.position.y = -0.05;
239
+ ground.receiveShadow = true; ground.castShadow = false;
 
 
240
  return ground;
241
  }
242
 
243
  // --- Procedural Generation Functions ---
244
+ 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; }
245
+ 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; }
246
+ function createWeaponsmithAssembly() { const group = new THREE.Group(); const bw=3, bh=2.5, bd=3.5; const bGeo = new THREE.BoxGeometry(bw, bh, bd); group.add(createMesh(bGeo, darkWoodMaterial, { x:0, y:bh/2, z:0 })); const ch=3.5; const cGeo = new THREE.CylinderGeometry(0.3, 0.4, ch, 8); group.add(createMesh(cGeo, stoneMaterial, { x:bw*0.3, y:ch/2, z:-bd*0.3 })); group.add(createGroundPlane()); return group; }
247
+ function createTempleAssembly() { const group = new THREE.Group(); const bs=5, bsh=0.5, ch=3, cr=0.25, rh=0.5; const bGeo = new THREE.BoxGeometry(bs, bsh, bs); group.add(createMesh(bGeo, templeMaterial, { x:0, y:bsh/2, z:0 })); const cGeo = new THREE.CylinderGeometry(cr, cr, ch, 12); const cPos = [{x:-bs/3, z:-bs/3}, {x:bs/3, z:-bs/3}, {x:-bs/3, z:bs/3}, {x:bs/3, z:bs/3}]; cPos.forEach(p=>group.add(createMesh(cGeo.clone(), templeMaterial, { x:p.x, y:bsh+ch/2, z:p.z }))); const rGeo = new THREE.BoxGeometry(bs*0.9, rh, bs*0.9); group.add(createMesh(rGeo, templeMaterial, { x:0, y:bsh+ch+rh/2, z:0 })); group.add(createGroundPlane()); return group; }
248
+ function createResistanceMeetingAssembly() { const group = new THREE.Group(); const tw=2, th=0.8, td=1, tt=0.1; const ttg = new THREE.BoxGeometry(tw, tt, td); group.add(createMesh(ttg, woodMaterial, { x:0, y:th-tt/2, z:0 })); const lh=th-tt, ls=0.1; const lg=new THREE.BoxGeometry(ls, lh, ls); const lofW=tw/2-ls*1.5; const lofD=td/2-ls*1.5; group.add(createMesh(lg, woodMaterial, { x:-lofW, y:lh/2, z:-lofD })); group.add(createMesh(lg.clone(), woodMaterial, { x:lofW, y:lh/2, z:-lofD })); group.add(createMesh(lg.clone(), woodMaterial, { x:-lofW, y:lh/2, z:lofD })); group.add(createMesh(lg.clone(), woodMaterial, { x:lofW, y:lh/2, z:lofD })); const ss=0.4; const sg=new THREE.BoxGeometry(ss, ss*0.8, ss); group.add(createMesh(sg, darkWoodMaterial, { x:-tw*0.6, y:ss*0.4, z:0 })); group.add(createMesh(sg.clone(), darkWoodMaterial, { x:tw*0.6, y:ss*0.4, z:0 })); group.add(createGroundPlane(stoneMaterial)); return group; }
249
+ function createForestAssembly(tc=10, a=10) { const group = new THREE.Group(); const cT=(x,z)=>{ const tg=new THREE.Group(); const th=Math.random()*1.5+2; const tr=Math.random()*0.1+0.1; const tGeo = new THREE.CylinderGeometry(tr*0.7, tr, th, 8); tg.add(createMesh(tGeo, woodMaterial, {x:0, y:th/2, z:0})); const fr=th*0.4+0.2; const fGeo=new THREE.SphereGeometry(fr, 8, 6); tg.add(createMesh(fGeo, leafMaterial, {x:0, y:th*0.9, z:0})); tg.position.set(x,0,z); return tg; }; for(let i=0; i<tc; i++){ const x=(Math.random()-0.5)*a; const z=(Math.random()-0.5)*a; if(Math.sqrt(x*x+z*z)>1.0) group.add(cT(x,z)); } group.add(createGroundPlane(groundMaterial, a*1.1)); return group; }
250
+ function createRoadAmbushAssembly() { const group = new THREE.Group(); const a=12; const fg = createForestAssembly(8, a); group.add(fg); const rw=3, rl=a*1.2; const rGeo=new THREE.PlaneGeometry(rw, rl); const rMat=new THREE.MeshStandardMaterial({color:0x966F33, roughness:0.9}); const r=createMesh(rGeo, rMat, {x:0, y:0.01, z:0}, {x:-Math.PI/2}); r.receiveShadow=true; group.add(r); const rkGeo=new THREE.SphereGeometry(0.5, 5, 4); const rkMat=new THREE.MeshStandardMaterial({color:0x666666, roughness:0.8}); group.add(createMesh(rkGeo, rkMat, {x:rw*0.7, y:0.25, z:1}, {y:Math.random()*Math.PI})); group.add(createMesh(rkGeo.clone().scale(0.8,0.8,0.8), rkMat, {x:-rw*0.8, y:0.2, z:-2}, {y:Math.random()*Math.PI})); return group; }
251
+ function createForestEdgeAssembly() { const group = new THREE.Group(); const a=15; const fg = createForestAssembly(15, a); const ttr=[]; fg.children.forEach(c => { if(c.type === 'Group' && c.position.x > 0) ttr.push(c); }); ttr.forEach(t => fg.remove(t)); group.add(fg); return group; }
252
+ function createPrisonerCellAssembly() { const group = new THREE.Group(); const cs=3, wh=2.5, wt=0.2, br=0.05, bsp=0.25; const cfMat=stoneMaterial.clone(); cfMat.color.setHex(0x555555); group.add(createGroundPlane(cfMat, cs)); const wbGeo=new THREE.BoxGeometry(cs, wh, wt); group.add(createMesh(wbGeo, stoneMaterial, {x:0, y:wh/2, z:-cs/2})); const wsGeo=new THREE.BoxGeometry(wt, wh, cs); group.add(createMesh(wsGeo, stoneMaterial, {x:-cs/2, y:wh/2, z:0})); group.add(createMesh(wsGeo.clone(), stoneMaterial, {x:cs/2, y:wh/2, z:0})); const bGeo=new THREE.CylinderGeometry(br, br, wh, 8); const nb=Math.floor(cs/bsp); for(let i=0; i<nb; i++){ const xp=-cs/2+(i+0.5)*bsp; group.add(createMesh(bGeo.clone(), metalMaterial, {x:xp, y:wh/2, z:cs/2})); } return group; }
253
+ function createGameOverAssembly() { const group = new THREE.Group(); const boxGeo = new THREE.BoxGeometry(2, 2, 2); group.add(createMesh(boxGeo, gameOverMaterial, { x: 0, y: 1, z: 0 })); group.add(createGroundPlane(stoneMaterial.clone().set({color: 0x333333}))); return group; }
254
+ function createErrorAssembly() { const group = new THREE.Group(); const coneGeo = new THREE.ConeGeometry( 0.8, 1.5, 8 ); group.add(createMesh(coneGeo, errorMaterial, { x: 0, y: 0.75, z: 0 })); group.add(createGroundPlane()); return group; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
 
256
  // --- Game Data ---
257
+ const itemsData = { "Flaming Sword":{type:"weapon", description:"A fiery blade"}, "Whispering Bow":{type:"weapon", description:"A silent bow"}, "Guardian Shield":{type:"armor", description:"A protective shield"}, "Healing Light Spell":{type:"spell", description:"Mends minor wounds"}, "Shield of Faith Spell":{type:"spell", description:"Temporary shield"}, "Binding Runes Scroll":{type:"spell", description:"Binds an enemy"}, "Secret Tunnel Map":{type:"quest", description:"Shows a hidden path"}, "Poison Daggers":{type:"weapon", description:"Daggers with poison"}, "Master Key":{type:"quest", description:"Unlocks many doors"}, "Crude Dagger":{type:"weapon", description:"A roughly made dagger."}, "Scout's Pouch":{type:"quest", description:"Contains odds and ends."} };
258
  const gameData = {
259
+ "1": { title: "The Crossroads", content: `<p>Dust swirls... 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" },
260
+ "2": { title: "Rolling Hills", content: `<p>Verdant hills stretch before you... It feels peaceful...</p>`, options: [ { text: "Follow the narrow path", next: 4 }, { text: "Try to hail the distant shepherd (Charisma Check?)", next: 99 } ], illustration: "rolling-green-hills-shepherd-distance" },
261
+ "3": { title: "Coastal Cliffs Edge", content: `<p>You stand atop windswept cliffs... A precarious-looking path descends...</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" },
262
+ "4": { title: "Hill Path Overlook", content: `<p>The path crests a hill... you see a small, overgrown shrine...</p>`, options: [ { text: "Investigate the overgrown shrine", next: 40 }, { text: "Continue towards the badlands", next: 41 } ], illustration: "hilltop-view-overgrown-shrine-wildflowers" },
263
+ "5": { title: "Shadowwood Entrance", content: `<p>Sunlight struggles to pierce the dense canopy... How do you proceed?</p>`, options: [ { text: "Follow the main, albeit overgrown, path", next: 6 }, { text: "Try to navigate through the lighter undergrowth", 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" },
264
+ "6": { title: "Overgrown Forest Path", content: `<p>The path is barely visible... You hear a twig snap nearby!</p>`, options: [ { text: "Ready your weapon and investigate", next: 10 }, { text: "Attempt to hide quietly (Dexterity Check)", check: { stat: 'dexterity', dc: 11, onFailure: 10 }, next: 11 }, { text: "Call out cautiously", next: 10 } ], illustration: "overgrown-forest-path-glowing-fungi-vines" },
265
+ "7": { title: "Tangled Undergrowth", content: `<p>Pushing through ferns... You stumble upon a small clearing containing a moss-covered, weathered stone statue...</p>`, options: [ { text: "Examine the statue closely (Intelligence Check)", check: { stat: 'intelligence', dc: 13, onFailure: 71 }, next: 70 }, { text: "Ignore the statue and press on", next: 72 }, { text: "Leave a small offering (if possible)", next: 73 } ], illustration: "forest-clearing-mossy-statue-weathered-stone" },
266
+ "8": { title: "Hidden Game Trail", content: `<p>Your sharp eyes spot a faint trail... It leads towards a ravine spanned by a 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 for another way across the ravine", next: 82 } ], illustration: "narrow-game-trail-forest-rope-bridge-ravine", reward: { xp: 20 } },
267
+ "10": { title: "Goblin Ambush!", content: `<p>Two scraggly goblins leap out, brandishing crude spears!</p>`, options: [ { text: "Fight the goblins!", next: 12 }, { text: "Attempt to dodge past them (Dexterity Check)", check: { stat: 'dexterity', dc: 13, onFailure: 10 }, next: 13 } ], illustration: "two-goblins-ambush-forest-path-spears" }, // Simplified options
268
+ "11": { title: "Hidden Evasion", content: `<p>You melt into the shadows as the goblins blunder past.</p><p>(+30 XP)</p>`, options: [ { text: "Continue cautiously", next: 14 } ], illustration: "forest-shadows-hiding-goblins-walking-past", reward: { xp: 30 } },
269
+ "12": { title: "Ambush Victory!", content: `<p>You defeat the goblins! Found a Crude Dagger.</p><p>(+50 XP)</p>`, options: [ { text: "Press onward", next: 14 } ], illustration: "defeated-goblins-forest-path-loot", reward: { xp: 50, addItem: "Crude Dagger" } }, // Added item directly
270
+ "13": { title: "Daring Escape", content: `<p>With surprising agility, you tumble past the goblins!</p><p>(+25 XP)</p>`, options: [ { text: "Keep running!", next: 14 } ], illustration: "blurred-motion-running-past-goblins-forest", reward: { xp: 25 } },
271
+ "14": { title: "Forest Stream Crossing", content: `<p>The path leads to a clear, shallow stream...</p>`, options: [ { text: "Wade across the stream", next: 16 }, { text: "Look for a drier crossing point (fallen log?)", next: 15 } ], illustration: "forest-stream-crossing-dappled-sunlight-stones" },
272
+ "15": { title: "Log Bridge", content: `<p>Further upstream, a large, mossy log spans the stream.</p>`, options: [ { text: "Cross carefully on the log (Dexterity Check)", check: { stat: 'dexterity', dc: 9, onFailure: 151 }, next: 16 }, { text: "Go back and wade instead", next: 14 } ], illustration: "mossy-log-bridge-over-forest-stream" },
273
+ "151": { title: "Splash!", content: `<p>You slip on the mossy log and tumble into the cold stream! You're soaked but unharmed.</p>`, options: [ { text: "Shake yourself off and continue", next: 16 } ], illustration: "character-splashing-into-stream-from-log" },
274
+ "16": { title: "Edge of the Woods", content: `<p>You emerge from the Shadowwood... Before you lie rocky foothills...</p>`, options: [ { text: "Begin the ascent into the foothills", next: 17 }, { text: "Scan the fortress from afar (Wisdom Check)", check: { stat: 'wisdom', dc: 14, onFailure: 17 }, next: 18 } ], illustration: "forest-edge-view-rocky-foothills-distant-mountain-fortress" },
275
+ "17": { title: "Rocky Foothills Path", content: `<p>The climb is arduous... The fortress looms larger now.</p>`, options: [ { text: "Continue the direct ascent", next: 19 }, { text: "Look for signs of a hidden trail (Wisdom Check)", check: { stat: 'wisdom', dc: 15, onFailure: 19 }, next: 20 } ], illustration: "climbing-rocky-foothills-path-fortress-closer" },
276
+ "18": { title: "Distant Observation", content: `<p>You notice what might be a less-guarded approach along the western ridge...</p><p>(+30 XP)</p>`, options: [ { text: "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 } },
277
+ "19": { title: "Blocked Pass", content: `<p>The main path is blocked by a recent rockslide!</p>`, options: [ { text: "Try to climb over (Strength Check)", check: { stat: 'strength', dc: 14, onFailure: 191 }, next: 190 }, { text: "Search for another way around", next: 192 } ], illustration: "rockslide-blocking-mountain-path-boulders" },
278
+ "20": { title: "Goat Trail", content: `<p>You discover a narrow trail barely wide enough for a mountain goat...</p><p>(+40 XP)</p>`, options: [ { text: "Follow the precarious goat trail", next: 22 } ], illustration: "narrow-goat-trail-mountainside-fortress-view", reward: { xp: 40 } },
279
+ "30": { title: "Hidden Cove", content: `<p>Your careful descent brings you to a secluded cove. A dark cave entrance is visible...</p><p>(+25 XP)</p>`, options: [ { text: "Explore the dark cave", next: 35 } ], illustration: "hidden-cove-beach-dark-cave-entrance", reward: { xp: 25 } },
280
+ "31": { title: "Tumbled Down", content: `<p>You lose your footing... landing hard on the sandy cove floor. You lose 5 HP. A dark cave entrance beckons.</p>`, options: [ { text: "Gingerly explore the dark cave", next: 35 } ], illustration: "character-fallen-at-bottom-of-cliff-path-cove", hpLoss: 5 },
281
+ "32": { title: "No Easier Path", content: `<p>You scan the cliffs intently but find no obviously easier routes.</p>`, options: [ { text: "Attempt the precarious descent (Dexterity Check)", check: { stat: 'dexterity', dc: 12, onFailure: 31 }, next: 30 } ], illustration: "scanning-sea-cliffs-no-other-paths-visible" },
282
+ "33": { title: "Smuggler's Steps?", content: `<p>Your keen eyes spot a series of barely visible handholds and steps carved into the rock...</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 } },
283
+ "35": { title: "Dark Cave", content: `<p>The cave smells of salt and decay. Water drips somewhere within.</p>`, options: [{ text: "Press deeper into the darkness", next: 99 } ], illustration: "dark-cave-entrance-dripping-water" }, // End of this branch for now
284
+ "40": { title: "Overgrown Shrine", content: `<p>Wildflowers grow thick around a small stone shrine. It feels ancient and neglected.</p>`, options: [{ text: "Examine the carvings (Intelligence Check)", check:{stat:'intelligence', dc:11, onFailure: 401}, next: 400 } ], illustration: "overgrown-stone-shrine-wildflowers-close" },
285
+ "41": { title: "Rocky Badlands", content: `<p>The green hills give way to cracked earth and jagged rock formations under a harsh sun.</p>`, options: [{ text: "Scout ahead", next: 99 } ], illustration: "rocky-badlands-cracked-earth-harsh-sun" }, // End of this branch
286
+ // Add pages 70-73, 80-82, 190-192, 21, 22, 400, 401 etc.
287
+ "190": { title: "Over the Rocks", content:"<p>With considerable effort, you manage to climb over the rockslide.</p><p>(+35 XP)</p>", options: [{text:"Continue up the path", next: 22}], illustration:"character-climbing-over-boulders", reward: {xp:35} },
288
+ "191": { title: "Climb Fails", content:"<p>The boulders are too unstable or sheer. You cannot climb them safely.</p>", options: [{text:"Search for another way around", next: 192}], illustration:"character-slipping-on-rockslide-boulders"},
289
+ "192": { title: "Detour Found", content:"<p>After some searching, you find a rough path leading around the rockslide, eventually rejoining the main trail.</p>", options: [{text:"Continue up the path", next: 22}], illustration:"rough-detour-path-around-rockslide"},
290
+ "21": { title: "Western Ridge", content:"<p>The ridge path is narrow and exposed, with strong winds threatening to push you off.</p>", options: [{text:"Proceed carefully (Dexterity Check)", check:{stat:'dexterity', dc: 14, onFailure: 211}, next: 22 } ], illustration:"narrow-windy-mountain-ridge-path" },
291
+ "22": { title: "Fortress Approach", content:"<p>You've navigated the treacherous paths and now stand near the outer walls of the dark fortress. Guards patrol the battlements.</p>", options: [{text:"Look for an unguarded entrance", next: 99}], illustration:"approaching-dark-fortress-walls-guards"}, // End for now
292
+ "211": {title:"Lost Balance", content:"<p>A strong gust of wind catches you off guard, sending you tumbling down a steep slope! You lose 10 HP.</p>", options:[{text:"Climb back up and find another way", next: 17}], illustration:"character-falling-off-windy-ridge", hpLoss: 10},
293
+
294
+ "99": { title: "Game Over / To Be Continued...", content: "<p>Your adventure ends here (for now).</p>", options: [{ text: "Restart", next: 1 }], illustration: "game-over-generic", gameOver: true }
295
  };
296
 
297
  // --- Game State ---
298
  let gameState = {
299
+ currentPageId: 1,
300
+ character: {
301
+ name: "Hero", race: "Human", alignment: "Neutral Good", class: "Adventurer",
302
+ level: 1, xp: 0, xpToNextLevel: 100,
303
+ stats: { strength: 8, intelligence: 10, wisdom: 10, dexterity: 10, constitution: 10, charisma: 8, hp: 12, maxHp: 12 },
304
+ inventory: []
305
+ }
306
  };
307
 
308
  // --- Game Logic Functions ---
309
  function startGame() {
310
+ const defaultChar = { name: "Hero", race: "Human", alignment: "Neutral Good", class: "Adventurer", level: 1, xp: 0, xpToNextLevel: 100, stats: { strength: 8, intelligence: 10, wisdom: 10, dexterity: 10, constitution: 10, charisma: 8, hp: 12, maxHp: 12 }, inventory: [] };
311
+ gameState = { currentPageId: 1, character: { ...defaultChar } }; // Reset state
312
+ // Potential load logic could go here later:
313
+ // if (loadCharacter()) { console.log("Loaded saved game."); }
314
  renderPage(gameState.currentPageId);
315
  }
316
 
317
+ function handleChoiceClick(choiceData) {
318
+ const optionNextPageId = parseInt(choiceData.nextPage);
319
+ const itemToAdd = choiceData.addItem;
320
+ let nextPageId = optionNextPageId;
321
+ let rollResultMessage = "";
322
+ const check = choiceData.check; // Get check data if it exists
323
+
324
+ if (isNaN(optionNextPageId) && !check) { console.error("Invalid choice data:", choiceData); return; }
325
+
326
+ if (check) {
327
+ const statValue = gameState.character.stats[check.stat] || 10;
328
+ const modifier = Math.floor((statValue - 10) / 2);
329
+ const roll = Math.floor(Math.random() * 20) + 1;
330
+ const totalResult = roll + modifier;
331
+ const dc = check.dc;
332
+ console.log(`Check: ${check.stat} (DC ${dc}) | Roll: ${roll} + Mod: ${modifier} = ${totalResult}`);
333
+ if (totalResult >= dc) {
334
+ nextPageId = optionNextPageId; // Check succeeds, use original 'next'
335
+ rollResultMessage = `<p class="roll-success"><em>Check Success! (${totalResult} vs DC ${dc})</em></p>`;
336
+ } else {
337
+ nextPageId = parseInt(check.onFailure); // Check fails, use 'onFailure'
338
+ rollResultMessage = `<p class="roll-failure"><em>Check Failed! (${totalResult} vs DC ${dc})</em></p>`;
339
+ if (isNaN(nextPageId)) { console.error("Invalid onFailure ID:", check.onFailure); nextPageId = 99; }
340
+ }
341
+ }
342
+
343
+ if (itemToAdd && !gameState.character.inventory.includes(itemToAdd)) {
344
+ gameState.character.inventory.push(itemToAdd);
345
+ console.log("Added item:", itemToAdd);
346
+ }
347
+
348
+ gameState.currentPageId = nextPageId;
349
+ const nextPageData = gameData[nextPageId];
350
+
351
+ if (nextPageData) {
352
+ if (nextPageData.hpLoss) {
353
+ gameState.character.stats.hp -= nextPageData.hpLoss;
354
+ console.log(`Lost ${nextPageData.hpLoss} HP.`);
355
+ if (gameState.character.stats.hp <= 0) { gameState.character.stats.hp = 0; console.log("Player died!"); nextPageId = 99; /* Force redirect handled below */ }
356
+ }
357
+ if (nextPageData.reward) {
358
+ if (nextPageData.reward.xp) { gameState.character.xp += nextPageData.reward.xp; console.log(`Gained ${nextPageData.reward.xp} XP! Total: ${gameState.character.xp}`); }
359
+ if (nextPageData.reward.statIncrease) { const stat = nextPageData.reward.statIncrease.stat; const amount = nextPageData.reward.statIncrease.amount; if (gameState.character.stats.hasOwnProperty(stat)) { gameState.character.stats[stat] += amount; console.log(`Stat ${stat} increased by ${amount}.`); } }
360
+ if(nextPageData.reward.addItem && !gameState.character.inventory.includes(nextPageData.reward.addItem)){ gameState.character.inventory.push(nextPageData.reward.addItem); console.log(`Found item: ${nextPageData.reward.addItem}`); }
361
+ }
362
+ // Recalculate max HP based on CON after potential stat changes
363
+ const conModifier = Math.floor((gameState.character.stats.constitution - 10) / 2);
364
+ // Example Max HP calculation: Base + CON modifier per level
365
+ gameState.character.stats.maxHp = 10 + (conModifier * gameState.character.level); // Adjust base 10 as needed
366
+ gameState.character.stats.hp = Math.min(gameState.character.stats.hp, gameState.character.stats.maxHp); // Clamp HP
367
+
368
+ // Handle forced game over from HP loss
369
+ if (nextPageId === 99 && gameState.character.stats.hp <= 0) {
370
+ renderPageInternal(99, gameData[99], rollResultMessage);
371
+ return;
372
+ }
373
+ } else {
374
+ console.error(`Data for page ${nextPageId} not found!`);
375
+ renderPageInternal(99, gameData[99], "<p><em>Error: Next page data missing!</em></p>");
376
+ return;
377
+ }
378
+ renderPageInternal(nextPageId, gameData[nextPageId], rollResultMessage);
379
+ }
380
+
381
+ function renderPageInternal(pageId, pageData, message = "") {
382
+ if (!pageData) { console.error(`Render Error: No data for page ${pageId}`); return; } // Guard clause
383
+ storyTitleElement.textContent = pageData.title || "Untitled Page";
384
+ storyContentElement.innerHTML = message + (pageData.content || "<p>...</p>");
385
  updateStatsDisplay(); updateInventoryDisplay();
386
  choicesElement.innerHTML = '';
387
+ if (pageData.options && pageData.options.length > 0) {
388
+ pageData.options.forEach(option => {
389
  const button = document.createElement('button'); button.classList.add('choice-button'); button.textContent = option.text; let requirementMet = true;
390
+ if (option.requireItem && !gameState.character.inventory.includes(option.requireItem)) { requirementMet = false; button.title = `Requires: ${option.requireItem}`; button.disabled = true; }
391
+ if (requirementMet) { const choiceData = { nextPage: option.next, addItem: option.addItem, check: option.check }; button.onclick = () => handleChoiceClick(choiceData); } else { button.classList.add('disabled'); } choicesElement.appendChild(button); });
392
+ } else { const button = document.createElement('button'); button.classList.add('choice-button'); button.textContent = pageData.gameOver ? "Restart Adventure" : "The End"; button.onclick = () => handleChoiceClick({ nextPage: pageData.gameOver ? 1 : 99 }); choicesElement.appendChild(button); if (!pageData.gameOver) choicesElement.insertAdjacentHTML('afterbegin', '<p><i>The path ends here.</i></p>'); }
393
+ updateScene(pageData.illustration || 'default');
394
  }
395
 
396
+ function renderPage(pageId) { renderPageInternal(pageId, gameData[pageId]); }
 
 
 
 
 
 
 
 
 
 
 
 
397
 
398
+ function updateStatsDisplay() { const char=gameState.character; statsElement.innerHTML = `<strong>Stats:</strong> <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>`; }
399
+ function updateInventoryDisplay() { let h='<strong>Inventory:</strong> '; if(gameState.character.inventory.length === 0){ h+='<em>Empty</em>'; } else { gameState.character.inventory.forEach(i=>{ const d=itemsData[i]||{type:'unknown',description:'???'}; const c=`item-${d.type||'unknown'}`; h+=`<span class="${c}" title="${d.description}">${i}</span>`; }); } inventoryElement.innerHTML = h; }
400
 
 
401
  function updateScene(illustrationKey) {
402
+ // console.log(`Updating scene to: ${illustrationKey}`); // Minimal log
403
+ if (currentAssemblyGroup) { scene.remove(currentAssemblyGroup); }
404
  currentAssemblyGroup = null; let assemblyFunction;
405
  switch (illustrationKey) {
406
  case 'city-gates': assemblyFunction = createCityGatesAssembly; break;
 
411
  case 'road-ambush': assemblyFunction = createRoadAmbushAssembly; break;
412
  case 'forest-edge': assemblyFunction = createForestEdgeAssembly; break;
413
  case 'prisoner-cell': assemblyFunction = createPrisonerCellAssembly; break;
414
+ case 'game-over': case 'game-over-generic': assemblyFunction = createGameOverAssembly; break; // Added generic key
415
  case 'error': assemblyFunction = createErrorAssembly; break;
416
+ case 'crossroads-signpost-sunny': // New keys added
417
+ case 'rolling-green-hills-shepherd-distance':
418
+ case 'windy-sea-cliffs-crashing-waves-path-down':
419
+ case 'hilltop-view-overgrown-shrine-wildflowers':
420
+ case 'dark-forest-entrance-gnarled-roots-filtered-light':
421
+ case 'overgrown-forest-path-glowing-fungi-vines':
422
+ case 'forest-clearing-mossy-statue-weathered-stone':
423
+ case 'narrow-game-trail-forest-rope-bridge-ravine':
424
+ case 'two-goblins-ambush-forest-path-spears':
425
+ case 'forest-shadows-hiding-goblins-walking-past':
426
+ case 'defeated-goblins-forest-path-loot':
427
+ case 'blurred-motion-running-past-goblins-forest':
428
+ case 'forest-stream-crossing-dappled-sunlight-stones':
429
+ case 'mossy-log-bridge-over-forest-stream':
430
+ case 'character-splashing-into-stream-from-log':
431
+ case 'forest-edge-view-rocky-foothills-distant-mountain-fortress':
432
+ case 'climbing-rocky-foothills-path-fortress-closer':
433
+ case 'zoomed-view-mountain-fortress-western-ridge':
434
+ case 'rockslide-blocking-mountain-path-boulders':
435
+ case 'narrow-goat-trail-mountainside-fortress-view':
436
+ case 'hidden-cove-beach-dark-cave-entrance':
437
+ case 'character-fallen-at-bottom-of-cliff-path-cove':
438
+ case 'scanning-sea-cliffs-no-other-paths-visible':
439
+ case 'close-up-handholds-carved-in-cliff-face':
440
+ case 'dark-cave-entrance-dripping-water':
441
+ case 'overgrown-stone-shrine-wildflowers-close':
442
+ case 'rocky-badlands-cracked-earth-harsh-sun':
443
+ case 'character-climbing-over-boulders':
444
+ case 'character-slipping-on-rockslide-boulders':
445
+ case 'rough-detour-path-around-rockslide':
446
+ case 'narrow-windy-mountain-ridge-path':
447
+ case 'approaching-dark-fortress-walls-guards':
448
+ case 'character-falling-off-windy-ridge':
449
+ // Add specific assembly functions for these later or assign existing ones
450
+ console.warn(`Assembly function not yet defined for: "${illustrationKey}". Using default.`);
451
+ assemblyFunction = createDefaultAssembly; break;
452
+ default:
453
+ console.warn(`Unknown illustration key: "${illustrationKey}". Using default.`);
454
+ assemblyFunction = createDefaultAssembly; break;
455
  }
456
  try { currentAssemblyGroup = assemblyFunction(); scene.add(currentAssemblyGroup); } catch (error) { console.error(`Error creating assembly for ${illustrationKey}:`, error); currentAssemblyGroup = createErrorAssembly(); scene.add(currentAssemblyGroup); }
457
  }
458
 
 
459
  document.addEventListener('DOMContentLoaded', () => {
460
+ console.log("DOM Ready.");
461
+ try { initThreeJS(); startGame(); } catch (error) { console.error("Init failed:", error); storyTitleElement.textContent = "Error"; storyContentElement.innerHTML = `<p>Init Error. Check console.</p><pre>${error}</pre>`; }
462
  });
463
 
464
  </script>