File size: 45,632 Bytes
087c83f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
import * as THREE from 'three';
// Optional: Add OrbitControls for debugging/viewing scene
// import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

// --- DOM Elements ---
const sceneContainer = document.getElementById('scene-container');
const storyTitleElement = document.getElementById('story-title');
const storyContentElement = document.getElementById('story-content');
const choicesElement = document.getElementById('choices');
// Removed old stats/inventory elements
// const statsElement = document.getElementById('stats-display');
// const inventoryElement = document.getElementById('inventory-display');

// Character Sheet Elements
const charNameInput = document.getElementById('char-name');
const charRaceSpan = document.getElementById('char-race');
const charAlignmentSpan = document.getElementById('char-alignment');
const charClassSpan = document.getElementById('char-class');
const charLevelSpan = document.getElementById('char-level');
const charXPSpan = document.getElementById('char-xp');
const charXPNextSpan = document.getElementById('char-xp-next');
const charHPSpan = document.getElementById('char-hp');
const charMaxHPSpan = document.getElementById('char-max-hp');
const charInventoryList = document.getElementById('char-inventory-list');
const statSpans = {
    strength: document.getElementById('stat-strength'), intelligence: document.getElementById('stat-intelligence'),
    wisdom: document.getElementById('stat-wisdom'), dexterity: document.getElementById('stat-dexterity'),
    constitution: document.getElementById('stat-constitution'), charisma: document.getElementById('stat-charisma'),
};
const statIncreaseButtons = document.querySelectorAll('.stat-increase');
const levelUpButton = document.getElementById('levelup-btn');
const saveCharButton = document.getElementById('save-char-btn');
const exportCharButton = document.getElementById('export-char-btn');
const statIncreaseCostSpan = document.getElementById('stat-increase-cost');
const statPointsAvailableSpan = document.getElementById('stat-points-available');


// --- Three.js Setup ---
let scene, camera, renderer;
let currentAssemblyGroup = null;
let directionalLight;
let sunAngle = Math.PI / 4; // Start sun partway through morning
const clock = new THREE.Clock();
let clouds = [];
// let controls;

// --- Shared Materials ---
const stoneMaterial = new THREE.MeshStandardMaterial({ color: 0x888888, roughness: 0.8, metalness: 0.1 });
const woodMaterial = new THREE.MeshStandardMaterial({ color: 0x8B4513, roughness: 0.7, metalness: 0 });
const darkWoodMaterial = new THREE.MeshStandardMaterial({ color: 0x5C3D20, roughness: 0.7, metalness: 0 });
const leafMaterial = new THREE.MeshStandardMaterial({ color: 0x2E8B57, roughness: 0.6, metalness: 0 });
const pineLeafMaterial = new THREE.MeshStandardMaterial({ color: 0x1A5A2A, roughness: 0.7, metalness: 0 });
const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x556B2F, roughness: 0.9, metalness: 0 });
const metalMaterial = new THREE.MeshStandardMaterial({ color: 0xaaaaaa, metalness: 0.8, roughness: 0.3 });
const fabricMaterial = new THREE.MeshStandardMaterial({ color: 0x696969, roughness: 0.9, metalness: 0 });
const waterMaterial = new THREE.MeshStandardMaterial({ color: 0x60A3D9, roughness: 0.2, metalness: 0.1, transparent: true, opacity: 0.7 });
const templeMaterial = new THREE.MeshStandardMaterial({ color: 0xA99B78, roughness: 0.7, metalness: 0.1 });
const fireMaterial = new THREE.MeshStandardMaterial({ color: 0xFF4500, emissive: 0xff6600, roughness: 0.5, metalness: 0 });
const errorMaterial = new THREE.MeshStandardMaterial({ color: 0xffa500, roughness: 0.5 });
const gameOverMaterial = new THREE.MeshStandardMaterial({ color: 0xff0000, roughness: 0.5 });
const windowMaterial = new THREE.MeshStandardMaterial({ color: 0x334455, roughness: 0.3, metalness: 0, transparent: true, opacity: 0.6 });


function initThreeJS() {
    scene = new THREE.Scene();

    // Skybox Loading
    const loader = new THREE.CubeTextureLoader();
     // !!! IMPORTANT: Replace this path with the correct one for your textures !!!
    const texturePath = 'textures/skybox/';
    const textureFiles = ['posx.jpg', 'negx.jpg', 'posy.jpg', 'negy.jpg', 'posz.jpg', 'negz.jpg'];

    try {
        const texture = loader.setPath(texturePath).load(textureFiles,
        () => { console.log("Skybox loaded"); scene.background = texture; },
        undefined,
        (err) => { console.error(`Skybox loading error from ${texturePath}:`, err); scene.background = new THREE.Color(0x557799); }
        );
    } catch (e) {
        console.error("Error initiating skybox load (check path format maybe?):", e);
        scene.background = new THREE.Color(0x557799); // Fallback
    }


    camera = new THREE.PerspectiveCamera(60, sceneContainer.clientWidth / sceneContainer.clientHeight, 0.1, 1000);
    camera.position.set(0, 3, 9);
    camera.lookAt(0, 1, 0);

    renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setSize(sceneContainer.clientWidth, sceneContainer.clientHeight);
    renderer.shadowMap.enabled = true;
    renderer.shadowMap.type = THREE.PCFSoftShadowMap;
    renderer.toneMapping = THREE.ACESFilmicToneMapping;
    renderer.toneMappingExposure = 1.0;
    sceneContainer.appendChild(renderer.domElement);

    // Lighting
    const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
    scene.add(ambientLight);

    directionalLight = new THREE.DirectionalLight(0xffffff, 1.5);
    directionalLight.position.set(20, 30, 15); // Initial position, updated in animate
    directionalLight.target.position.set(0, 0, 0);
    directionalLight.castShadow = true;
    directionalLight.shadow.mapSize.width = 1024;
    directionalLight.shadow.mapSize.height = 1024;
    directionalLight.shadow.camera.near = 1;
    directionalLight.shadow.camera.far = 100;
    const shadowCamSize = 25;
    directionalLight.shadow.camera.left = -shadowCamSize; directionalLight.shadow.camera.right = shadowCamSize;
    directionalLight.shadow.camera.top = shadowCamSize; directionalLight.shadow.camera.bottom = -shadowCamSize;
    directionalLight.shadow.bias = -0.001;
    scene.add(directionalLight);
    scene.add(directionalLight.target);

    // Clouds
    createClouds(15);

    window.addEventListener('resize', onWindowResize, false);
    animate();
}

