awacke1 commited on
Commit
44717a8
·
verified ·
1 Parent(s): 4b81814

Update game.js

Browse files
Files changed (1) hide show
  1. game.js +510 -75
game.js CHANGED
@@ -11,39 +11,71 @@ const statsElement = document.getElementById('stats-display');
11
  const inventoryElement = document.getElementById('inventory-display');
12
 
13
  // --- Three.js Setup ---
14
- let scene, camera, renderer, cube; // Basic scene object
 
15
  // let controls; // Optional OrbitControls
16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  function initThreeJS() {
18
  // Scene
19
  scene = new THREE.Scene();
20
  scene.background = new THREE.Color(0x222222); // Match body background
 
21
 
22
  // Camera
23
  camera = new THREE.PerspectiveCamera(75, sceneContainer.clientWidth / sceneContainer.clientHeight, 0.1, 1000);
24
- camera.position.z = 5;
 
25
 
26
  // Renderer
27
  renderer = new THREE.WebGLRenderer({ antialias: true });
28
  renderer.setSize(sceneContainer.clientWidth, sceneContainer.clientHeight);
 
 
29
  sceneContainer.appendChild(renderer.domElement);
30
 
31
  // Basic Lighting
32
- const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); // Soft white light
33
  scene.add(ambientLight);
34
- const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0);
35
- directionalLight.position.set(5, 10, 7.5);
 
 
 
 
 
 
 
 
 
 
36
  scene.add(directionalLight);
 
 
 
 
 
37
 
38
- // Basic Object (Placeholder for scene illustration)
39
- const geometry = new THREE.BoxGeometry(1, 1, 1);
40
- const material = new THREE.MeshStandardMaterial({ color: 0xcccccc }); // Default color
41
- cube = new THREE.Mesh(geometry, material);
42
- scene.add(cube);
43
 
44
  // Optional Controls
45
  // controls = new OrbitControls(camera, renderer.domElement);
46
  // controls.enableDamping = true;
 
47
 
48
  // Handle Resize
49
  window.addEventListener('resize', onWindowResize, false);
@@ -52,6 +84,344 @@ function initThreeJS() {
52
  animate();
53
  }
