awacke1 commited on
Commit
087c83f
·
verified ·
1 Parent(s): 124d01b

Create script.js

Browse files
Files changed (1) hide show
  1. script.js +426 -0
script.js ADDED
@@ -0,0 +1,426 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as THREE from 'three';
2
+ // Optional: Add OrbitControls for debugging/viewing scene
3
+ // import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
4
+
5
+ // --- DOM Elements ---
6
+ const sceneContainer = document.getElementById('scene-container');
7
+ const storyTitleElement = document.getElementById('story-title');
8
+ const storyContentElement = document.getElementById('story-content');
9
+ const choicesElement = document.getElementById('choices');
10
+ // Removed old stats/inventory elements
11
+ // const statsElement = document.getElementById('stats-display');
12
+ // const inventoryElement = document.getElementById('inventory-display');
13
+
14
+ // Character Sheet Elements
15
+ const charNameInput = document.getElementById('char-name');
16
+ const charRaceSpan = document.getElementById('char-race');
17
+ const charAlignmentSpan = document.getElementById('char-alignment');
18
+ const charClassSpan = document.getElementById('char-class');
19
+ const charLevelSpan = document.getElementById('char-level');
20
+ const charXPSpan = document.getElementById('char-xp');
21
+ const charXPNextSpan = document.getElementById('char-xp-next');
22
+ const charHPSpan = document.getElementById('char-hp');
23
+ const charMaxHPSpan = document.getElementById('char-max-hp');
24
+ const charInventoryList = document.getElementById('char-inventory-list');
25
+ const statSpans = {
26
+ strength: document.getElementById('stat-strength'), intelligence: document.getElementById('stat-intelligence'),
27
+ wisdom: document.getElementById('stat-wisdom'), dexterity: document.getElementById('stat-dexterity'),
28
+ constitution: document.getElementById('stat-constitution'), charisma: document.getElementById('stat-charisma'),
29
+ };
30
+ const statIncreaseButtons = document.querySelectorAll('.stat-increase');
31
+ const levelUpButton = document.getElementById('levelup-btn');
32
+ const saveCharButton = document.getElementById('save-char-btn');
33
+ const exportCharButton = document.getElementById('export-char-btn');
34
+ const statIncreaseCostSpan = document.getElementById('stat-increase-cost');
35
+ const statPointsAvailableSpan = document.getElementById('stat-points-available');
36
+
37
+
38
+ // --- Three.js Setup ---
39
+ let scene, camera, renderer;
40
+ let currentAssemblyGroup = null;
41
+ let directionalLight;
42
+ let sunAngle = Math.PI / 4; // Start sun partway through morning
43
+ const clock = new THREE.Clock();
44
+ let clouds = [];
45
+ // let controls;
46
+
47
+ // --- Shared Materials ---
48
+ const stoneMaterial = new THREE.MeshStandardMaterial({ color: 0x888888, roughness: 0.8, metalness: 0.1 });
49
+ const woodMaterial = new THREE.MeshStandardMaterial({ color: 0x8B4513, roughness: 0.7, metalness: 0 });
50
+ const darkWoodMaterial = new THREE.MeshStandardMaterial({ color: 0x5C3D20, roughness: 0.7, metalness: 0 });
51
+ const leafMaterial = new THREE.MeshStandardMaterial({ color: 0x2E8B57, roughness: 0.6, metalness: 0 });
52
+ const pineLeafMaterial = new THREE.MeshStandardMaterial({ color: 0x1A5A2A, roughness: 0.7, metalness: 0 });
53
+ const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x556B2F, roughness: 0.9, metalness: 0 });
54
+ const metalMaterial = new THREE.MeshStandardMaterial({ color: 0xaaaaaa, metalness: 0.8, roughness: 0.3 });
55
+ const fabricMaterial = new THREE.MeshStandardMaterial({ color: 0x696969, roughness: 0.9, metalness: 0 });
56
+ const waterMaterial = new THREE.MeshStandardMaterial({ color: 0x60A3D9, roughness: 0.2, metalness: 0.1, transparent: true, opacity: 0.7 });
57
+ const templeMaterial = new THREE.MeshStandardMaterial({ color: 0xA99B78, roughness: 0.7, metalness: 0.1 });
58
+ const fireMaterial = new THREE.MeshStandardMaterial({ color: 0xFF4500, emissive: 0xff6600, roughness: 0.5, metalness: 0 });
59
+ const errorMaterial = new THREE.MeshStandardMaterial({ color: 0xffa500, roughness: 0.5 });
60
+ const gameOverMaterial = new THREE.MeshStandardMaterial({ color: 0xff0000, roughness: 0.5 });
61
+ const windowMaterial = new THREE.MeshStandardMaterial({ color: 0x334455, roughness: 0.3, metalness: 0, transparent: true, opacity: 0.6 });
62
+
63
+
64
+ function initThreeJS() {
65
+ scene = new THREE.Scene();
66
+
67
+ // Skybox Loading
68
+ const loader = new THREE.CubeTextureLoader();
69
+ // !!! IMPORTANT: Replace this path with the correct one for your textures !!!
70
+ const texturePath = 'textures/skybox/';
71
+ const textureFiles = ['posx.jpg', 'negx.jpg', 'posy.jpg', 'negy.jpg', 'posz.jpg', 'negz.jpg'];
72
+
73
+ try {
74
+ const texture = loader.setPath(texturePath).load(textureFiles,
75
+ () => { console.log("Skybox loaded"); scene.background = texture; },
76
+ undefined,
77
+ (err) => { console.error(`Skybox loading error from ${texturePath}:`, err); scene.background = new THREE.Color(0x557799); }
78
+ );
79
+ } catch (e) {
80
+ console.error("Error initiating skybox load (check path format maybe?):", e);
81
+ scene.background = new THREE.Color(0x557799); // Fallback
82
+ }
83
+
84
+
85
+ camera = new THREE.PerspectiveCamera(60, sceneContainer.clientWidth / sceneContainer.clientHeight, 0.1, 1000);
86
+ camera.position.set(0, 3, 9);
87
+ camera.lookAt(0, 1, 0);
88
+
89
+ renderer = new THREE.WebGLRenderer({ antialias: true });
90
+ renderer.setSize(sceneContainer.clientWidth, sceneContainer.clientHeight);
91
+ renderer.shadowMap.enabled = true;
92
+ renderer.shadowMap.type = THREE.PCFSoftShadowMap;
93
+ renderer.toneMapping = THREE.ACESFilmicToneMapping;
94
+ renderer.toneMappingExposure = 1.0;
95
+ sceneContainer.appendChild(renderer.domElement);
96
+
97
+ // Lighting
98
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
99
+ scene.add(ambientLight);
100
+
101
+ directionalLight = new THREE.DirectionalLight(0xffffff, 1.5);
102
+ directionalLight.position.set(20, 30, 15); // Initial position, updated in animate
103
+ directionalLight.target.position.set(0, 0, 0);
104
+ directionalLight.castShadow = true;
105
+ directionalLight.shadow.mapSize.width = 1024;
106
+ directionalLight.shadow.mapSize.height = 1024;
107
+ directionalLight.shadow.camera.near = 1;
108
+ directionalLight.shadow.camera.far = 100;
109
+ const shadowCamSize = 25;
110
+ directionalLight.shadow.camera.left = -shadowCamSize; directionalLight.shadow.camera.right = shadowCamSize;
111
+ directionalLight.shadow.camera.top = shadowCamSize; directionalLight.shadow.camera.bottom = -shadowCamSize;
112
+ directionalLight.shadow.bias = -0.001;
113
+ scene.add(directionalLight);
114
+ scene.add(directionalLight.target);
115
+
116
+ // Clouds
117
+ createClouds(15);
118
+
119
+ window.addEventListener('resize', onWindowResize, false);
120
+ animate();
121
+ }
122
+
123
+ // Helper function to create meshes
124
+ 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 }) {
125
+ const mesh = new THREE.Mesh(geometry, material);
126
+ mesh.position.set(position.x, position.y, position.z);
127
+ mesh.rotation.set(rotation.x, rotation.y, rotation.z);
128
+ mesh.scale.set(scale.x, scale.y, scale.z);
129
+ mesh.castShadow = true;
130
+ mesh.receiveShadow = true;
131
+ return mesh;
132
+ }
133
+
134
+
135
+ // Cloud Creation
136
+ function createClouds(count) {
137
+ const textureLoader = new THREE.TextureLoader();
138
+ // !!! IMPORTANT: Replace this path with the correct one for your cloud texture !!!
139
+ const cloudTexturePath = 'textures/cloud.png';
140
+ try {
141
+ const cloudTexture = textureLoader.load(cloudTexturePath,
142
+ () => { console.log("Cloud texture loaded"); }, undefined,
143
+ (err) => { console.error(`Cloud texture loading error from ${cloudTexturePath}:`, err); }
144
+ );
145
+
146
+ const cloudMaterial = new THREE.MeshBasicMaterial({
147
+ map: cloudTexture, transparent: true, alphaTest: 0.2,
148
+ depthWrite: false, side: THREE.DoubleSide,
149
+ });
150
+ const cloudGeo = new THREE.PlaneGeometry(6, 3); // Slightly larger clouds
151
+
152
+ for (let i = 0; i < count; i++) {
153
+ const cloud = new THREE.Mesh(cloudGeo, cloudMaterial.clone()); // Clone material needed if alphaTest differs per cloud later
154
+ cloud.position.set( (Math.random() - 0.5) * 80, 15 + Math.random() * 5, (Math.random() - 0.5) * 50 );
155
+ cloud.rotation.y = Math.random() * Math.PI * 2;
156
+ cloud.rotation.z = Math.random() * 0.2 - 0.1;
157
+ cloud.userData.speed = (Math.random() * 0.05 + 0.02);
158
+ clouds.push(cloud);
159
+ scene.add(cloud);
160
+ }
161
+ console.log(`Created ${clouds.length} clouds (or tried to).`);
162
+ } catch(e) {
163
+ console.error("Error initiating cloud texture load (check path format maybe?):", e);
164
+ }
165
+ }
166
+
167
+
168
+ // --- Procedural Generation Functions ---
169
+
170
+ function createGroundPlane(material = groundMaterial, size = 20) {
171
+ const groundGeo = new THREE.PlaneGeometry(size, size);
172
+ const ground = new THREE.Mesh(groundGeo, material);
173
+ ground.rotation.x = -Math.PI / 2;
174
+ ground.position.y = -0.05;
175
+ ground.receiveShadow = true;
176
+ ground.castShadow = false;
177
+ return ground;
178
+ }
179
+
180
+ function createDefaultAssembly() { /* ... (same as before) ... */
181
+ const group = new THREE.Group(); const sphereGeo = new THREE.SphereGeometry(0.5, 16, 16);
182
+ group.add(createMesh(sphereGeo, stoneMaterial, { x: 0, y: 0.5, z: 0 })); group.add(createGroundPlane()); return group;
183
+ }
184
+ function createCityGatesAssembly() { /* ... (same as before) ... */
185
+ const group = new THREE.Group(); const gateWallHeight = 4; const gateWallWidth = 1.5; const gateWallDepth = 0.8; const archHeight = 1; const archWidth = 3;
186
+ const towerLeftGeo = new THREE.BoxGeometry(gateWallWidth, gateWallHeight, gateWallDepth); group.add(createMesh(towerLeftGeo, stoneMaterial, { x: -(archWidth / 2 + gateWallWidth / 2), y: gateWallHeight / 2, z: 0 }));
187
+ const towerRightGeo = new THREE.BoxGeometry(gateWallWidth, gateWallHeight, gateWallDepth); group.add(createMesh(towerRightGeo, stoneMaterial, { x: (archWidth / 2 + gateWallWidth / 2), y: gateWallHeight / 2, z: 0 }));
188
+ const archGeo = new THREE.BoxGeometry(archWidth, archHeight, gateWallDepth); group.add(createMesh(archGeo, stoneMaterial, { x: 0, y: gateWallHeight - archHeight / 2, z: 0 }));
189
+ const crenellationSize = 0.4; const crenGeo = new THREE.BoxGeometry(crenellationSize, crenellationSize, gateWallDepth * 1.1);
190
+ 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); }
191
+ 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); }
192
+ group.add(createGroundPlane(stoneMaterial)); return group;
193
+ }
194
+ function createWeaponsmithAssembly() { /* ... (enhanced version from before) ... */
195
+ 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;
196
+ 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);
197
+ 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);
198
+ 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 }));
199
+ 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})); }
200
+ 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 })); }
201
+ 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})); }
202
+ 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 }));
203
+ 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;
204
+ }
205
+ function createTempleAssembly() { /* ... (same as before) ... */
206
+ 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;
207
+ }
208
+ function createResistanceMeetingAssembly() { /* ... (same as before) ... */
209
+ 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;
210
+ }
211
+ function createForestAssembly(treeCount = 15, area = 12) { /* ... (enhanced version from before) ... */
212
+ const group = new THREE.Group();
213
+ 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; };
214
+ 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;
215
+ }
216
+ function createRoadAmbushAssembly() { /* ... (same as before) ... */
217
+ 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;
218
+ }
219
+ function createForestEdgeAssembly() { /* ... (same as before) ... */
220
+ const group = new THREE.Group(); const area = 15; const forestGroup = new THREE.Group();
221
+ // Reusing createForestAssembly logic more cleanly might require refactoring createTree out,
222
+ // but this temporary approach works for now.
223
+ 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; };
224
+ 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;
225
+ }
226
+ function createPrisonerCellAssembly() { /* ... (same as before) ... */
227
+ 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;
228
+ }
229
+ function createGameOverAssembly() { /* ... (same as before) ... */
230
+ 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;
231
+ }
232
+ function createErrorAssembly() { /* ... (same as before) ... */
233
+ 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;
234
+ }
235
+
236
+ // Window Resize
237
+ function onWindowResize() {
238
+ if (!renderer || !camera) return;
239
+ camera.aspect = sceneContainer.clientWidth / sceneContainer.clientHeight;
240
+ camera.updateProjectionMatrix();
241
+ renderer.setSize(sceneContainer.clientWidth, sceneContainer.clientHeight);
242
+ }
243
+
244
+ // Animation Loop
245
+ function animate() {
246
+ requestAnimationFrame(animate);
247
+ const delta = clock.getDelta();
248
+
249
+ // Sun Movement
250
+ const sunSpeed = 0.05; sunAngle += delta * sunSpeed;
251
+ const sunDistance = 40; const sunHeight = 30; const duskAngle = Math.PI * 0.15;
252
+ directionalLight.position.x = Math.cos(sunAngle) * sunDistance;
253
+ directionalLight.position.y = Math.max(0.1, Math.sin(sunAngle) * sunHeight);
254
+ directionalLight.position.z = Math.sin(sunAngle * 0.75) * sunDistance * 0.6;
255
+
256
+ // Sun Color/Intensity
257
+ const normalizedY = directionalLight.position.y / sunHeight;
258
+ directionalLight.intensity = Math.max(0.1, normalizedY * 1.5);
259
+ const white = new THREE.Color(0xffffff); const dusk = new THREE.Color(0xFFAB6B);
260
+ const blendFactor = Math.max(0, Math.min(1, Math.pow(1 - normalizedY, 2)));
261
+ if (Math.sin(sunAngle) > 0) { directionalLight.color.lerpColors(white, dusk, blendFactor); }
262
+ else { directionalLight.intensity = 0.05; directionalLight.color.set(0x6688cc); }
263
+
264
+ // Cloud Movement
265
+ clouds.forEach(cloud => {
266
+ cloud.position.x += cloud.userData.speed * delta * 50;
267
+ if (cloud.position.x > 60) { cloud.position.x = -60; cloud.position.z = (Math.random() - 0.5) * 50; }
268
+ cloud.lookAt(camera.position); // Billboarding
269
+ });
270
+
271
+ // Render
272
+ if (renderer && scene && camera) { renderer.render(scene, camera); }
273
+ }
274
+
275
+
276
+ // --- Game Data ---
277
+ const itemsData = {
278
+ "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" },
279
+ "Scout's Pouch": { type: "quest", description: "Contains odds and ends."} // Added item from example reward
280
+ };
281
+
282
+ const gameData = {
283
+ "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" },
284
+ "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" },
285
+ "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" },
286
+ "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" },
287
+ "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" },
288
+ "6": { title: "Ambush!", content: "<p>...</p>", options: [{ text: "Fight!", next: 9 }, { text: "Try to flee!", next: 10 }], illustration: "road-ambush" },
289
+ "7": { title: "River Path", content: "<p>...</p>", options: [{ text: "Continue", next: 11 }, { text: "Investigate", next: 12 }], illustration: "river-spirit" /* TODO */ },
290
+ "8": { title: "Ancient Ruins", content: "<p>...</p>", options: [{ text: "Search", next: 13 }, { text: "Look for passages", next: 14 }], illustration: "ancient-ruins" /* TODO */ },
291
+ "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
292
+ "10": { title: "Captured!", content: "<p>...</p>", options: [{ text: "Wait", next: 20 }], illustration: "prisoner-cell" },
293
+ // Add many more pages...
294
+ "15": { title: "Fortress Plains", content: "<p>...</p>", options: [{ text: "Approach gate", next: 30 }, { text: "Scout", next: 31 }], illustration: "fortress-plains" /* TODO */ },
295
+ "20": { title: "Inside the Cell", content: "<p>...</p>", options: [{ text: "Look for weaknesses", next: 21 }, { text: "Talk to guard", next: 22 }], illustration: "prisoner-cell" },
296
+ "99": { title: "Game Over", content: "<p>Your adventure ends here.</p>", options: [{ text: "Restart", next: 1 }], illustration: "game-over", gameOver: true }
297
+ };
298
+
299
+
300
+ // --- Game State ---
301
+ let gameState = {
302
+ currentPageId: 1,
303
+ character: {
304
+ name: "Hero", race: "Human", alignment: "Neutral Good", class: "Fighter",
305
+ level: 1, xp: 0, xpToNextLevel: 100, statPointsPerLevel: 1, availableStatPoints: 0,
306
+ stats: { strength: 7, intelligence: 5, wisdom: 5, dexterity: 6, constitution: 6, charisma: 5, hp: 30, maxHp: 30 },
307
+ inventory: []
308
+ }
309
+ };
310
+
311
+
312
+ // --- Character Sheet Functions ---
313
+
314
+ function renderCharacterSheet() {
315
+ const char = gameState.character;
316
+ charNameInput.value = char.name;
317
+ charRaceSpan.textContent = char.race; charAlignmentSpan.textContent = char.alignment; charClassSpan.textContent = char.class;
318
+ charLevelSpan.textContent = char.level; charXPSpan.textContent = char.xp; charXPNextSpan.textContent = char.xpToNextLevel;
319
+ char.stats.hp = Math.min(char.stats.hp, char.stats.maxHp); charHPSpan.textContent = char.stats.hp; charMaxHPSpan.textContent = char.stats.maxHp;
320
+ for (const stat in statSpans) { if (statSpans.hasOwnProperty(stat) && char.stats.hasOwnProperty(stat)) { statSpans[stat].textContent = char.stats[stat]; } }
321
+ charInventoryList.innerHTML = ''; const maxSlots = 15;
322
+ 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); }
323
+ updateLevelUpAvailability();
324
+ }
325
+
326
+ function calculateStatIncreaseCost() { return (gameState.character.level * 10) + 5; }
327
+
328
+ function updateLevelUpAvailability() {
329
+ const char = gameState.character; const canLevelUp = char.xp >= char.xpToNextLevel; levelUpButton.disabled = !canLevelUp;
330
+ const cost = calculateStatIncreaseCost(); const canIncreaseWithXP = char.xp >= cost; const canIncreaseWithPoints = char.availableStatPoints > 0;
331
+ statIncreaseButtons.forEach(button => { button.disabled = !(canIncreaseWithPoints || canIncreaseWithXP); });
332
+ statIncreaseCostSpan.textContent = cost; statPointsAvailableSpan.textContent = char.availableStatPoints;
333
+ }
334
+
335
+ function handleLevelUp() {
336
+ 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."); }
337
+ }
338
+
339
+ function handleStatIncrease(statName) {
340
+ const char = gameState.character; const cost = calculateStatIncreaseCost();
341
+ 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; }
342
+ 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}.`); }
343
+ }
344
+
345
+ 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.'); } }
346
+
347
+ 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; }
348
+
349
+ 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.'); } }
350
+
351
+ // Event Listeners
352
+ charNameInput.addEventListener('change', () => { gameState.character.name = charNameInput.value.trim() || "Hero"; console.log(`Name changed to: ${gameState.character.name}`); });
353
+ levelUpButton.addEventListener('click', handleLevelUp);
354
+ statIncreaseButtons.forEach(button => { button.addEventListener('click', () => { const statToIncrease = button.dataset.stat; if (statToIncrease) { handleStatIncrease(statToIncrease); } }); });
355
+ saveCharButton.addEventListener('click', saveCharacter);
356
+ exportCharButton.addEventListener('click', exportCharacter);
357
+
358
+
359
+ // --- Game Logic Functions ---
360
+
361
+ function startGame() {
362
+ if (!loadCharacter()) { console.log("No saved character found, starting new."); }
363
+ // Ensure full character structure after load
364
+ 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: [] };
365
+ gameState.character = { ...defaultChar, ...gameState.character };
366
+ gameState.character.stats = { ...defaultChar.stats, ...(gameState.character.stats || {}) };
367
+
368
+ gameState.currentPageId = 1;
369
+ renderCharacterSheet();
370
+ renderPage(gameState.currentPageId);
371
+ }
372
+
373
+ function renderPage(pageId) {
374
+ const page = gameData[pageId];
375
+ 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; }
376
+ storyTitleElement.textContent = page.title || "Untitled Page"; storyContentElement.innerHTML = page.content || "<p>...</p>";
377
+ choicesElement.innerHTML = '';
378
+ if (page.options && page.options.length > 0) {
379
+ page.options.forEach(option => {
380
+ const button = document.createElement('button'); button.classList.add('choice-button'); button.textContent = option.text; let requirementMet = true;
381
+ if (option.requireItem && !gameState.character.inventory.includes(option.requireItem)) { requirementMet = false; button.title = `Requires: ${option.requireItem}`; button.disabled = true; }
382
+ 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); });
383
+ } 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); }
384
+ updateScene(page.illustration || 'default');
385
+ }
386
+
387
+ function handleChoiceClick(choiceData) {
388
+ const nextPageId = parseInt(choiceData.nextPage); const itemToAdd = choiceData.addItem; if (isNaN(nextPageId)) { console.error("Invalid nextPageId:", choiceData.nextPage); return; }
389
+ if (itemToAdd && !gameState.character.inventory.includes(itemToAdd)) { gameState.character.inventory.push(itemToAdd); console.log("Added item:", itemToAdd); }
390
+ gameState.currentPageId = nextPageId; const nextPageData = gameData[nextPageId];
391
+ 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; } }
392
+ 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}`); } }
393
+ if (nextPageData.gameOver) { console.log("Reached Game Over."); renderCharacterSheet(); renderPage(nextPageId); return; }
394
+ } else { console.error(`Data for page ${nextPageId} not found!`); renderCharacterSheet(); renderPage(99); return; }
395
+ renderCharacterSheet(); renderPage(nextPageId);
396
+ }
397
+
398
+ // Scene Update Function
399
+ function updateScene(illustrationKey) {
400
+ console.log(`Updating scene for key: "${illustrationKey}"`);
401
+ if (currentAssemblyGroup) { scene.remove(currentAssemblyGroup); /* TODO: Dispose if needed */ }
402
+ currentAssemblyGroup = null; let assemblyFunction;
403
+ switch (illustrationKey) {
404
+ case 'city-gates': assemblyFunction = createCityGatesAssembly; break;
405
+ case 'weaponsmith': assemblyFunction = createWeaponsmithAssembly; break;
406
+ case 'temple': assemblyFunction = createTempleAssembly; break;
407
+ case 'resistance-meeting': assemblyFunction = createResistanceMeetingAssembly; break;
408
+ case 'shadowwood-forest': assemblyFunction = createForestAssembly; break;
409
+ case 'road-ambush': assemblyFunction = createRoadAmbushAssembly; break;
410
+ case 'forest-edge': assemblyFunction = createForestEdgeAssembly; break;
411
+ case 'prisoner-cell': assemblyFunction = createPrisonerCellAssembly; break;
412
+ case 'game-over': assemblyFunction = createGameOverAssembly; break;
413
+ case 'error': assemblyFunction = createErrorAssembly; break;
414
+ case 'river-spirit': console.warn("Scene 'river-spirit' not implemented."); assemblyFunction = createDefaultAssembly; break;
415
+ case 'ancient-ruins': console.warn("Scene 'ancient-ruins' not implemented."); assemblyFunction = createDefaultAssembly; break;
416
+ case 'fortress-plains': console.warn("Scene 'fortress-plains' not implemented."); assemblyFunction = createDefaultAssembly; break;
417
+ default: console.warn(`Unknown illustration key: "${illustrationKey}". Using default.`); assemblyFunction = createDefaultAssembly; break;
418
+ }
419
+ try { currentAssemblyGroup = assemblyFunction(); } catch (error) { console.error(`Error creating assembly for ${illustrationKey}:`, error); currentAssemblyGroup = createErrorAssembly(); }
420
+ if (currentAssemblyGroup) { scene.add(currentAssemblyGroup); } else { console.error(`Assembly failed for ${illustrationKey}.`); currentAssemblyGroup = createErrorAssembly(); scene.add(currentAssemblyGroup); }
421
+ }
422
+
423
+
424
+ // --- Initialization ---
425
+ initThreeJS(); // Set up the 3D scene first
426
+ startGame(); // Load data, render character sheet, and show first page