// Helper function to create meshes
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 }) {
    const mesh = new THREE.Mesh(geometry, material);
    mesh.position.set(position.x, position.y, position.z);
    mesh.rotation.set(rotation.x, rotation.y, rotation.z);
    mesh.scale.set(scale.x, scale.y, scale.z);
    mesh.castShadow = true;
    mesh.receiveShadow = true;
    return mesh;
}


// Cloud Creation
function createClouds(count) {
    const textureLoader = new THREE.TextureLoader();
    // !!! IMPORTANT: Replace this path with the correct one for your cloud texture !!!
    const cloudTexturePath = 'textures/cloud.png';
    try {
        const cloudTexture = textureLoader.load(cloudTexturePath,
            () => { console.log("Cloud texture loaded"); }, undefined,
            (err) => { console.error(`Cloud texture loading error from ${cloudTexturePath}:`, err); }
        );

        const cloudMaterial = new THREE.MeshBasicMaterial({
            map: cloudTexture, transparent: true, alphaTest: 0.2,
            depthWrite: false, side: THREE.DoubleSide,
        });
        const cloudGeo = new THREE.PlaneGeometry(6, 3); // Slightly larger clouds

        for (let i = 0; i < count; i++) {
            const cloud = new THREE.Mesh(cloudGeo, cloudMaterial.clone()); // Clone material needed if alphaTest differs per cloud later
            cloud.position.set( (Math.random() - 0.5) * 80, 15 + Math.random() * 5, (Math.random() - 0.5) * 50 );
            cloud.rotation.y = Math.random() * Math.PI * 2;
            cloud.rotation.z = Math.random() * 0.2 - 0.1;
            cloud.userData.speed = (Math.random() * 0.05 + 0.02);
            clouds.push(cloud);
            scene.add(cloud);
        }
        console.log(`Created ${clouds.length} clouds (or tried to).`);
    } catch(e) {
         console.error("Error initiating cloud texture load (check path format maybe?):", e);
    }
}


// --- Procedural Generation Functions ---

function createGroundPlane(material = groundMaterial, size = 20) {
    const groundGeo = new THREE.PlaneGeometry(size, size);
    const ground = new THREE.Mesh(groundGeo, material);
    ground.rotation.x = -Math.PI / 2;
    ground.position.y = -0.05;
    ground.receiveShadow = true;
    ground.castShadow = false;
    return ground;
}

