Spaces:
Running
Running
Update game.js
Browse files
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
|
|
|
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.
|
|
|
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.
|
33 |
scene.add(ambientLight);
|
34 |
-
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.
|
35 |
-
directionalLight.position.set(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
36 |
scene.add(directionalLight);
|
|
|
|
|
|
|
|
|
|
|
37 |
|
38 |
-
// Basic Object (Placeholder
|
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 |
-
//
|
66 |
-
if (
|
67 |
-
|
68 |
-
|
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 |
-
|
134 |
-
|
135 |
-
|
136 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
137 |
},
|
138 |
-
// ... Add many more pages based on your Python data ...
|
139 |
"9": { // Example continuation
|
140 |
-
|
141 |
-
|
142 |
-
|
143 |
-
|
144 |
},
|
145 |
"10": { // Example continuation
|
146 |
-
|
147 |
-
|
148 |
-
|
149 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
150 |
},
|
151 |
// Game Over placeholder
|
152 |
"99": {
|
153 |
-
|
154 |
-
|
155 |
-
|
156 |
-
|
157 |
-
|
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="
|
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 |
-
|
235 |
-
|
236 |
-
|
237 |
-
|
238 |
-
|
239 |
-
|
240 |
-
|
|
|
|
|
|
|
|
|
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.
|
250 |
-
button.onclick = () => handleChoiceClick(button.dataset);
|
251 |
choicesElement.appendChild(button);
|
252 |
} else {
|
253 |
-
|
|
|
254 |
const button = document.createElement('button');
|
255 |
button.classList.add('choice-button');
|
256 |
button.textContent = "Restart Adventure";
|
257 |
-
button.
|
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 |
-
|
268 |
-
|
269 |
-
|
|
|
|
|
|
|
270 |
|
271 |
if (isNaN(nextPageId)) {
|
272 |
-
console.error("Invalid nextPageId:",
|
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 |
-
//
|
359 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
360 |
switch (illustrationKey) {
|
361 |
-
case 'city-gates':
|
362 |
-
case 'weaponsmith':
|
363 |
-
case 'temple':
|
364 |
-
case 'resistance-meeting':
|
365 |
-
case 'shadowwood-forest':
|
366 |
-
case 'road-ambush':
|
367 |
-
case '
|
368 |
-
case '
|
369 |
-
case '
|
370 |
-
case '
|
371 |
-
|
372 |
-
|
373 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
374 |
}
|
375 |
-
cube.material.color.setHex(color);
|
376 |
|
377 |
-
|
378 |
-
//
|
379 |
-
|
380 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
381 |
}
|
382 |
|
383 |
|
@@ -385,6 +821,5 @@ function updateScene(illustrationKey) {
|
|
385 |
initThreeJS();
|
386 |
startGame(); // Start the game after setting up Three.js
|
387 |
|
388 |
-
//
|
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;
|