awacke1 commited on
Commit
0a69594
·
verified ·
1 Parent(s): 7c0cab7

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +358 -85
index.html CHANGED
@@ -3,13 +3,13 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Basic 3D Setup Test</title>
7
  <style>
8
- /* --- Minimal Base Styles --- */
9
  body {
10
  font-family: 'Courier New', monospace;
11
- background-color: #222;
12
- color: #eee;
13
  margin: 0;
14
  padding: 0;
15
  overflow: hidden; /* Prevent scrollbars */
@@ -31,32 +31,90 @@
31
  border-right: 2px solid #555;
32
  min-width: 200px;
33
  background-color: #1a1a1a;
34
- height: 100%; /* Ensure it takes full height of flex parent */
35
- box-sizing: border-box; /* Include border in size */
36
  }
37
 
38
  #ui-container {
39
  flex-grow: 2; /* Space for UI elements */
40
  padding: 20px;
41
- overflow-y: auto; /* Allow scrolling if content overflows */
42
  background-color: #333;
43
  min-width: 280px;
44
- height: 100%; /* Ensure it takes full height of flex parent */
45
- box-sizing: border-box; /* Include padding in size */
 
 
46
  }
47
 
48
- #scene-container canvas {
49
- display: block; /* Remove extra space below canvas */
50
- }
51
 
52
- /* --- Basic UI Placeholders --- */
53
- #ui-container h2 {
54
  color: #ffcc66;
55
  margin-top: 0;
56
  margin-bottom: 15px;
57
  border-bottom: 1px solid #555;
58
  padding-bottom: 10px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
 
61
  </style>
62
  </head>
@@ -67,12 +125,24 @@
67
  </div>
68
 
69
  <div id="ui-container">
70
- <h2 id="story-title">UI Panel</h2>
71
  <div id="story-content">
72
- <p>This panel will hold the story text, choices, and character sheet later.</p>
73
- <p>Focus is on getting the 3D cube rendering in the left panel.</p>
 
 
 
 
 
 
74
  </div>
75
- </div>
 
 
 
 
 
 
76
  </div>
77
 
78
  <script type="importmap">
@@ -86,120 +156,323 @@
86
 
87
  <script type="module">
88
  import * as THREE from 'three';
89
- // OrbitControls is not used in this minimal version
90
  // import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
91
 
92
  // --- DOM Elements ---
93
  const sceneContainer = document.getElementById('scene-container');
94
- // References to UI elements (not used in this version, but kept for context)
95
- // const storyTitleElement = document.getElementById('story-title');
96
- // const storyContentElement = document.getElementById('story-content');
97
- // const choicesElement = document.getElementById('choices');
98
- // const statsElement = document.getElementById('stats-display');
99
- // const inventoryElement = document.getElementById('inventory-display');
100
 
101
  // --- Three.js Setup ---
102
- let scene, camera, renderer, cube; // Basic scene objects
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
 
104
  function initThreeJS() {
105
- console.log("Attempting to initialize Three.js..."); // Debug log
106
 
107
- if (!sceneContainer) {
108
- console.error("Scene container not found!");
109
- return; // Stop if container doesn't exist
110
- }
111
 
112
- // Scene
113
  scene = new THREE.Scene();
114
- scene.background = new THREE.Color(0x222222); // Dark grey background
115
 
116
- // Camera
117
- // Use container dimensions for aspect ratio calculation
118
  const width = sceneContainer.clientWidth;
119
  const height = sceneContainer.clientHeight;
120
- if (width === 0 || height === 0) {
121
- console.warn("Scene container has zero dimensions on init. Renderer/Camera might be sized incorrectly.");
122
- }
123
- camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
124
- camera.position.z = 5; // Position camera back
125
 
126
- // Renderer
127
  renderer = new THREE.WebGLRenderer({ antialias: true });
128
- // Set size explicitly, fallback if container size is 0 initially
129
- renderer.setSize(width || 400, height || 300); // Use fallback size if needed
130
- sceneContainer.appendChild(renderer.domElement); // Add canvas to the container
131
- console.log("Renderer initialized and added to DOM.");
 
132
 
133
- // Basic Lighting
134
- const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); // Soft white light
135
  scene.add(ambientLight);
136
- const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0);
137
- directionalLight.position.set(5, 10, 7.5); // Position the light
 
 
 
 
 
 
 
 
138
  scene.add(directionalLight);
139
 
140
- // Basic Cube Object
141
- const geometry = new THREE.BoxGeometry(1, 1, 1); // 1x1x1 cube
142
- const material = new THREE.MeshStandardMaterial({ color: 0xcccccc }); // Light grey color
143
- cube = new THREE.Mesh(geometry, material);
144
- scene.add(cube); // Add cube to the scene
145
- console.log("Cube created and added to scene.");
146
 
147
- // Handle Resize
148
  window.addEventListener('resize', onWindowResize, false);
149
- // Initial resize call in case the flexbox layout takes a moment
150
- setTimeout(onWindowResize, 100); // Delay slightly to help layout settle
151
 