function createDefaultAssembly() { /* ... (same as before) ... */
     const group = new THREE.Group(); const sphereGeo = new THREE.SphereGeometry(0.5, 16, 16);
     group.add(createMesh(sphereGeo, stoneMaterial, { x: 0, y: 0.5, z: 0 })); group.add(createGroundPlane()); return group;
}
function createCityGatesAssembly() { /* ... (same as before) ... */
    const group = new THREE.Group(); const gateWallHeight = 4; const gateWallWidth = 1.5; const gateWallDepth = 0.8; const archHeight = 1; const archWidth = 3;
    const towerLeftGeo = new THREE.BoxGeometry(gateWallWidth, gateWallHeight, gateWallDepth); group.add(createMesh(towerLeftGeo, stoneMaterial, { x: -(archWidth / 2 + gateWallWidth / 2), y: gateWallHeight / 2, z: 0 }));
    const towerRightGeo = new THREE.BoxGeometry(gateWallWidth, gateWallHeight, gateWallDepth); group.add(createMesh(towerRightGeo, stoneMaterial, { x: (archWidth / 2 + gateWallWidth / 2), y: gateWallHeight / 2, z: 0 }));
    const archGeo = new THREE.BoxGeometry(archWidth, archHeight, gateWallDepth); group.add(createMesh(archGeo, stoneMaterial, { x: 0, y: gateWallHeight - archHeight / 2, z: 0 }));
    const crenellationSize = 0.4; const crenGeo = new THREE.BoxGeometry(crenellationSize, crenellationSize, gateWallDepth * 1.1);
    for (let i = -Math.floor(gateWallWidth / (crenellationSize * 1.5)); i <= Math.floor(gateWallWidth / (crenellationSize * 1.5)); i++) { const xPosTower = i * crenellationSize * 1.5; const crenMeshLeft = createMesh(crenGeo.clone(), stoneMaterial, { x: -(archWidth / 2 + gateWallWidth / 2) + xPosTower, y: gateWallHeight + crenellationSize / 2, z: 0 }); const crenMeshRight = createMesh(crenGeo.clone(), stoneMaterial, { x: (archWidth / 2 + gateWallWidth / 2) + xPosTower, y: gateWallHeight + crenellationSize / 2, z: 0 }); group.add(crenMeshLeft); group.add(crenMeshRight); }
    for (let i = -Math.floor(archWidth / (crenellationSize * 1.5 * 2)); i <= Math.floor(archWidth / (crenellationSize * 1.5 * 2)); i++){ const xPosArch = i * crenellationSize * 1.5; const crenMeshArch = createMesh(crenGeo.clone(), stoneMaterial, { x: xPosArch, y: gateWallHeight + archHeight - crenellationSize/2, z: 0 }); group.add(crenMeshArch); }
    group.add(createGroundPlane(stoneMaterial)); return group;
}
function createWeaponsmithAssembly() { /* ... (enhanced version from before) ... */
    const group = new THREE.Group(); const buildingWidth = 3; const buildingHeight = 2.5; const buildingDepth = 3.5; const roofPitch = Math.random() * 0.3 + 0.4; const roofHeight = (buildingDepth / 2) * roofPitch; const roofOverhang = 0.2;
    const wallMaterial = Math.random() < 0.6 ? darkWoodMaterial : stoneMaterial; const buildingGeo = new THREE.BoxGeometry(buildingWidth, buildingHeight, buildingDepth); const mainBuilding = createMesh(buildingGeo, wallMaterial, { x: 0, y: buildingHeight / 2, z: 0 }); group.add(mainBuilding);
    const roofMaterial = Math.random() < 0.7 ? woodMaterial : darkWoodMaterial; const roofLength = Math.sqrt(Math.pow(buildingDepth / 2 + roofOverhang, 2) + Math.pow(roofHeight, 2)); const roofGeo = new THREE.PlaneGeometry(buildingWidth + roofOverhang * 2, roofLength); const roofAngle = Math.atan2(roofHeight, buildingDepth / 2); const roofY = buildingHeight + roofHeight / 2 - (roofOverhang * Math.sin(roofAngle)) / 2; const roofZ = (buildingDepth / 4 + roofOverhang / 4) * Math.cos(roofAngle); const roofLeft = createMesh(roofGeo, roofMaterial, { x: 0, y: roofY, z: -roofZ }, { x: roofAngle, y: 0, z: 0 }); const roofRight = createMesh(roofGeo.clone(), roofMaterial, { x: 0, y: roofY, z: roofZ }, { x: -roofAngle, y: 0, z: 0 }); group.add(roofLeft); group.add(roofRight);
    const gableShape = new THREE.Shape(); gableShape.moveTo(-buildingWidth / 2, buildingHeight); gableShape.lineTo(buildingWidth / 2, buildingHeight); gableShape.lineTo(buildingWidth/2, buildingHeight + roofHeight); gableShape.lineTo(-buildingWidth/2, buildingHeight + roofHeight); gableShape.closePath(); const gableGeo = new THREE.ShapeGeometry(gableShape); group.add(createMesh(gableGeo, wallMaterial, { x: 0, y: 0, z: buildingDepth / 2 }, { x: 0, y: 0, z: 0 })); group.add(createMesh(gableGeo.clone(), wallMaterial, { x: 0, y: 0, z: -buildingDepth / 2 }, { x: 0, y: Math.PI, z: 0 }));
    const doorHeight = 1.8; const doorWidth = 0.8; const windowSize = 0.6; const frameThickness = 0.05; const doorGeo = new THREE.BoxGeometry(doorWidth, doorHeight, 0.05); const doorFrameGeo = new THREE.BoxGeometry(doorWidth + frameThickness*2, doorHeight + frameThickness*2, 0.06); const doorSide = Math.random() < 0.5 ? 'front' : 'side'; if (doorSide === 'front') { group.add(createMesh(doorFrameGeo, darkWoodMaterial, { x: 0, y: doorHeight / 2, z: buildingDepth / 2 + 0.03 })); group.add(createMesh(doorGeo, darkWoodMaterial, { x: 0, y: doorHeight / 2, z: buildingDepth / 2 + 0.05 })); } else { group.add(createMesh(doorFrameGeo, darkWoodMaterial, { x: buildingWidth / 2 + 0.03, y: doorHeight / 2, z: 0}, {y: Math.PI/2})); group.add(createMesh(doorGeo, darkWoodMaterial, { x: buildingWidth / 2 + 0.05, y: doorHeight / 2, z: 0}, {y: Math.PI/2})); }
    const windowGeo = new THREE.BoxGeometry(windowSize, windowSize, 0.05); const windowFrameGeo = new THREE.BoxGeometry(windowSize + frameThickness*2, windowSize + frameThickness*2, 0.06); if (Math.random() < 0.7 && doorSide !== 'front') { const winX = (Math.random() - 0.5) * (buildingWidth - windowSize - 0.5); group.add(createMesh(windowFrameGeo, darkWoodMaterial, {x: winX, y: buildingHeight * 0.6, z: buildingDepth / 2 + 0.03})); group.add(createMesh(windowGeo, windowMaterial, { x: winX, y: buildingHeight * 0.6, z: buildingDepth / 2 + 0.05 })); }
    if (Math.random() < 0.6 && doorSide !== 'side') { const winZ = (Math.random() - 0.5) * (buildingDepth - windowSize); group.add(createMesh(windowFrameGeo.clone(), darkWoodMaterial, {x: buildingWidth / 2 + 0.03, y: buildingHeight * 0.6, z: winZ}, {y: Math.PI/2})); group.add(createMesh(windowGeo.clone(), windowMaterial, { x: buildingWidth / 2 + 0.05, y: buildingHeight * 0.6, z: winZ }, {y: Math.PI/2})); } if (Math.random() < 0.6) { const winZ = (Math.random() - 0.5) * (buildingDepth - windowSize); group.add(createMesh(windowFrameGeo.clone(), darkWoodMaterial, {x: -buildingWidth / 2 - 0.03, y: buildingHeight * 0.6, z: winZ}, {y: -Math.PI/2})); group.add(createMesh(windowGeo.clone(), windowMaterial, { x: -buildingWidth / 2 - 0.05, y: buildingHeight * 0.6, z: winZ }, {y: -Math.PI/2})); }
    const forgeHeight = buildingHeight + roofHeight + 0.5; const forgeGeo = new THREE.CylinderGeometry(0.3, 0.4, forgeHeight, 8); group.add(createMesh(forgeGeo, stoneMaterial, { x: buildingWidth * 0.3, y: forgeHeight / 2, z: -buildingDepth * 0.3 }));
    const anvilGeo = new THREE.BoxGeometry(0.4, 0.5, 0.7); group.add(createMesh(anvilGeo, metalMaterial, { x: -buildingWidth * 0.2, y: 0.25, z: buildingDepth * 0.2 })); group.add(createGroundPlane()); return group;
}
function createTempleAssembly() { /* ... (same as before) ... */
    const group = new THREE.Group(); const baseSize = 5; const baseHeight = 0.5; const columnHeight = 3; const columnRadius = 0.25; const roofHeight = 1; const baseGeo = new THREE.BoxGeometry(baseSize, baseHeight, baseSize); group.add(createMesh(baseGeo, templeMaterial, { x: 0, y: baseHeight / 2, z: 0 })); 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 }, ]; const colGeo = new THREE.CylinderGeometry(columnRadius, columnRadius, columnHeight, 12); colPositions.forEach(pos => { group.add(createMesh(colGeo.clone(), templeMaterial, { x: pos.x, y: baseHeight + columnHeight / 2, z: pos.z })); }); const roofGeo = new THREE.BoxGeometry(baseSize * 0.8, roofHeight / 2, baseSize * 0.8); group.add(createMesh(roofGeo, templeMaterial, { x: 0, y: baseHeight + columnHeight + roofHeight / 4, z: 0 })); const pyramidGeo = new THREE.ConeGeometry(baseSize * 0.5, roofHeight * 1.5, 4); group.add(createMesh(pyramidGeo, templeMaterial, { x: 0, y: baseHeight + columnHeight + roofHeight *0.75, z: 0 }, { x: 0, y: Math.PI / 4, z: 0 })); group.add(createGroundPlane()); return group;
}
function createResistanceMeetingAssembly() { /* ... (same as before) ... */
     const group = new THREE.Group(); const tableWidth = 2; const tableHeight = 0.8; const tableDepth = 1; const tableThickness = 0.1; const tableTopGeo = new THREE.BoxGeometry(tableWidth, tableThickness, tableDepth); group.add(createMesh(tableTopGeo, woodMaterial, { x: 0, y: tableHeight - tableThickness / 2, z: 0 })); 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 })); 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 })); group.add(createMesh(stoolGeo.clone(), darkWoodMaterial, { x: 0, y: stoolSize * 0.4, z: -tableDepth * 0.7 })); const wallHeight = 3; const wallThickness = 0.2; const roomSize = 5; const wallBackGeo = new THREE.BoxGeometry(roomSize, wallHeight, wallThickness); group.add(createMesh(wallBackGeo, stoneMaterial, { x: 0, y: wallHeight / 2, z: -roomSize / 2 }, {})); const wallLeftGeo = new THREE.BoxGeometry(wallThickness, wallHeight, roomSize); group.add(createMesh(wallLeftGeo, stoneMaterial, { x: -roomSize / 2, y: wallHeight / 2, z: 0 }, {})); group.add(createGroundPlane(stoneMaterial)); return group;
}
function createForestAssembly(treeCount = 15, area = 12) { /* ... (enhanced version from before) ... */
    const group = new THREE.Group();
    const createTree = (x, z, type) => { const treeGroup = new THREE.Group(); let trunkHeight, trunkRadius, leafMat; if (type === 'pine') { trunkHeight = Math.random() * 3 + 4; trunkRadius = Math.random() * 0.1 + 0.1; leafMat = pineLeafMaterial; } else { trunkHeight = Math.random() * 2 + 2.5; trunkRadius = Math.random() * 0.2 + 0.15; leafMat = leafMaterial; } const trunkGeo = new THREE.CylinderGeometry(trunkRadius * 0.7, trunkRadius, trunkHeight, 8); const trunkMesh = createMesh(trunkGeo, woodMaterial, { x: 0, y: trunkHeight / 2, z: 0 }); treeGroup.add(trunkMesh); const branchCount = Math.floor(Math.random() * 5) + 3; const branchStartHeight = trunkHeight * (0.4 + Math.random() * 0.3); const branchGeo = new THREE.CylinderGeometry(trunkRadius * 0.1, trunkRadius * 0.3, trunkHeight * 0.4, 5); for (let i = 0; i < branchCount; i++) { const yPos = branchStartHeight + Math.random() * (trunkHeight - branchStartHeight) * 0.8; const angleY = Math.random() * Math.PI * 2; const angleX = Math.PI / 3 + Math.random() * Math.PI / 4; const branch = createMesh(branchGeo.clone(), woodMaterial, { x: 0, y: yPos, z: 0 }, { x: angleX, y: angleY, z: 0 } ); const branchLength = trunkHeight * 0.1; branch.position.x = Math.sin(angleY) * Math.sin(angleX) * branchLength; branch.position.z = Math.cos(angleY) * Math.sin(angleX) * branchLength; branch.position.y = yPos; treeGroup.add(branch); } const foliageBaseY = trunkHeight * 0.8; const foliageClusterRadius = trunkRadius * 5 + Math.random() * 1; if (type === 'pine') { const numCones = 3; for(let i=0; i<numCones; i++){ const coneRadius = foliageClusterRadius * (1 - i*0.25); const coneHeight = trunkHeight * 0.5 * (1 - i*0.15); const coneY = foliageBaseY + i * coneHeight * 0.4; const coneGeo = new THREE.ConeGeometry(coneRadius, coneHeight, 8); treeGroup.add(createMesh(coneGeo, leafMat, { x: 0, y: coneY, z: 0 })); } } else { const foliageCount = Math.floor(Math.random() * 5) + 5; const sphereRadius = foliageClusterRadius * 0.3 + Math.random() * 0.2; const sphereGeo = new THREE.SphereGeometry(sphereRadius, 6, 5); for (let i = 0; i < foliageCount; i++) { const offsetX = (Math.random() - 0.5) * foliageClusterRadius * 0.8; const offsetY = Math.random() * foliageClusterRadius * 0.5; const offsetZ = (Math.random() - 0.5) * foliageClusterRadius * 0.8; treeGroup.add(createMesh(sphereGeo.clone(), leafMat, { x: offsetX, y: foliageBaseY + offsetY, z: offsetZ })); } } treeGroup.position.set(x, 0, z); treeGroup.rotation.y = Math.random() * Math.PI * 2; return treeGroup; };
    for (let i = 0; i < treeCount; i++) { const x = (Math.random() - 0.5) * area; const z = (Math.random() - 0.5) * area; const treeType = Math.random() < 0.3 ? 'pine' : 'deciduous'; if (Math.sqrt(x * x + z * z) > 1.5) { group.add(createTree(x, z, treeType)); } else if (i < treeCount / 2) { group.add(createTree(x, z, treeType)); } } group.add(createGroundPlane(groundMaterial, area * 1.1)); return group;
}
function createRoadAmbushAssembly() { /* ... (same as before) ... */
     const group = new THREE.Group(); const area = 12; const forestGroup = createForestAssembly(10, area); group.add(forestGroup); const roadWidth = 3; const roadLength = area * 1.5; 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); const rockGeo = new THREE.DodecahedronGeometry(0.6, 0); const rockMaterial = new THREE.MeshStandardMaterial({ color: 0x666666, roughness: 0.8 }); group.add(createMesh(rockGeo, rockMaterial, {x: roadWidth * 0.7, y: 0.3, z: 1}, {y: Math.random() * Math.PI})); group.add(createMesh(rockGeo.clone().scale(0.7,0.7,0.7), rockMaterial, {x: -roadWidth * 0.8, y: 0.25, z: -2}, {y: Math.random() * Math.PI, x: Math.random()*0.2})); group.add(createMesh(new THREE.DodecahedronGeometry(0.8, 0), rockMaterial, {x: roadWidth * 0.9, y: 0.4, z: -3}, {y: Math.random() * Math.PI})); return group;
}
function createForestEdgeAssembly() { /* ... (same as before) ... */
    const group = new THREE.Group(); const area = 15; const forestGroup = new THREE.Group();
    // Reusing createForestAssembly logic more cleanly might require refactoring createTree out,
    // but this temporary approach works for now.
    const tempTreeCreator = (x, z, type) => { /* Simplified copy or refactor needed */ const treeGroup = new THREE.Group(); let trunkHeight=2, trunkRadius=0.2, leafMat = leafMaterial; if(type==='pine'){trunkHeight=5; trunkRadius=0.1; leafMat=pineLeafMaterial;} const trunkGeo = new THREE.CylinderGeometry(trunkRadius * 0.7, trunkRadius, trunkHeight, 8); treeGroup.add(createMesh(trunkGeo, woodMaterial, { x: 0, y: trunkHeight / 2, z: 0 })); const foliageGeo = new THREE.SphereGeometry(trunkRadius*5, 6, 5); treeGroup.add(createMesh(foliageGeo, leafMat, {x:0, y: trunkHeight*0.9, z:0})); treeGroup.position.set(x, 0, z); treeGroup.rotation.y = Math.random() * Math.PI * 2; return treeGroup; };
    for (let i = 0; i < 20; i++) { const x = (Math.random() - 0.9) * area / 2; const z = (Math.random() - 0.5) * area; const treeType = Math.random() < 0.3 ? 'pine' : 'deciduous'; forestGroup.add(tempTreeCreator(x,z,treeType)); } group.add(forestGroup); group.add(createGroundPlane(groundMaterial, area * 1.2)); return group;
}
function createPrisonerCellAssembly() { /* ... (same as before) ... */
    const group = new THREE.Group(); const cellSize = 3; const wallHeight = 2.5; const wallThickness = 0.2; const barRadius = 0.04; const barSpacing = 0.2; const cellFloorMaterial = stoneMaterial.clone(); cellFloorMaterial.color.setHex(0x555555); group.add(createGroundPlane(cellFloorMaterial, cellSize)); 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 })); const barGeo = new THREE.CylinderGeometry(barRadius, barRadius, wallHeight, 6); const numBars = Math.floor(cellSize / barSpacing); for (let i = 0; i <= numBars; i++) { const xPos = -cellSize / 2 + i * barSpacing + barSpacing/2; group.add(createMesh(barGeo.clone(), metalMaterial, { x: xPos, y: wallHeight / 2, z: cellSize / 2 })); } const horizBarGeo = new THREE.BoxGeometry(cellSize + barSpacing, barRadius * 2.5, barRadius * 2.5); group.add(createMesh(horizBarGeo, metalMaterial, {x: 0, y: wallHeight - barRadius*1.25, z: cellSize/2})); group.add(createMesh(horizBarGeo.clone(), metalMaterial, {x: 0, y: barRadius*1.25, z: cellSize/2})); return group;
}
function createGameOverAssembly() { /* ... (same as before) ... */
     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;
}
function createErrorAssembly() { /* ... (same as before) ... */
     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;
}