54
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  function onWindowResize() {
56
  if (!renderer || !camera) return;
57
  camera.aspect = sceneContainer.clientWidth / sceneContainer.clientHeight;
@@ -62,10 +432,10 @@ function onWindowResize() {
62
  function animate() {
63
  requestAnimationFrame(animate);
64
 
65
- // Simple animation
66
- if (cube) {
67
- cube.rotation.x += 0.005;
68
- cube.rotation.y += 0.005;
69
  }
70
 
71
  // if (controls) controls.update(); // If using OrbitControls
@@ -122,39 +492,63 @@ const gameData = {
122
  content: `<p>You leave Silverhold and enter the corrupted Shadowwood Forest. Strange sounds echo. Which path will you take?</p>`,
123
  options: [
124
  { text: "Take the main road", next: 6 }, // Leads to page 6 (Ambush)
125
- { text: "Follow the river path", next: 7 }, // Leads to page 7 (River Spirit)
126
- { text: "Brave the ruins shortcut", next: 8 } // Leads to page 8 (Ruins)
127
  ],
128
  illustration: "shadowwood-forest" // Key for Three.js scene
129
  // Add more pages here...
130
  },
131
  // Add placeholder pages 6, 7, 8 etc. to continue the story
132
  "6": {
133
- title: "Ambush!",
134
- content: "<p>Scouts jump out! 'Surrender!'</p>",
135
- options: [{ text: "Fight!", next: 9 }, { text: "Try to flee!", next: 10 }], // Example links
136
- illustration: "road-ambush"
 
 
 
 
 
 
 
 
 
 
 
 
137
  },
138
- // ... Add many more pages based on your Python data ...
139
  "9": { // Example continuation
140
- title: "Victory!",
141
- content: "<p>You defeat the scouts and continue.</p>",
142
- options: [{ text: "Proceed to the fortress plains", next: 15 }],
143
- illustration: "forest-edge"
144
  },
145
  "10": { // Example continuation
146
- title: "Captured!",
147
- content: "<p>You failed to escape and are captured!</p>",
148
- options: [{ text: "Accept fate (for now)", next: 20 }], // Go to prison wagon page
149
- illustration: "prisoner-cell"
 
 
 
 
 
 
 
 
 
 
 
 
 
150
  },
151
  // Game Over placeholder
152
  "99": {
153
- title: "Game Over",
154
- content: "<p>Your adventure ends here.</p>",
155
- options: [{ text: "Restart", next: 1 }], // Link back to start
156
- illustration: "game-over",
157
- gameOver: true
158
  }
159
  };
160
 
@@ -201,7 +595,7 @@ function renderPage(pageId) {
201
  console.error(`Error: Page data not found for ID: ${pageId}`);
202
  storyTitleElement.textContent = "Error";
203
  storyContentElement.innerHTML = "<p>Could not load page data. Adventure halted.</p>";
204
- choicesElement.innerHTML = '<button class="choice-button" onclick="handleChoice(1)">Restart</button>'; // Provide restart option
205
  updateScene('error'); // Show error scene
206
  return;
207
  }
@@ -230,14 +624,18 @@ function renderPage(pageId) {
230
  // Add requireAnyItem check here later if needed
231
 
232
  if (requirementMet) {
233
- // Store data needed for handling the choice
234
- button.dataset.nextPage = option.next;
235
- if (option.addItem) {
236
- button.dataset.addItem = option.addItem;
237
- }
238
- // Add other potential effects as data attributes if needed
239
-
240
- button.onclick = () => handleChoiceClick(button.dataset);
 
 
 
 
241
  }
242
 
243
  choicesElement.appendChild(button);
@@ -246,16 +644,15 @@ function renderPage(pageId) {
246
  const button = document.createElement('button');
247
  button.classList.add('choice-button');
248
  button.textContent = "Restart Adventure";
249
- button.dataset.nextPage = 1; // Restart goes to page 1
250
- button.onclick = () => handleChoiceClick(button.dataset);
251
  choicesElement.appendChild(button);
252
  } else {
253
- choicesElement.innerHTML = '<p><i>No further options available from here.</i></p>';
 
254
  const button = document.createElement('button');
255
  button.classList.add('choice-button');
256
  button.textContent = "Restart Adventure";
257
- button.dataset.nextPage = 1; // Restart goes to page 1
258
- button.onclick = () => handleChoiceClick(button.dataset);
259
  choicesElement.appendChild(button);
260
  }
261
 
@@ -264,12 +661,15 @@ function renderPage(pageId) {
264
  updateScene(page.illustration || 'default');
265
  }
266
 
267
- function handleChoiceClick(dataset) {
268
- const nextPageId = parseInt(dataset.nextPage); // Ensure it's a number
269
- const itemToAdd = dataset.addItem;
 
 
 
270
 
271
  if (isNaN(nextPageId)) {
272
- console.error("Invalid nextPageId:", dataset.nextPage);
273
  return;
274
  }
275
 
@@ -353,31 +753,67 @@ function updateInventoryDisplay() {
353
 
354
  function updateScene(illustrationKey) {
355
  console.log("Updating scene for:", illustrationKey);
356
- if (!cube) return; // Don't do anything if cube isn't initialized
357
 
358
- // Simple scene update: Change cube color based on key
359
- let color = 0xcccccc; // Default grey
 
 
 
 
 
 
 
 
 
 
 
 
 
 
360
  switch (illustrationKey) {
361
- case 'city-gates': color = 0xaaaaaa; break;
362
- case 'weaponsmith': color = 0x8B4513; break; // Brown
363
- case 'temple': color = 0xFFFFE0; break; // Light yellow
364
- case 'resistance-meeting': color = 0x696969; break; // Dim grey
365
- case 'shadowwood-forest': color = 0x228B22; break; // Forest green
366
- case 'road-ambush': color = 0xD2691E; break; // Chocolate (dirt road)
367
- case 'river-spirit': color = 0xADD8E6; break; // Light blue
368
- case 'ancient-ruins': color = 0x778899; break; // Light slate grey
369
- case 'forest-edge': color = 0x90EE90; break; // Light green
370
- case 'prisoner-cell': color = 0x444444; break; // Dark grey
371
- case 'game-over': color = 0xff0000; break; // Red
372
- case 'error': color = 0xffa500; break; // Orange
373
- default: color = 0xcccccc; break; // Default grey for unknown
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
374
  }
375
- cube.material.color.setHex(color);
376
 
377
- // In a more complex setup, you would:
378
- // 1. Remove old objects from the scene (scene.remove(object))
379
- // 2. Load/create new objects based on illustrationKey
380
- // 3. Add new objects to the scene (scene.add(newObject))
 
 
 
 
 
 
 
 
 
381
  }
382
 
383
 
@@ -385,6 +821,5 @@ function updateScene(illustrationKey) {
385
  initThreeJS();
386
  startGame(); // Start the game after setting up Three.js
387
 
388
- // Make handleChoiceClick globally accessible IF using inline onclick
389
- // If using addEventListener, this is not needed.
390
  // window.handleChoiceClick = handleChoiceClick;
 
11
  const inventoryElement = document.getElementById('inventory-display');
12
 
13
  // --- Three.js Setup ---
14
+ let scene, camera, renderer; // Basic scene objects
15
+ let currentAssemblyGroup = null; // Group to hold the current scene's objects
16
  // let controls; // Optional OrbitControls
17
 
18
+ // --- Shared Materials (Define common materials here for reuse) ---
19
+ const stoneMaterial = new THREE.MeshStandardMaterial({ color: 0x888888, roughness: 0.8, metalness: 0.1 });
20
+ const woodMaterial = new THREE.MeshStandardMaterial({ color: 0x8B4513, roughness: 0.7, metalness: 0 });
21
+ const darkWoodMaterial = new THREE.MeshStandardMaterial({ color: 0x5C3D20, roughness: 0.7, metalness: 0 });
22
+ const leafMaterial = new THREE.MeshStandardMaterial({ color: 0x2E8B57, roughness: 0.6, metalness: 0 }); // SeaGreen
23
+ const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x556B2F, roughness: 0.9, metalness: 0 }); // DarkOliveGreen
24
+ const metalMaterial = new THREE.MeshStandardMaterial({ color: 0xaaaaaa, metalness: 0.8, roughness: 0.3 });
25
+ const fabricMaterial = new THREE.MeshStandardMaterial({ color: 0x696969, roughness: 0.9, metalness: 0 }); // DimGray
26
+ const waterMaterial = new THREE.MeshStandardMaterial({ color: 0x60A3D9, roughness: 0.2, metalness: 0.1, transparent: true, opacity: 0.7 });
27
+ const templeMaterial = new THREE.MeshStandardMaterial({ color: 0xA99B78, roughness: 0.7, metalness: 0.1 }); // Light stone/sandstone
28
+ const fireMaterial = new THREE.MeshStandardMaterial({ color: 0xFF4500, emissive: 0xff6600, roughness: 0.5, metalness: 0 }); // OrangeRed emissive
29
+ const errorMaterial = new THREE.MeshStandardMaterial({ color: 0xffa500, roughness: 0.5 }); // Orange
30
+ const gameOverMaterial = new THREE.MeshStandardMaterial({ color: 0xff0000, roughness: 0.5 }); // Red
31
+
32
+
33
  function initThreeJS() {
34
  // Scene
35
  scene = new THREE.Scene();
36
  scene.background = new THREE.Color(0x222222); // Match body background
37
+ // scene.fog = new THREE.Fog(0x222222, 8, 20); // Optional: Add fog for depth
38
 
39
  // Camera
40
  camera = new THREE.PerspectiveCamera(75, sceneContainer.clientWidth / sceneContainer.clientHeight, 0.1, 1000);
41
+ camera.position.set(0, 2.5, 7); // Adjusted camera position for better view
42
+ camera.lookAt(0, 0, 0);
43
 
44
  // Renderer
45
  renderer = new THREE.WebGLRenderer({ antialias: true });
46
  renderer.setSize(sceneContainer.clientWidth, sceneContainer.clientHeight);
47
+ renderer.shadowMap.enabled = true; // Enable shadows
48
+ renderer.shadowMap.type = THREE.PCFSoftShadowMap; // Softer shadows
49
  sceneContainer.appendChild(renderer.domElement);
50
 
51
  // Basic Lighting
52
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.5); // Soft white light
53
  scene.add(ambientLight);
54
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2);
55
+ directionalLight.position.set(8, 15, 10);
56
+ directionalLight.castShadow = true; // Enable shadow casting
57
+ // Configure shadow properties (optional, adjust for quality/performance)
58
+ directionalLight.shadow.mapSize.width = 1024;
59
+ directionalLight.shadow.mapSize.height = 1024;
60
+ directionalLight.shadow.camera.near = 0.5;
61
+ directionalLight.shadow.camera.far = 50;
62
+ directionalLight.shadow.camera.left = -15;
63
+ directionalLight.shadow.camera.right = 15;
64
+ directionalLight.shadow.camera.top = 15;
65
+ directionalLight.shadow.camera.bottom = -15;
66
  scene.add(directionalLight);
67
+ // const lightHelper = new THREE.DirectionalLightHelper(directionalLight, 5); // Helper to visualize light
68
+ // scene.add(lightHelper);
69
+ // const shadowCameraHelper = new THREE.CameraHelper(directionalLight.shadow.camera); // Helper for shadow camera
70
+ // scene.add(shadowCameraHelper);
71
+
72
 
73
+ // REMOVED: Basic Object (Placeholder) - Will be added dynamically
 
 
 
 
74
 
75
  // Optional Controls
76
  // controls = new OrbitControls(camera, renderer.domElement);
77
  // controls.enableDamping = true;
78
+ // controls.target.set(0, 1, 0); // Adjust target if needed
79
 
80
  // Handle Resize
81
  window.addEventListener('resize', onWindowResize, false);
 
84
  animate();
85
  }
86
 
87
+ // --- Helper function to create meshes ---
88
+ 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 }) {
89
+ const mesh = new THREE.Mesh(geometry, material);
90
+ mesh.position.set(position.x, position.y, position.z);
91
+ mesh.rotation.set(rotation.x, rotation.y, rotation.z);
92
+ mesh.scale.set(scale.x, scale.y, scale.z);
93
+ mesh.castShadow = true;
94
+ mesh.receiveShadow = true;
95
+ return mesh;
96
+ }
97
+
98
+ // --- Procedural Generation Functions ---
99
+
100
+ function createGroundPlane(material = groundMaterial, size = 20) {
101
+ const groundGeo = new THREE.PlaneGeometry(size, size);
102
+ const ground = new THREE.Mesh(groundGeo, material);
103
+ ground.rotation.x = -Math.PI / 2; // Rotate flat
104
+ ground.position.y = -0.05; // Slightly below origin
105
+ ground.receiveShadow = true;
106
+ return ground;
107
+ }
108
+
109
+ function createDefaultAssembly() {
110
+ const group = new THREE.Group();
111
+ const sphereGeo = new THREE.SphereGeometry(0.5, 16, 16);
112
+ group.add(createMesh(sphereGeo, stoneMaterial, { x: 0, y: 0.5, z: 0 }));
113
+ group.add(createGroundPlane());
114
+ return group;
115
+ }
116
+
117
+ function createCityGatesAssembly() {
118
+ const group = new THREE.Group();
119
+ const gateWallHeight = 4;
120
+ const gateWallWidth = 1.5;
121
+ const gateWallDepth = 0.8;
122
+ const archHeight = 1;
123
+ const archWidth = 3;
124
+
125
+ // Left Tower
126
+ const towerLeftGeo = new THREE.BoxGeometry(gateWallWidth, gateWallHeight, gateWallDepth);
127
+ group.add(createMesh(towerLeftGeo, stoneMaterial, { x: -(archWidth / 2 + gateWallWidth / 2), y: gateWallHeight / 2, z: 0 }));
128
+
129
+ // Right Tower
130
+ const towerRightGeo = new THREE.BoxGeometry(gateWallWidth, gateWallHeight, gateWallDepth);
131
+ group.add(createMesh(towerRightGeo, stoneMaterial, { x: (archWidth / 2 + gateWallWidth / 2), y: gateWallHeight / 2, z: 0 }));
132
+
133
+ // Arch Top
134
+ const archGeo = new THREE.BoxGeometry(archWidth, archHeight, gateWallDepth);
135
+ group.add(createMesh(archGeo, stoneMaterial, { x: 0, y: gateWallHeight - archHeight / 2, z: 0 }));
136
+
137
+ // Optional: Add crenellations (battlements)
138
+ const crenellationSize = 0.4;
139
+ for (let i = -2; i <= 2; i += 1) {
140
+ const crenGeo = new THREE.BoxGeometry(crenellationSize, crenellationSize, gateWallDepth * 1.1);
141
+ group.add(createMesh(crenGeo, stoneMaterial, { x: -(archWidth / 2 + gateWallWidth / 2) + i * crenellationSize * 1.5, y: gateWallHeight + crenellationSize / 2, z: 0 }));
142
+ group.add(createMesh(crenGeo, stoneMaterial, { x: (archWidth / 2 + gateWallWidth / 2) + i * crenellationSize * 1.5, y: gateWallHeight + crenellationSize / 2, z: 0 }));
143
+ }
144
+
145
+ group.add(createGroundPlane(stoneMaterial)); // Stone ground
146
+ return group;
147
+ }
148
+
149
+ function createWeaponsmithAssembly() {
150
+ const group = new THREE.Group();
151
+ const buildingWidth = 3;
152
+ const buildingHeight = 2.5;
153
+ const buildingDepth = 3.5;
154
+ const roofHeight = 1;
155
+
156
+ // Main Building
157
+ const buildingGeo = new THREE.BoxGeometry(buildingWidth, buildingHeight, buildingDepth);
158
+ group.add(createMesh(buildingGeo, darkWoodMaterial, { x: 0, y: buildingHeight / 2, z: 0 }));
159
+
160
+ // Roof (simple triangular prism shape made of two planes)
161
+ const roofGeo = new THREE.PlaneGeometry(buildingWidth * 1.1, Math.sqrt(Math.pow(buildingDepth / 2, 2) + Math.pow(roofHeight, 2)));
162
+ const roofLeft = createMesh(roofGeo, woodMaterial, { x: 0, y: buildingHeight + roofHeight / 2, z: -buildingDepth / 4 }, { x: 0, y: 0, z: Math.atan2(roofHeight, buildingDepth / 2) });
163
+ const roofRight = createMesh(roofGeo, woodMaterial, { x: 0, y: buildingHeight + roofHeight / 2, z: buildingDepth / 4 }, { x: 0, y: Math.PI, z: -Math.atan2(roofHeight, buildingDepth / 2) });
164
+ group.add(roofLeft);
165
+ group.add(roofRight);
166
+ // Add gable ends (triangles)
167
+ const gableShape = new THREE.Shape();
168
+ gableShape.moveTo(-buildingWidth/2, buildingHeight);
169
+ gableShape.lineTo(buildingWidth/2, buildingHeight);
170
+ gableShape.lineTo(0, buildingHeight + roofHeight);
171
+ gableShape.closePath();
172
+ const gableGeo = new THREE.ShapeGeometry(gableShape);
173
+ group.add(createMesh(gableGeo, woodMaterial, {x: 0, y: 0, z: buildingDepth/2}, {x: 0, y: 0, z: 0}));
174
+ group.add(createMesh(gableGeo, woodMaterial, {x: 0, y: 0, z: -buildingDepth/2}, {x: 0, y: Math.PI, z: 0}));
175
+
176
+
177
+ // Forge/Chimney (simple representation)
178
+ const forgeHeight = 3;
179
+ const forgeGeo = new THREE.CylinderGeometry(0.3, 0.4, forgeHeight, 8);
180
+ group.add(createMesh(forgeGeo, stoneMaterial, { x: buildingWidth * 0.3, y: forgeHeight / 2, z: -buildingDepth * 0.3 }));
181
+
182
+ // Anvil (simple block)
183
+ const anvilGeo = new THREE.BoxGeometry(0.4, 0.5, 0.7);
184
+ group.add(createMesh(anvilGeo, metalMaterial, { x: -buildingWidth * 0.2, y: 0.25, z: buildingDepth * 0.2 }));
185
+
186
+
187
+ group.add(createGroundPlane());
188
+ return group;
189
+ }
190
+
191
+ function createTempleAssembly() {
192
+ const group = new THREE.Group();
193
+ const baseSize = 5;
194
+ const baseHeight = 0.5;
195
+ const columnHeight = 3;
196
+ const columnRadius = 0.25;
197
+ const roofHeight = 1;
198
+
199
+ // Base Platform
200
+ const baseGeo = new THREE.BoxGeometry(baseSize, baseHeight, baseSize);
201
+ group.add(createMesh(baseGeo, templeMaterial, { x: 0, y: baseHeight / 2, z: 0 }));
202
+
203
+ // Columns (example: 4 columns)
204
+ const colPositions = [
205
+ { x: -baseSize / 3, z: -baseSize / 3 },
206
+ { x: baseSize / 3, z: -baseSize / 3 },
207
+ { x: -baseSize / 3, z: baseSize / 3 },
208
+ { x: baseSize / 3, z: baseSize / 3 },
209
+ ];
210
+ const colGeo = new THREE.CylinderGeometry(columnRadius, columnRadius, columnHeight, 12);
211
+ colPositions.forEach(pos => {
212
+ group.add(createMesh(colGeo, templeMaterial, { x: pos.x, y: baseHeight + columnHeight / 2, z: pos.z }));
213
+ });
214
+
215
+ // Simple Roof Slab
216
+ const roofGeo = new THREE.BoxGeometry(baseSize * 0.8, roofHeight / 2, baseSize * 0.8);
217
+ group.add(createMesh(roofGeo, templeMaterial, { x: 0, y: baseHeight + columnHeight + roofHeight / 4, z: 0 }));
218
+
219
+ // Optional: Pyramid roof top
220
+ const pyramidGeo = new THREE.ConeGeometry(baseSize * 0.5, roofHeight * 1.5, 4); // 4 sides for pyramid
221
+ group.add(createMesh(pyramidGeo, templeMaterial, { x: 0, y: baseHeight + columnHeight + roofHeight *0.75, z: 0 }, { x: 0, y: Math.PI / 4, z: 0 })); // Rotate for alignment
222
+
223
+
224
+ group.add(createGroundPlane());
225
+ return group;
226
+ }
227
+
228
+ function createResistanceMeetingAssembly() {
229
+ const group = new THREE.Group();
230
+ const tableWidth = 2;
231
+ const tableHeight = 0.8;
232
+ const tableDepth = 1;
233
+ const tableThickness = 0.1;
234
+
235
+ // Table Top
236
+ const tableTopGeo = new THREE.BoxGeometry(tableWidth, tableThickness, tableDepth);
237
+ group.add(createMesh(tableTopGeo, woodMaterial, { x: 0, y: tableHeight - tableThickness / 2, z: 0 }));
238
+
239
+ // Table Legs
240
+ const legHeight = tableHeight - tableThickness;
241
+ const legSize = 0.1;
242
+ const legGeo = new THREE.BoxGeometry(legSize, legHeight, legSize);
243
+ const legOffsetW = tableWidth / 2 - legSize * 1.5;
244
+ const legOffsetD = tableDepth / 2 - legSize * 1.5;
245
+ group.add(createMesh(legGeo, woodMaterial, { x: -legOffsetW, y: legHeight / 2, z: -legOffsetD }));
246
+ group.add(createMesh(legGeo, woodMaterial, { x: legOffsetW, y: legHeight / 2, z: -legOffsetD }));
247
+ group.add(createMesh(legGeo, woodMaterial, { x: -legOffsetW, y: legHeight / 2, z: legOffsetD }));
248
+ group.add(createMesh(legGeo, woodMaterial, { x: legOffsetW, y: legHeight / 2, z: legOffsetD }));
249
+
250
+ // Simple Stools/Boxes for people to sit on
251
+ const stoolSize = 0.4;
252
+ const stoolGeo = new THREE.BoxGeometry(stoolSize, stoolSize * 0.8, stoolSize);
253
+ group.add(createMesh(stoolGeo, darkWoodMaterial, { x: -tableWidth * 0.6, y: stoolSize * 0.4, z: 0 }));
254
+ group.add(createMesh(stoolGeo, darkWoodMaterial, { x: tableWidth * 0.6, y: stoolSize * 0.4, z: 0 }));
255
+ group.add(createMesh(stoolGeo, darkWoodMaterial, { x: 0, y: stoolSize * 0.4, z: -tableDepth * 0.7 }));
256
+
257
+ // Dim room feeling - maybe add simple walls
258
+ const wallHeight = 3;
259
+ const wallThickness = 0.2;
260
+ const roomSize = 5;
261
+ const wallBackGeo = new THREE.BoxGeometry(roomSize, wallHeight, wallThickness);
262
+ group.add(createMesh(wallBackGeo, stoneMaterial, { x: 0, y: wallHeight / 2, z: -roomSize / 2 }, {}));
263
+ const wallLeftGeo = new THREE.BoxGeometry(wallThickness, wallHeight, roomSize);
264
+ group.add(createMesh(wallLeftGeo, stoneMaterial, { x: -roomSize / 2, y: wallHeight / 2, z: 0 }, {}));
265
+
266
+
267
+ group.add(createGroundPlane(stoneMaterial)); // Stone floor
268
+ return group;
269
+ }
270
+
271
+ function createForestAssembly(treeCount = 15, area = 10) {
272
+ const group = new THREE.Group();
273
+
274
+ // Tree generation function
275
+ const createTree = (x, z) => {
276
+ const treeGroup = new THREE.Group();
277
+ const trunkHeight = Math.random() * 2 + 2; // Random height between 2 and 4
278
+ const trunkRadius = Math.random() * 0.15 + 0.1; // Random radius
279
+ const trunkGeo = new THREE.CylinderGeometry(trunkRadius * 0.7, trunkRadius, trunkHeight, 8);
280
+ treeGroup.add(createMesh(trunkGeo, woodMaterial, { x: 0, y: trunkHeight / 2, z: 0 }));
281
+
282
+ // Foliage (simple sphere or cone)
283
+ const foliageType = Math.random();
284
+ const foliageHeight = trunkHeight * (Math.random() * 0.5 + 0.8); // Relative to trunk
285
+ if (foliageType < 0.6) { // Sphere foliage
286
+ const foliageRadius = trunkHeight * 0.4;
287
+ const foliageGeo = new THREE.SphereGeometry(foliageRadius, 8, 6);
288
+ treeGroup.add(createMesh(foliageGeo, leafMaterial, { x: 0, y: trunkHeight * 0.9 + foliageRadius * 0.5, z: 0 }));
289
+ } else { // Cone foliage
290
+ const foliageRadius = trunkHeight * 0.5;
291
+ const coneGeo = new THREE.ConeGeometry(foliageRadius, foliageHeight, 8);
292
+ treeGroup.add(createMesh(coneGeo, leafMaterial, { x: 0, y: trunkHeight * 0.9 + foliageHeight * 0.5, z: 0 }));
293
+ }
294
+ treeGroup.position.set(x, 0, z); // Set position for the whole tree
295
+ // Slight random rotation for variation
296
+ treeGroup.rotation.y = Math.random() * Math.PI * 2;
297
+ return treeGroup;
298
+ };
299
+
300
+ // Scatter trees
301
+ for (let i = 0; i < treeCount; i++) {
302
+ const x = (Math.random() - 0.5) * area;
303
+ const z = (Math.random() - 0.5) * area;
304
+ // Basic check to avoid trees too close to the center (optional)
305
+ if (Math.sqrt(x*x + z*z) > 1.5) {
306
+ group.add(createTree(x, z));
307
+ }
308
+ }
309
+
310
+ group.add(createGroundPlane()); // Forest floor
311
+ return group;
312
+ }
313
+
314
+ function createRoadAmbushAssembly() {
315
+ const group = new THREE.Group();
316
+ const area = 12;
317
+
318
+ // Add some forest elements
319
+ const forestGroup = createForestAssembly(10, area);
320
+ group.add(forestGroup); // Reuse forest generation
321
+
322
+ // Add a simple road (a flat, wider plane)
323
+ const roadWidth = 3;
324
+ const roadLength = area * 1.5;
325
+ const roadGeo = new THREE.PlaneGeometry(roadWidth, roadLength);
326
+ const roadMaterial = new THREE.MeshStandardMaterial({ color: 0x966F33, roughness: 0.9 }); // Muddy brown
327
+ const road = createMesh(roadGeo, roadMaterial, {x: 0, y: 0.01, z: 0}, {x: -Math.PI / 2}); // Slightly above ground
328
+ road.receiveShadow = true; // Ensure road receives shadows too
329
+ group.add(road);
330
+
331
+ // Add some rocks/bushes for cover (simple spheres/low boxes)
332
+ const rockGeo = new THREE.SphereGeometry(0.5, 5, 4);
333
+ const rockMaterial = new THREE.MeshStandardMaterial({ color: 0x666666, roughness: 0.8 });
334
+ group.add(createMesh(rockGeo, rockMaterial, {x: roadWidth * 0.7, y: 0.25, z: 1}, {y: Math.random() * Math.PI}));
335
+ group.add(createMesh(rockGeo, rockMaterial, {x: -roadWidth * 0.8, y: 0.3, z: -2}, {y: Math.random() * Math.PI, x: Math.random()*0.2}));
336
+ group.add(createMesh(new THREE.SphereGeometry(0.7, 5, 4), rockMaterial, {x: roadWidth * 0.9, y: 0.35, z: -3}, {y: Math.random() * Math.PI}));
337
+
338
+ // Suggestion: You could add simple cylinder/box figures near cover later for the ambushers
339
+
340
+ // Ground plane is added by createForestAssembly
341
+
342
+ return group;
343
+ }
344
+
345
+ function createForestEdgeAssembly() {
346
+ const group = new THREE.Group();
347
+ const area = 15;
348
+
349
+ // Dense forest on one side
350
+ const forestGroup = new THREE.Group();
351
+ for (let i = 0; i < 20; i++) { // More trees, denser area
352
+ const x = (Math.random() - 0.9) * area / 2; // Skew to one side (negative X)
353
+ const z = (Math.random() - 0.5) * area;
354
+ forestGroup.add(createForestAssembly(1, 0).children[0].position.set(x,0,z)); // Add single tree procedurally
355
+ }
356
+ group.add(forestGroup);
357
+
358
+
359
+ // Open plains on the other side (just ground)
360
+ group.add(createGroundPlane(groundMaterial, area * 1.2)); // Larger ground plane
361
+
362
+ return group;
363
+ }
364
+
365
+ function createPrisonerCellAssembly() {
366
+ const group = new THREE.Group();
367
+ const cellSize = 3;
368
+ const wallHeight = 2.5;
369
+ const wallThickness = 0.2;
370
+ const barRadius = 0.04;
371
+ const barSpacing = 0.2;
372
+
373
+ // Floor
374
+ group.add(createGroundPlane(stoneMaterial, cellSize));
375
+
376
+ // Back Wall
377
+ const wallBackGeo = new THREE.BoxGeometry(cellSize, wallHeight, wallThickness);
378
+ group.add(createMesh(wallBackGeo, stoneMaterial, { x: 0, y: wallHeight / 2, z: -cellSize / 2 }));
379
+
380
+ // Left Wall
381
+ const wallSideGeo = new THREE.BoxGeometry(wallThickness, wallHeight, cellSize);
382
+ group.add(createMesh(wallSideGeo, stoneMaterial, { x: -cellSize / 2, y: wallHeight / 2, z: 0 }));
383
+
384
+ // Right Wall (Partial or Full)
385
+ group.add(createMesh(wallSideGeo, stoneMaterial, { x: cellSize / 2, y: wallHeight / 2, z: 0 }));
386
+
387
+ // Ceiling (optional)
388
+ // const ceilingGeo = new THREE.BoxGeometry(cellSize, wallThickness, cellSize);
389
+ // group.add(createMesh(ceilingGeo, stoneMaterial, { x: 0, y: wallHeight, z: 0 }));
390
+
391
+
392
+ // Bars for the front
393
+ const barGeo = new THREE.CylinderGeometry(barRadius, barRadius, wallHeight, 8);
394
+ const numBars = Math.floor(cellSize / barSpacing);
395
+ for (let i = 0; i <= numBars; i++) {
396
+ const xPos = -cellSize / 2 + i * barSpacing;
397
+ group.add(createMesh(barGeo, metalMaterial, { x: xPos, y: wallHeight / 2, z: cellSize / 2 }));
398
+ }
399
+ // Horizontal bars (top/bottom)
400
+ const horizBarGeo = new THREE.BoxGeometry(cellSize, barRadius * 2, barRadius * 2);
401
+ group.add(createMesh(horizBarGeo, metalMaterial, {x: 0, y: wallHeight - barRadius, z: cellSize/2}));
402
+ group.add(createMesh(horizBarGeo, metalMaterial, {x: 0, y: barRadius, z: cellSize/2}));
403
+
404
+
405
+ return group;
406
+ }
407
+
408
+ function createGameOverAssembly() {
409
+ const group = new THREE.Group();
410
+ const boxGeo = new THREE.BoxGeometry(2, 2, 2);
411
+ group.add(createMesh(boxGeo, gameOverMaterial, { x: 0, y: 1, z: 0 }));
412
+ group.add(createGroundPlane(stoneMaterial.clone().set({color: 0x333333}))); // Darker ground
413
+ return group;
414
+ }
415
+
416
+ function createErrorAssembly() {
417
+ const group = new THREE.Group();
418
+ const coneGeo = new THREE.ConeGeometry( 0.8, 1.5, 8 );
419
+ group.add(createMesh(coneGeo, errorMaterial, { x: 0, y: 0.75, z: 0 }));
420
+ group.add(createGroundPlane());
421
+ return group;
422
+ }
423
+
424
+
425
  function onWindowResize() {
426
  if (!renderer || !camera) return;
427
  camera.aspect = sceneContainer.clientWidth / sceneContainer.clientHeight;
 
432
  function animate() {
433
  requestAnimationFrame(animate);
434
 
435
+ // Optional: Add subtle animation to the entire assembly
436
+ if (currentAssemblyGroup) {
437
+ // Example: Very slow rotation
438
+ // currentAssemblyGroup.rotation.y += 0.0005;
439
  }
440
 
441
  // if (controls) controls.update(); // If using OrbitControls
 
492
  content: `<p>You leave Silverhold and enter the corrupted Shadowwood Forest. Strange sounds echo. Which path will you take?</p>`,
493
  options: [
494
  { text: "Take the main road", next: 6 }, // Leads to page 6 (Ambush)
495
+ { text: "Follow the river path", next: 7 }, // Leads to page 7 (River Spirit) - NEEDS 3D Scene
496
+ { text: "Brave the ruins shortcut", next: 8 } // Leads to page 8 (Ruins) - NEEDS 3D Scene
497
  ],
498
  illustration: "shadowwood-forest" // Key for Three.js scene
499
  // Add more pages here...
500
  },
501
  // Add placeholder pages 6, 7, 8 etc. to continue the story
502
  "6": {
503
+ title: "Ambush!",
504
+ content: "<p>Scouts jump out from behind rocks and trees! 'Surrender!'</p>",
505
+ options: [{ text: "Fight!", next: 9 }, { text: "Try to flee!", next: 10 }], // Example links
506
+ illustration: "road-ambush"
507
+ },
508
+ "7": { // Placeholder - NEEDS 3D Scene function
509
+ title: "River Path",
510
+ content: "<p>You follow the winding river. The water seems unnaturally dark.</p>",
511
+ options: [{ text: "Continue along the river", next: 11 }, { text: "Investigate strange glow", next: 12 }],
512
+ illustration: "river-spirit" // Needs createRiverSpiritAssembly()
513
+ },
514
+ "8": { // Placeholder - NEEDS 3D Scene function
515
+ title: "Ancient Ruins",
516
+ content: "<p>Crumbling stones and overgrown vines mark ancient ruins. It feels watched.</p>",
517
+ options: [{ text: "Search the main structure", next: 13 }, { text: "Look for hidden passages", next: 14 }],
518
+ illustration: "ancient-ruins" // Needs createRuinsAssembly()
519
  },
 
520
  "9": { // Example continuation
521
+ title: "Victory!",
522
+ content: "<p>You defeat the scouts and retrieve some basic supplies. The forest edge is near.</p>",
523
+ options: [{ text: "Proceed to the fortress plains", next: 15 }],
524
+ illustration: "forest-edge"
525
  },
526
  "10": { // Example continuation
527
+ title: "Captured!",
528
+ content: "<p>Your attempt to flee fails! You are knocked out and awaken in a dark, damp cell.</p>",
529
+ options: [{ text: "Wait and observe", next: 20 }], // Go to prison observation page
530
+ illustration: "prisoner-cell"
531
+ },
532
+ // ... Add many more pages based on your Python data ...
533
+ "15": { // Placeholder for plains
534
+ title: "Fortress Plains",
535
+ content: "<p>You emerge from the forest onto windswept plains. The dark fortress looms ahead.</p>",
536
+ options: [{ text: "Approach the main gate", next: 30 }, { text: "Scout the perimeter", next: 31 }],
537
+ illustration: "fortress-plains" // Needs createFortressPlainsAssembly()
538
+ },
539
+ "20": { // Placeholder for cell observation
540
+ title: "Inside the Cell",
541
+ content: "<p>The cell is small and cold. You hear guards patrolling outside.</p>",
542
+ options: [{ text: "Look for weaknesses in the bars", next: 21 }, { text: "Try to talk to a guard", next: 22 }],
543
+ illustration: "prisoner-cell" // Reuse cell
544
  },
545
  // Game Over placeholder
546
  "99": {
547
+ title: "Game Over",
548
+ content: "<p>Your adventure ends here.</p>",
549
+ options: [{ text: "Restart", next: 1 }], // Link back to start
550
+ illustration: "game-over",
551
+ gameOver: true
552
  }
553
  };