152
- // Start Animation Loop
153
  animate();
154
  console.log("Animation loop started.");
155
  }
156
 
157
  function onWindowResize() {
158
  if (!renderer || !camera || !sceneContainer) return;
159
-
160
  const width = sceneContainer.clientWidth;
161
  const height = sceneContainer.clientHeight;
162
-
163
- // Only resize if dimensions are valid
164
  if (width > 0 && height > 0) {
165
  camera.aspect = width / height;
166
- camera.updateProjectionMatrix(); // Important after changing aspect
167
  renderer.setSize(width, height);
168
- // console.log(`Resized to ${width}x${height}`); // Debug log for resize
169
- } else {
170
- console.warn("onWindowResize called with zero dimensions.");
171
  }
172
  }
173
 
174
  function animate() {
175
- requestAnimationFrame(animate); // Loop the animation
176
 
177
- // Simple cube rotation
178
- if (cube) {
179
- cube.rotation.x += 0.005;
180
- cube.rotation.y += 0.005;
181
- }
182
 
183
- // Render the scene
184
  if (renderer && scene && camera) {
185
  renderer.render(scene, camera);
186
  }
187
  }
188
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
  // --- Initialization ---
190
- // Wait for the basic HTML structure to be ready before running the script
191
  document.addEventListener('DOMContentLoaded', () => {
192
- console.log("DOM Content Loaded. Initializing...");
193
- try {
194
- initThreeJS();
195
- } catch (error) {
196
- console.error("Error during initialization:", error);
197
- // Display error to user in the UI panel
198
- const uiContainer = document.getElementById('ui-container');
199
- if (uiContainer) {
200
- uiContainer.innerHTML = `<h2>Error</h2><p>Failed to initialize 3D scene. Please check the browser console (F12) for details.</p><pre>${error.message}\n${error.stack}</pre>`;
201
- }
202
- }
203
  });
204
 
205
  </script>
 
3
  <head>
4
  <meta charset="UTF-8">
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 */
 
31
  border-right: 2px solid #555;
32
  min-width: 200px;
33
  background-color: #1a1a1a;
34
+ height: 100%;
35
+ box-sizing: border-box;
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;
56
  margin-bottom: 15px;
57
  border-bottom: 1px solid #555;
58
  padding-bottom: 10px;
59
+ font-size: 1.4em;
60
+ }
61
+
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; }
69
+
70
+ #stats-inventory-container {
71
+ margin-bottom: 20px;
72
+ padding-bottom: 15px;
73
+ border-bottom: 1px solid #555;
74
+ font-size: 0.9em;
75
+ }
76
+ #stats-display, #inventory-display {
77
+ margin-bottom: 10px;
78
+ line-height: 1.8;
79
  }
80
+ #stats-display span, #inventory-display span {
81
+ display: inline-block;
82
+ background-color: #444;
83
+ padding: 3px 8px;
84
+ border-radius: 15px;
85
+ margin-right: 8px;
86
+ margin-bottom: 5px;
87
+ border: 1px solid #666;
88
+ white-space: nowrap;
89
+ }
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;}
97
+ #inventory-display .item-spell { background-color: #303066; border-color: #484899;}
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
+ }
105
+ #choices-container h3 { margin-top: 0; margin-bottom: 10px; color: #aaa; }
106
+ #choices { display: flex; flex-direction: column; gap: 10px; }
107
+
108
+ .choice-button {
109
+ display: block; width: 100%; padding: 10px 12px; margin-bottom: 0;
110
+ background-color: #555; color: #eee; border: 1px solid #777;
111
+ border-radius: 5px; cursor: pointer; text-align: left;
112
+ font-family: 'Courier New', monospace; font-size: 1em;
113
+ transition: background-color 0.2s, border-color 0.2s;
114
+ box-sizing: border-box;
115
+ }
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>
 
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>
147
 
148
  <script type="importmap">
 
156
 
157
  <script type="module">
158
  import * as THREE from 'three';
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');
166
+ const choicesElement = document.getElementById('choices');
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);
214
+ directionalLight.position.set(8, 15, 10);
215
+ directionalLight.castShadow = true;
216
+ directionalLight.shadow.mapSize.width = 1024;
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() {
235
  if (!renderer || !camera || !sceneContainer) return;
 
236
  const width = sceneContainer.clientWidth;
237
  const height = sceneContainer.clientHeight;
 
 
238
  if (width > 0 && height > 0) {
239
  camera.aspect = width / height;
240
+ camera.updateProjectionMatrix();
241
  renderer.setSize(width, height);
 
 
 
242
  }
243
  }
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;
456
+ case 'weaponsmith': assemblyFunction = createWeaponsmithAssembly; break;
457
+ case 'temple': assemblyFunction = createTempleAssembly; break;
458
+ case 'resistance-meeting': assemblyFunction = createResistanceMeetingAssembly; break;
459
+ case 'shadowwood-forest': assemblyFunction = createForestAssembly; break;
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>