// Window Resize
function onWindowResize() {
    if (!renderer || !camera) return;
    camera.aspect = sceneContainer.clientWidth / sceneContainer.clientHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(sceneContainer.clientWidth, sceneContainer.clientHeight);
}

// Animation Loop
function animate() {
    requestAnimationFrame(animate);
    const delta = clock.getDelta();

    // Sun Movement
    const sunSpeed = 0.05; sunAngle += delta * sunSpeed;
    const sunDistance = 40; const sunHeight = 30; const duskAngle = Math.PI * 0.15;
    directionalLight.position.x = Math.cos(sunAngle) * sunDistance;
    directionalLight.position.y = Math.max(0.1, Math.sin(sunAngle) * sunHeight);
    directionalLight.position.z = Math.sin(sunAngle * 0.75) * sunDistance * 0.6;

    // Sun Color/Intensity
    const normalizedY = directionalLight.position.y / sunHeight;
    directionalLight.intensity = Math.max(0.1, normalizedY * 1.5);
    const white = new THREE.Color(0xffffff); const dusk = new THREE.Color(0xFFAB6B);
    const blendFactor = Math.max(0, Math.min(1, Math.pow(1 - normalizedY, 2)));
    if (Math.sin(sunAngle) > 0) { directionalLight.color.lerpColors(white, dusk, blendFactor); }
    else { directionalLight.intensity = 0.05; directionalLight.color.set(0x6688cc); }

    // Cloud Movement
    clouds.forEach(cloud => {
        cloud.position.x += cloud.userData.speed * delta * 50;
        if (cloud.position.x > 60) { cloud.position.x = -60; cloud.position.z = (Math.random() - 0.5) * 50; }
        cloud.lookAt(camera.position); // Billboarding
    });

    // Render
    if (renderer && scene && camera) { renderer.render(scene, camera); }
}