554
 
 
595
  console.error(`Error: Page data not found for ID: ${pageId}`);
596
  storyTitleElement.textContent = "Error";
597
  storyContentElement.innerHTML = "<p>Could not load page data. Adventure halted.</p>";
598
+ choicesElement.innerHTML = '<button class="choice-button" onclick="handleChoiceClick({ nextPage: 1 })">Restart</button>'; // Provide restart option
599
  updateScene('error'); // Show error scene
600
  return;
601
  }
 
624
  // Add requireAnyItem check here later if needed
625
 
626
  if (requirementMet) {
627
+ // Store data needed for handling the choice using dataset
628
+ const choiceData = { nextPage: option.next }; // Always include next page
629
+ if (option.addItem) {
630
+ choiceData.addItem = option.addItem;
631
+ }
632
+ // Add other potential effects as data attributes if needed (e.g., data-stat-change="strength:1")
633
+
634
+ // Use an event listener instead of inline onclick for better practice
635
+ button.addEventListener('click', () => handleChoiceClick(choiceData));
636
+
637
+ } else {
638
+ button.classList.add('disabled'); // Style disabled buttons
639
  }
640
 
641
  choicesElement.appendChild(button);
 
644
  const button = document.createElement('button');
645
  button.classList.add('choice-button');
646
  button.textContent = "Restart Adventure";