// --- Game Data ---
const itemsData = {
    "Flaming Sword": { type: "weapon", description: "A fiery blade" }, "Whispering Bow": { type: "weapon", description: "A silent bow" }, "Guardian Shield": { type: "armor", description: "A protective shield" }, "Healing Light Spell": { type: "spell", description: "Mends minor wounds" }, "Shield of Faith Spell": { type: "spell", description: "Temporary shield" }, "Binding Runes Scroll": { type: "spell", description: "Binds an enemy" }, "Secret Tunnel Map": { type: "quest", description: "Shows a hidden path" }, "Poison Daggers": { type: "weapon", description: "Daggers with poison" }, "Master Key": { type: "quest", description: "Unlocks many doors" },
    "Scout's Pouch": { type: "quest", description: "Contains odds and ends."} // Added item from example reward
};

const gameData = {
    "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" },
    "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" },
    "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" },
    "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" },
    "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" },
    "6": { title: "Ambush!", content: "<p>...</p>", options: [{ text: "Fight!", next: 9 }, { text: "Try to flee!", next: 10 }], illustration: "road-ambush" },
    "7": { title: "River Path", content: "<p>...</p>", options: [{ text: "Continue", next: 11 }, { text: "Investigate", next: 12 }], illustration: "river-spirit" /* TODO */ },
    "8": { title: "Ancient Ruins", content: "<p>...</p>", options: [{ text: "Search", next: 13 }, { text: "Look for passages", next: 14 }], illustration: "ancient-ruins" /* TODO */ },
    "9": { title: "Victory!", content: "<p>...</p>", options: [{ text: "Proceed", next: 15 }], illustration: "forest-edge", reward: { xp: 75, statIncrease: { stat: "strength", amount: 1 }, addItem: "Scout's Pouch" } }, // Example Reward
    "10": { title: "Captured!", content: "<p>...</p>", options: [{ text: "Wait", next: 20 }], illustration: "prisoner-cell" },
    // Add many more pages...
    "15": { title: "Fortress Plains", content: "<p>...</p>", options: [{ text: "Approach gate", next: 30 }, { text: "Scout", next: 31 }], illustration: "fortress-plains" /* TODO */ },
    "20": { title: "Inside the Cell", content: "<p>...</p>", options: [{ text: "Look for weaknesses", next: 21 }, { text: "Talk to guard", next: 22 }], illustration: "prisoner-cell" },
    "99": { title: "Game Over", content: "<p>Your adventure ends here.</p>", options: [{ text: "Restart", next: 1 }], illustration: "game-over", gameOver: true }
};


// --- Game State ---
let gameState = {
    currentPageId: 1,
    character: {
        name: "Hero", race: "Human", alignment: "Neutral Good", class: "Fighter",
        level: 1, xp: 0, xpToNextLevel: 100, statPointsPerLevel: 1, availableStatPoints: 0,
        stats: { strength: 7, intelligence: 5, wisdom: 5, dexterity: 6, constitution: 6, charisma: 5, hp: 30, maxHp: 30 },
        inventory: []
    }
};


// --- Character Sheet Functions ---

function renderCharacterSheet() {
    const char = gameState.character;
    charNameInput.value = char.name;
    charRaceSpan.textContent = char.race; charAlignmentSpan.textContent = char.alignment; charClassSpan.textContent = char.class;
    charLevelSpan.textContent = char.level; charXPSpan.textContent = char.xp; charXPNextSpan.textContent = char.xpToNextLevel;
    char.stats.hp = Math.min(char.stats.hp, char.stats.maxHp); charHPSpan.textContent = char.stats.hp; charMaxHPSpan.textContent = char.stats.maxHp;
    for (const stat in statSpans) { if (statSpans.hasOwnProperty(stat) && char.stats.hasOwnProperty(stat)) { statSpans[stat].textContent = char.stats[stat]; } }
    charInventoryList.innerHTML = ''; const maxSlots = 15;
    for (let i = 0; i < maxSlots; i++) { const li = document.createElement('li'); if (i < char.inventory.length) { const item = char.inventory[i]; const itemInfo = itemsData[item] || { type: 'unknown', description: '???' }; const itemSpan = document.createElement('span'); itemSpan.classList.add(`item-${itemInfo.type || 'unknown'}`); itemSpan.title = itemInfo.description; itemSpan.textContent = item; li.appendChild(itemSpan); } else { const emptySlotSpan = document.createElement('span'); emptySlotSpan.classList.add('item-slot'); emptySlotSpan.textContent = '[Empty]'; li.appendChild(emptySlotSpan); } charInventoryList.appendChild(li); }
    updateLevelUpAvailability();
}

function calculateStatIncreaseCost() { return (gameState.character.level * 10) + 5; }