647
+ button.addEventListener('click', () => handleChoiceClick({ nextPage: 1 })); // Restart goes to page 1
 
648
  choicesElement.appendChild(button);
649
  } else {
650
+ // Handle dead ends where no options are defined and it's not game over
651
+ choicesElement.innerHTML = '<p><i>There are no further paths from here.</i></p>';
652
  const button = document.createElement('button');
653
  button.classList.add('choice-button');
654
  button.textContent = "Restart Adventure";
655
+ button.addEventListener('click', () => handleChoiceClick({ nextPage: 1 })); // Restart goes to page 1
 
656
  choicesElement.appendChild(button);
657
  }
658
 
 
661
  updateScene(page.illustration || 'default');
662
  }
663
 
664
+
665
+ // Modified handleChoiceClick to accept an object
666
+ function handleChoiceClick(choiceData) {
667
+ const nextPageId = parseInt(choiceData.nextPage); // Ensure it's a number
668
+ const itemToAdd = choiceData.addItem;
669
+ // Add other potential effects from choiceData here (e.g., stat changes tied to the *choice itself*)
670
 
671
  if (isNaN(nextPageId)) {
672
+ console.error("Invalid nextPageId:", choiceData.nextPage);
673
  return;
674
  }
675
 
 
753
 
754
  function updateScene(illustrationKey) {
755
  console.log("Updating scene for:", illustrationKey);
 
756
 
757
+ // 1. Remove the old assembly if it exists
758
+ if (currentAssemblyGroup) {
759
+ scene.remove(currentAssemblyGroup);
760
+ // Optional: Dispose of geometries and materials if scenes get complex
761
+ // currentAssemblyGroup.traverse(child => {
762
+ // if (child.isMesh) {
763
+ // if(child.geometry) child.geometry.dispose();
764
+ // // Dispose materials carefully if they are shared!
765
+ // // If not shared: if(child.material) child.material.dispose();
766
+ // }
767
+ // });
768
+ }
769
+ currentAssemblyGroup = null; // Reset the reference
770
+
771
+ // 2. Select the generation function based on the key
772
+ let assemblyFunction;
773
  switch (illustrationKey) {
774
+ case 'city-gates': assemblyFunction = createCityGatesAssembly; break;
775
+ case 'weaponsmith': assemblyFunction = createWeaponsmithAssembly; break;
776
+ case 'temple': assemblyFunction = createTempleAssembly; break;
777
+ case 'resistance-meeting': assemblyFunction = createResistanceMeetingAssembly; break;
778
+ case 'shadowwood-forest': assemblyFunction = createForestAssembly; break;
779
+ case 'road-ambush': assemblyFunction = createRoadAmbushAssembly; break;
780
+ case 'forest-edge': assemblyFunction = createForestEdgeAssembly; break;
781
+ case 'prisoner-cell': assemblyFunction = createPrisonerCellAssembly; break;
782
+ case 'game-over': assemblyFunction = createGameOverAssembly; break;
783
+ case 'error': assemblyFunction = createErrorAssembly; break;
784
+
785
+ // --- Add cases for new/missing scenes ---
786
+ // case 'river-spirit': assemblyFunction = createRiverSpiritAssembly; break; // TODO
787
+ // case 'ancient-ruins': assemblyFunction = createRuinsAssembly; break; // TODO
788
+ // case 'fortress-plains': assemblyFunction = createFortressPlainsAssembly; break; // TODO
789
+
790
+ default:
791
+ console.warn(`No specific assembly function found for key: ${illustrationKey}. Using default.`);
792
+ assemblyFunction = createDefaultAssembly;
793
+ break;
794
+ }
795
+
796
+ // 3. Create the new assembly
797
+ try {
798
+ currentAssemblyGroup = assemblyFunction();
799
+ } catch (error) {
800
+ console.error(`Error creating assembly for key ${illustrationKey}:`, error);
801
+ currentAssemblyGroup = createErrorAssembly(); // Show error scene on generation failure
802
  }
 
803
 
804
+
805
+ // 4. Add the new assembly to the scene
806
+ if (currentAssemblyGroup) {
807
+ scene.add(currentAssemblyGroup);
808
+ // Optional: Slightly randomize overall rotation/position for non-fixed scenes like forests
809
+ if (['shadowwood-forest', 'road-ambush', 'forest-edge'].includes(illustrationKey)) {
810
+ currentAssemblyGroup.rotation.y = Math.random() * 0.1 - 0.05; // Small random Y rotation
811
+ }
812
+ } else {
813
+ console.error(`Assembly function for ${illustrationKey} did not return a group.`);
814
+ currentAssemblyGroup = createErrorAssembly(); // Fallback
815
+ scene.add(currentAssemblyGroup);
816
+ }
817
  }
818
 
819
 
 
821
  initThreeJS();
822
  startGame(); // Start the game after setting up Three.js
823
 
824
+ // Removed global handleChoiceClick - now using event listeners in renderPage
 
825
  // window.handleChoiceClick = handleChoiceClick;