function updateLevelUpAvailability() {
    const char = gameState.character; const canLevelUp = char.xp >= char.xpToNextLevel; levelUpButton.disabled = !canLevelUp;
    const cost = calculateStatIncreaseCost(); const canIncreaseWithXP = char.xp >= cost; const canIncreaseWithPoints = char.availableStatPoints > 0;
    statIncreaseButtons.forEach(button => { button.disabled = !(canIncreaseWithPoints || canIncreaseWithXP); });
    statIncreaseCostSpan.textContent = cost; statPointsAvailableSpan.textContent = char.availableStatPoints;
}

function handleLevelUp() {
    const char = gameState.character; if (char.xp >= char.xpToNextLevel) { char.level++; char.xp -= char.xpToNextLevel; char.xpToNextLevel = Math.floor(char.xpToNextLevel * 1.6); char.availableStatPoints += char.statPointsPerLevel; const conModifier = Math.floor((char.stats.constitution - 10) / 2); const hpGain = Math.max(1, Math.floor(Math.random() * 6) + 1 + conModifier); char.stats.maxHp += hpGain; char.stats.hp = char.stats.maxHp; console.log(`Leveled Up to ${char.level}! Gained ${char.statPointsPerLevel} stat point(s) and ${hpGain} HP.`); renderCharacterSheet(); } else { console.warn("Not enough XP to level up yet."); }
}

function handleStatIncrease(statName) {
    const char = gameState.character; const cost = calculateStatIncreaseCost();
    if (char.availableStatPoints > 0) { char.stats[statName]++; char.availableStatPoints--; console.log(`Increased ${statName} using a point. ${char.availableStatPoints} points remaining.`); if (statName === 'constitution') { const oldMod = Math.floor((char.stats.constitution - 1 - 10) / 2); const newMod = Math.floor((char.stats.constitution - 10) / 2); const hpBonus = Math.max(0, newMod - oldMod) * char.level; if(hpBonus > 0){ char.stats.maxHp += hpBonus; char.stats.hp += hpBonus; console.log(`+${hpBonus} HP from CON.`);} } renderCharacterSheet(); return; }
    if (char.xp >= cost) { char.stats[statName]++; char.xp -= cost; console.log(`Increased ${statName} for ${cost} XP.`); if (statName === 'constitution') { const oldMod = Math.floor((char.stats.constitution - 1 - 10) / 2); const newMod = Math.floor((char.stats.constitution - 10) / 2); const hpBonus = Math.max(0, newMod - oldMod) * char.level; if(hpBonus > 0){ char.stats.maxHp += hpBonus; char.stats.hp += hpBonus; console.log(`+${hpBonus} HP from CON.`);} } renderCharacterSheet(); } else { console.warn(`Not enough XP or points to increase ${statName}.`); }
}

function saveCharacter() { try { localStorage.setItem('textAdventureCharacter', JSON.stringify(gameState.character)); console.log('Character saved locally.'); saveCharButton.textContent = 'Saved!'; saveCharButton.disabled = true; setTimeout(() => { saveCharButton.textContent = 'Save'; saveCharButton.disabled = false; }, 1500); } catch (e) { console.error('Error saving character:', e); alert('Failed to save character.'); } }

function loadCharacter() { try { const savedData = localStorage.getItem('textAdventureCharacter'); if (savedData) { const loadedChar = JSON.parse(savedData); gameState.character = { ...gameState.character, ...loadedChar, stats: { ...gameState.character.stats, ...(loadedChar.stats || {}) }, inventory: loadedChar.inventory || [] }; console.log('Character loaded from local storage.'); return true; } } catch (e) { console.error('Error loading character:', e); } return false; }

function exportCharacter() { try { const charJson = JSON.stringify(gameState.character, null, 2); const blob = new Blob([charJson], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; const filename = `${gameState.character.name.replace(/[^a-z0-9]/gi, '_').toLowerCase() || 'character'}_save.json`; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); console.log(`Character exported as ${filename}`); } catch (e) { console.error('Error exporting character:', e); alert('Failed to export character data.'); } }

// Event Listeners
charNameInput.addEventListener('change', () => { gameState.character.name = charNameInput.value.trim() || "Hero"; console.log(`Name changed to: ${gameState.character.name}`); });
levelUpButton.addEventListener('click', handleLevelUp);
statIncreaseButtons.forEach(button => { button.addEventListener('click', () => { const statToIncrease = button.dataset.stat; if (statToIncrease) { handleStatIncrease(statToIncrease); } }); });
saveCharButton.addEventListener('click', saveCharacter);
exportCharButton.addEventListener('click', exportCharacter);


// --- Game Logic Functions ---

function startGame() {
    if (!loadCharacter()) { console.log("No saved character found, starting new."); }
     // Ensure full character structure after load
     const defaultChar = { name: "Hero", race: "Human", alignment: "Neutral Good", class: "Fighter", level: 1, xp: 0, xpToNextLevel: 100, statPointsPerLevel: 1, availableStatPoints: 0, stats: { strength: 7, intelligence: 5, wisdom: 5, dexterity: 6, constitution: 6, charisma: 5, hp: 30, maxHp: 30 }, inventory: [] };
     gameState.character = { ...defaultChar, ...gameState.character };
     gameState.character.stats = { ...defaultChar.stats, ...(gameState.character.stats || {}) };

    gameState.currentPageId = 1;
    renderCharacterSheet();
    renderPage(gameState.currentPageId);
}

function renderPage(pageId) {
    const page = gameData[pageId];
    if (!page) { console.error(`Error: Page data not found for ID: ${pageId}`); storyTitleElement.textContent = "Error"; storyContentElement.innerHTML = "<p>Could not load page data.</p>"; choicesElement.innerHTML = '<button class="choice-button" onclick="handleChoiceClick({ nextPage: 1 })">Restart</button>'; updateScene('error'); return; }
    storyTitleElement.textContent = page.title || "Untitled Page"; storyContentElement.innerHTML = page.content || "<p>...</p>";
    choicesElement.innerHTML = '';
    if (page.options && page.options.length > 0) {
        page.options.forEach(option => {
            const button = document.createElement('button'); button.classList.add('choice-button'); button.textContent = option.text; let requirementMet = true;
            if (option.requireItem && !gameState.character.inventory.includes(option.requireItem)) { requirementMet = false; button.title = `Requires: ${option.requireItem}`; button.disabled = true; }
            if (requirementMet) { const choiceData = { nextPage: option.next }; if (option.addItem) { choiceData.addItem = option.addItem; } button.addEventListener('click', () => handleChoiceClick(choiceData)); } else { button.classList.add('disabled'); } choicesElement.appendChild(button); });
    } else if (page.gameOver) { const button = document.createElement('button'); button.classList.add('choice-button'); button.textContent = "Restart Adventure"; button.addEventListener('click', () => handleChoiceClick({ nextPage: 1 })); choicesElement.appendChild(button); } else { choicesElement.innerHTML = '<p><i>There are no further paths from here.</i></p>'; const button = document.createElement('button'); button.classList.add('choice-button'); button.textContent = "Restart Adventure"; button.addEventListener('click', () => handleChoiceClick({ nextPage: 1 })); choicesElement.appendChild(button); }
    updateScene(page.illustration || 'default');
}

function handleChoiceClick(choiceData) {
    const nextPageId = parseInt(choiceData.nextPage); const itemToAdd = choiceData.addItem; if (isNaN(nextPageId)) { console.error("Invalid nextPageId:", choiceData.nextPage); return; }
    if (itemToAdd && !gameState.character.inventory.includes(itemToAdd)) { gameState.character.inventory.push(itemToAdd); console.log("Added item:", itemToAdd); }
    gameState.currentPageId = nextPageId; const nextPageData = gameData[nextPageId];
    if (nextPageData) { if (nextPageData.hpLoss) { gameState.character.stats.hp -= nextPageData.hpLoss; console.log(`Lost ${nextPageData.hpLoss} HP.`); if (gameState.character.stats.hp <= 0) { gameState.character.stats.hp = 0; console.log("Player died!"); renderCharacterSheet(); renderPage(99); return; } }
        if (nextPageData.reward) { if (nextPageData.reward.xp) { gameState.character.xp += nextPageData.reward.xp; console.log(`Gained ${nextPageData.reward.xp} XP!`); } if (nextPageData.reward.statIncrease) { const stat = nextPageData.reward.statIncrease.stat; const amount = nextPageData.reward.statIncrease.amount; if (gameState.character.stats.hasOwnProperty(stat)) { gameState.character.stats[stat] += amount; console.log(`Stat ${stat} increased by ${amount}!`); if (stat === 'constitution') { const oldMod = Math.floor((gameState.character.stats.constitution - amount - 10) / 2); const newMod = Math.floor((gameState.character.stats.constitution - 10) / 2); const hpBonus = Math.max(0, newMod - oldMod) * gameState.character.level; if(hpBonus > 0){ gameState.character.stats.maxHp += hpBonus; gameState.character.stats.hp += hpBonus; console.log(`+${hpBonus} HP from CON.`);} } } } if(nextPageData.reward.addItem && !gameState.character.inventory.includes(nextPageData.reward.addItem)){ gameState.character.inventory.push(nextPageData.reward.addItem); console.log(`Found item: ${nextPageData.reward.addItem}`); } }
        if (nextPageData.gameOver) { console.log("Reached Game Over."); renderCharacterSheet(); renderPage(nextPageId); return; }
    } else { console.error(`Data for page ${nextPageId} not found!`); renderCharacterSheet(); renderPage(99); return; }
    renderCharacterSheet(); renderPage(nextPageId);
}

// Scene Update Function
function updateScene(illustrationKey) {
    console.log(`Updating scene for key: "${illustrationKey}"`);
    if (currentAssemblyGroup) { scene.remove(currentAssemblyGroup); /* TODO: Dispose if needed */ }
    currentAssemblyGroup = null; let assemblyFunction;
    switch (illustrationKey) {
        case 'city-gates': assemblyFunction = createCityGatesAssembly; break;
        case 'weaponsmith': assemblyFunction = createWeaponsmithAssembly; break;
        case 'temple': assemblyFunction = createTempleAssembly; break;
        case 'resistance-meeting': assemblyFunction = createResistanceMeetingAssembly; break;
        case 'shadowwood-forest': assemblyFunction = createForestAssembly; break;
        case 'road-ambush': assemblyFunction = createRoadAmbushAssembly; break;
        case 'forest-edge': assemblyFunction = createForestEdgeAssembly; break;
        case 'prisoner-cell': assemblyFunction = createPrisonerCellAssembly; break;
        case 'game-over': assemblyFunction = createGameOverAssembly; break;
        case 'error': assemblyFunction = createErrorAssembly; break;
        case 'river-spirit': console.warn("Scene 'river-spirit' not implemented."); assemblyFunction = createDefaultAssembly; break;
        case 'ancient-ruins': console.warn("Scene 'ancient-ruins' not implemented."); assemblyFunction = createDefaultAssembly; break;
        case 'fortress-plains': console.warn("Scene 'fortress-plains' not implemented."); assemblyFunction = createDefaultAssembly; break;
        default: console.warn(`Unknown illustration key: "${illustrationKey}". Using default.`); assemblyFunction = createDefaultAssembly; break;
    }
    try { currentAssemblyGroup = assemblyFunction(); } catch (error) { console.error(`Error creating assembly for ${illustrationKey}:`, error); currentAssemblyGroup = createErrorAssembly(); }
    if (currentAssemblyGroup) { scene.add(currentAssemblyGroup); } else { console.error(`Assembly failed for ${illustrationKey}.`); currentAssemblyGroup = createErrorAssembly(); scene.add(currentAssemblyGroup); }
}


// --- Initialization ---
initThreeJS(); // Set up the 3D scene first
startGame(); // Load data, render character sheet, and show first page