File size: 44,930 Bytes
1d353bd
 
 
 
 
edaf0c0
7c0cab7
7f369a4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
edaf0c0
7c0cab7
1d353bd
 
 
7c0cab7
6c1f7e8
7c0cab7
0a69594
7c0cab7
0a69594
 
 
6c1f7e8
 
edaf0c0
0a69594
 
6c1f7e8
0a69594
 
7c0cab7
1d353bd
7c0cab7
 
 
 
 
 
 
 
 
 
 
 
 
0a69594
 
 
 
 
7c0cab7
0a69594
6c1f7e8
0a69594
 
 
 
 
 
 
 
 
 
edaf0c0
 
 
7c0cab7
 
0a69594
7c0cab7
0a69594
edaf0c0
0a69594
edaf0c0
7c0cab7
0a69594
edaf0c0
0a69594
edaf0c0
0a69594
edaf0c0
7f369a4
 
 
0a69594
 
7c0cab7
 
6c1f7e8
7c0cab7
 
 
 
 
7f369a4
 
7c0cab7
 
 
0a69594
7f369a4
7c0cab7
 
0a69594
 
7f369a4
6c1f7e8
0a69594
 
 
 
 
 
6c1f7e8
 
0a69594
 
 
edaf0c0
6c1f7e8
edaf0c0
 
 
 
 
 
 
 
7f369a4
edaf0c0
 
 
6c1f7e8
 
0a69594
 
7f369a4
edaf0c0
 
7f369a4
 
 
edaf0c0
 
7f369a4
edaf0c0
 
 
 
 
 
6c1f7e8
 
 
 
edaf0c0
6c1f7e8
edaf0c0
 
7f369a4
 
edaf0c0
 
7f369a4
edaf0c0
 
7f369a4
edaf0c0
 
 
7f369a4
edaf0c0
 
 
7f369a4
 
 
 
 
edaf0c0
7f369a4
edaf0c0
6c1f7e8
 
0a69594
 
 
 
6c1f7e8
 
 
edaf0c0
6c1f7e8
 
 
0a69594
 
 
 
7f369a4
 
edaf0c0
 
0a69594
 
 
7f369a4
 
edaf0c0
7f369a4
 
edaf0c0
 
 
 
 
 
 
7f369a4
 
 
 
 
 
edaf0c0
7f369a4
 
6c1f7e8
 
 
 
 
7f369a4
 
edaf0c0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6c1f7e8
edaf0c0
6c1f7e8
 
 
 
 
 
7f369a4
6c1f7e8
7f369a4
edaf0c0
7f369a4
 
edaf0c0
 
7f369a4
edaf0c0
 
7f369a4
 
6c1f7e8
 
 
 
edaf0c0
6c1f7e8
edaf0c0
6c1f7e8
 
edaf0c0
6c1f7e8
 
 
 
 
 
 
edaf0c0
6c1f7e8
 
7f369a4
edaf0c0
 
6c1f7e8
7f369a4
 
 
edaf0c0
 
 
 
7f369a4
 
 
 
edaf0c0
7f369a4
edaf0c0
7f369a4
edaf0c0
 
 
7f369a4
edaf0c0
 
 
 
6c1f7e8
7f369a4
 
6c1f7e8
edaf0c0
6c1f7e8
edaf0c0
6c1f7e8
 
 
7f369a4
6c1f7e8
 
0a69594
 
6c1f7e8
 
0a69594
6c1f7e8
7f369a4
6c1f7e8
edaf0c0
0a69594
 
6c1f7e8
0a69594
edaf0c0
6c1f7e8
0a69594
edaf0c0
6c1f7e8
0a69594
edaf0c0
0a69594
 
 
 
 
edaf0c0
 
 
0a69594
7f369a4
0a69594
edaf0c0
 
0a69594
edaf0c0
0a69594
 
7c0cab7
6c1f7e8
7c0cab7
 
 
1d353bd
 
 
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
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Choose Your Own Procedural Adventure (Stable Base)</title>
    <style>
        body{font-family:'Courier New',monospace;background-color:#222;color:#eee;margin:0;padding:0;overflow:hidden;display:flex;flex-direction:column;height:100vh}
        #game-container{display:flex;flex-grow:1;overflow:hidden}
        #scene-container{flex-grow:3;position:relative;border-right:2px solid #555;min-width:200px;background-color:#1a1a1a;height:100%;box-sizing:border-box}
        #ui-container{flex-grow:2;padding:20px;overflow-y:auto;background-color:#333;min-width:280px;height:100%;box-sizing:border-box;display:flex;flex-direction:column}
        #scene-container canvas{display:block}
        #story-title{color:#ffcc66;margin-top:0;margin-bottom:15px;border-bottom:1px solid #555;padding-bottom:10px;font-size:1.4em}
        #story-content{margin-bottom:20px;line-height:1.6;flex-grow:1}
        #story-content p{margin-bottom:1em}
        #story-content p:last-child{margin-bottom:0}
        #stats-inventory-container{margin-bottom:20px;padding-bottom:15px;border-bottom:1px solid #555;font-size:0.9em}
        #stats-display, #inventory-display{margin-bottom:10px;line-height:1.8}
        #stats-display span, #inventory-display span{display:inline-block;background-color:#444;padding:3px 8px;border-radius:15px;margin-right:8px;margin-bottom:5px;border:1px solid #666;white-space:nowrap}
        #stats-display strong, #inventory-display strong{color:#aaa;margin-right:5px}
        #inventory-display em{color:#888;font-style:normal}
        #inventory-display .item-quest{background-color:#666030;border-color:#999048}
        #inventory-display .item-weapon{background-color:#663030;border-color:#994848}
        #inventory-display .item-armor{background-color:#306630;border-color:#489948}
        #inventory-display .item-spell{background-color:#303066;border-color:#484899}
        #inventory-display .item-unknown{background-color:#555;border-color:#777}
        #choices-container{margin-top:auto;padding-top:15px;border-top:1px solid #555}
        #choices-container h3{margin-top:0;margin-bottom:10px;color:#aaa}
        #choices{display:flex;flex-direction:column;gap:10px}
        .choice-button{display:block;width:100%;padding:10px 12px;margin-bottom:0;background-color:#555;color:#eee;border:1px solid #777;border-radius:5px;cursor:pointer;text-align:left;font-family:'Courier New',monospace;font-size:1em;transition:background-color 0.2s,border-color 0.2s;box-sizing:border-box}
        .choice-button:hover:not(:disabled){background-color:#d4a017;color:#222;border-color:#b8860b}
        .choice-button:disabled{background-color:#444;color:#888;cursor:not-allowed;border-color:#666;opacity:0.7}
        .roll-success{color:#7f7;border-left:3px solid #4a4;padding-left:8px;margin-bottom:1em;font-size:0.9em}
        .roll-failure{color:#f77;border-left:3px solid #a44;padding-left:8px;margin-bottom:1em;font-size:0.9em}
        .xp-gain{color:#7af;font-style:italic;font-size:0.9em;display:block;margin-top:0.5em;} /* Style for XP messages */
    </style>
</head>
<body>

    <div id="game-container">
        <div id="scene-container"></div>
        <div id="ui-container">
            <h2 id="story-title">Loading Adventure...</h2>
            <div id="story-content">
                <p>Please wait while the adventure loads.</p>
            </div>
            <div id="stats-inventory-container">
                 <div id="stats-display"></div>
                 <div id="inventory-display"></div>
            </div>
            <div id="choices-container">
                 <h3>What will you do?</h3>
                 <div id="choices"></div>
            </div>
        </div>
    </div>

    <script type="importmap">
        {
            "imports": {
                "three": "https://unpkg.com/[email protected]/build/three.module.js",
                "three/addons/": "https://unpkg.com/[email protected]/examples/jsm/"
            }
        }
    </script>

    <script type="module">
        import * as THREE from 'three';

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

        let scene, camera, renderer;
        let currentAssemblyGroup = null;

        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 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 templeMaterial = new THREE.MeshStandardMaterial({ color: 0xA99B78, roughness: 0.7, metalness: 0.1 });
        const errorMaterial = new THREE.MeshStandardMaterial({ color: 0xffa500, roughness: 0.5 });
        const gameOverMaterial = new THREE.MeshStandardMaterial({ color: 0xff0000, roughness: 0.5 });
        // Added materials for forest variation from previous step, keep them
        const pineLeafMaterial = new THREE.MeshStandardMaterial({ color: 0x1A5A2A, roughness: 0.7, metalness: 0 });
        const gnarledWoodMaterial = new THREE.MeshStandardMaterial({ color: 0x6B4F3A, roughness: 0.85, metalness: 0 });

        function initThreeJS() {
            if (!sceneContainer) { console.error("Scene container not found!"); return; }
            scene = new THREE.Scene();
            scene.background = new THREE.Color(0x222222);
            const width = sceneContainer.clientWidth; const height = sceneContainer.clientHeight;
            camera = new THREE.PerspectiveCamera(75, (width / height) || 1, 0.1, 1000);
            camera.position.set(0, 2.5, 7); camera.lookAt(0, 0.5, 0);
            renderer = new THREE.WebGLRenderer({ antialias: true });
            renderer.setSize(width || 400, height || 300);
            renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFSoftShadowMap;
            sceneContainer.appendChild(renderer.domElement);
            const ambientLight = new THREE.AmbientLight(0xffffff, 0.5); scene.add(ambientLight);
            const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2);
            directionalLight.position.set(8, 15, 10); directionalLight.castShadow = true;
            directionalLight.shadow.mapSize.width = 1024; directionalLight.shadow.mapSize.height = 1024;
            directionalLight.shadow.camera.near = 0.5; directionalLight.shadow.camera.far = 50;
            const shadowCamSize = 15;
            directionalLight.shadow.camera.left = -shadowCamSize; directionalLight.shadow.camera.right = shadowCamSize;
            directionalLight.shadow.camera.top = shadowCamSize; directionalLight.shadow.camera.bottom = -shadowCamSize;
            scene.add(directionalLight);
            window.addEventListener('resize', onWindowResize, false);
            setTimeout(onWindowResize, 100);
            animate();
        }

        function onWindowResize() {
            if (!renderer || !camera || !sceneContainer) return;
            const width = sceneContainer.clientWidth; const height = sceneContainer.clientHeight;
            if (width > 0 && height > 0) { camera.aspect = width / height; camera.updateProjectionMatrix(); renderer.setSize(width, height); }
        }

        function animate() {
            requestAnimationFrame(animate);
            if (renderer && scene && camera) { renderer.render(scene, camera); }
        }

        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;
        }

        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;
        }

        // --- Procedural Generation Functions (Using simpler, non-parameterized versions first) ---
        function createDefaultAssembly() { 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() { const group = new THREE.Group(); const gh=4, gw=1.5, gd=0.8, ah=1, aw=3; const tlGeo = new THREE.BoxGeometry(gw, gh, gd); group.add(createMesh(tlGeo, stoneMaterial, { x:-(aw/2+gw/2), y:gh/2, z:0 })); const trGeo = new THREE.BoxGeometry(gw, gh, gd); group.add(createMesh(trGeo, stoneMaterial, { x:(aw/2+gw/2), y:gh/2, z:0 })); const aGeo = new THREE.BoxGeometry(aw, ah, gd); group.add(createMesh(aGeo, stoneMaterial, { x:0, y:gh-ah/2, z:0 })); const cs=0.4; const cg = new THREE.BoxGeometry(cs, cs, gd*1.1); for(let i=-1; i<=1; i+=2){ group.add(createMesh(cg.clone(), stoneMaterial, { x:-(aw/2+gw/2)+i*cs*0.7, y:gh+cs/2, z:0 })); group.add(createMesh(cg.clone(), stoneMaterial, { x:(aw/2+gw/2)+i*cs*0.7, y:gh+cs/2, z:0 })); } group.add(createMesh(cg.clone(), stoneMaterial, { x:0, y:gh+ah-cs/2, z:0 })); group.add(createGroundPlane(stoneMaterial)); return group; }
        function createWeaponsmithAssembly() { const group = new THREE.Group(); const bw=3, bh=2.5, bd=3.5; const bGeo = new THREE.BoxGeometry(bw, bh, bd); group.add(createMesh(bGeo, darkWoodMaterial, { x:0, y:bh/2, z:0 })); const ch=3.5; const cGeo = new THREE.CylinderGeometry(0.3, 0.4, ch, 8); group.add(createMesh(cGeo, stoneMaterial, { x:bw*0.3, y:ch/2, z:-bd*0.3 })); group.add(createGroundPlane()); return group; }
        function createTempleAssembly() { const group = new THREE.Group(); const bs=5, bsh=0.5, ch=3, cr=0.25, rh=0.5; const bGeo = new THREE.BoxGeometry(bs, bsh, bs); group.add(createMesh(bGeo, templeMaterial, { x:0, y:bsh/2, z:0 })); const cGeo = new THREE.CylinderGeometry(cr, cr, ch, 12); const cPos = [{x:-bs/3, z:-bs/3}, {x:bs/3, z:-bs/3}, {x:-bs/3, z:bs/3}, {x:bs/3, z:bs/3}]; cPos.forEach(p=>group.add(createMesh(cGeo.clone(), templeMaterial, { x:p.x, y:bsh+ch/2, z:p.z }))); const rGeo = new THREE.BoxGeometry(bs*0.9, rh, bs*0.9); group.add(createMesh(rGeo, templeMaterial, { x:0, y:bsh+ch+rh/2, z:0 })); group.add(createGroundPlane()); return group; }
        function createResistanceMeetingAssembly() { const group = new THREE.Group(); const tw=2, th=0.8, td=1, tt=0.1; const ttg = new THREE.BoxGeometry(tw, tt, td); group.add(createMesh(ttg, woodMaterial, { x:0, y:th-tt/2, z:0 })); const lh=th-tt, ls=0.1; const lg=new THREE.BoxGeometry(ls, lh, ls); const lofW=tw/2-ls*1.5; const lofD=td/2-ls*1.5; group.add(createMesh(lg, woodMaterial, { x:-lofW, y:lh/2, z:-lofD })); group.add(createMesh(lg.clone(), woodMaterial, { x:lofW, y:lh/2, z:-lofD })); group.add(createMesh(lg.clone(), woodMaterial, { x:-lofW, y:lh/2, z:lofD })); group.add(createMesh(lg.clone(), woodMaterial, { x:lofW, y:lh/2, z:lofD })); const ss=0.4; const sg=new THREE.BoxGeometry(ss, ss*0.8, ss); group.add(createMesh(sg, darkWoodMaterial, { x:-tw*0.6, y:ss*0.4, z:0 })); group.add(createMesh(sg.clone(), darkWoodMaterial, { x:tw*0.6, y:ss*0.4, z:0 })); group.add(createGroundPlane(stoneMaterial)); return group; }
        function createForestAssembly(params = {}) { // Kept params for potential future use, but using defaults now
            const group = new THREE.Group(); const tc=10, a=10; // Use fixed values
            const cT=(x,z)=>{ const tg=new THREE.Group(); const th=Math.random()*1.5+2; const tr=Math.random()*0.1+0.1; const tGeo=new THREE.CylinderGeometry(tr*0.7, tr, th, 8); tg.add(createMesh(tGeo, woodMaterial, {x:0, y:th/2, z:0})); const fr=th*0.4+0.2; const fGeo=new THREE.SphereGeometry(fr, 8, 6); tg.add(createMesh(fGeo, leafMaterial, {x:0, y:th*0.9, z:0})); tg.position.set(x,0,z); return tg; };
            for(let i=0; i<tc; i++){ const x=(Math.random()-0.5)*a; const z=(Math.random()-0.5)*a; if(Math.sqrt(x*x+z*z)>1.0) group.add(cT(x,z)); } group.add(createGroundPlane(groundMaterial, a*1.1)); return group;
        }
        function createRoadAmbushAssembly() { const group = new THREE.Group(); const a=12; const fg = createForestAssembly(); group.add(fg); const rw=3, rl=a*1.2; const rGeo=new THREE.PlaneGeometry(rw, rl); const rMat=new THREE.MeshStandardMaterial({color:0x966F33, roughness:0.9}); const r=createMesh(rGeo, rMat, {x:0, y:0.01, z:0}, {x:-Math.PI/2}); r.receiveShadow=true; group.add(r); const rkGeo=new THREE.SphereGeometry(0.5, 5, 4); const rkMat=new THREE.MeshStandardMaterial({color:0x666666, roughness:0.8}); group.add(createMesh(rkGeo, rkMat, {x:rw*0.7, y:0.25, z:1}, {y:Math.random()*Math.PI})); group.add(createMesh(rkGeo.clone().scale(0.8,0.8,0.8), rkMat, {x:-rw*0.8, y:0.2, z:-2}, {y:Math.random()*Math.PI})); return group; }
        function createForestEdgeAssembly() { const group = new THREE.Group(); const a=15; const fg = createForestAssembly(); const ttr=[]; fg.children.forEach(c => { if(c.type === 'Group' && c.position.x > 0) ttr.push(c); }); ttr.forEach(t => fg.remove(t)); group.add(fg); return group; }
        function createPrisonerCellAssembly() { const group = new THREE.Group(); const cs=3, wh=2.5, wt=0.2, br=0.05, bsp=0.25; const cfMat=stoneMaterial.clone(); cfMat.color.setHex(0x555555); group.add(createGroundPlane(cfMat, cs)); const wbGeo=new THREE.BoxGeometry(cs, wh, wt); group.add(createMesh(wbGeo, stoneMaterial, {x:0, y:wh/2, z:-cs/2})); const wsGeo=new THREE.BoxGeometry(wt, wh, cs); group.add(createMesh(wsGeo, stoneMaterial, {x:-cs/2, y:wh/2, z:0})); group.add(createMesh(wsGeo.clone(), stoneMaterial, {x:cs/2, y:wh/2, z:0})); const bGeo=new THREE.CylinderGeometry(br, br, wh, 8); const nb=Math.floor(cs/bsp); for(let i=0; i<nb; i++){ const xp=-cs/2+(i+0.5)*bsp; group.add(createMesh(bGeo.clone(), metalMaterial, {x:xp, y:wh/2, z:cs/2})); } return group; }
        function createGameOverAssembly() { 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() { 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; }

        // --- 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"}, "Crude Dagger":{type:"weapon", description:"A roughly made dagger."}, "Scout's Pouch":{type:"quest", description:"Contains odds and ends."}, "Healing Poultice":{type:"spell", description:"Soothes wounds, heals 5 HP."} };
        const gameData = { // Using the expanded game data with ~20 pages and checks
            "1": { title: "The Crossroads", content: `<p>Dust swirls... Which path calls to you?</p>`, options: [ { text: "Enter the Shadowwood Forest (North)", next: 5 }, { text: "Head towards the Rolling Hills (East)", next: 2 }, { text: "Investigate the Coastal Cliffs (West)", next: 3 } ], illustration: "crossroads-signpost-sunny" },
            "2": { title: "Rolling Hills", content: `<p>Verdant hills stretch before you...</p>`, options: [ { text: "Follow the narrow path", next: 4 }, { text: "Head back to the crossroads", next: 1 } ], illustration: "rolling-green-hills-shepherd-distance" },
            "3": { title: "Coastal Cliffs Edge", content: `<p>You stand atop windswept cliffs...</p>`, options: [ { text: "Attempt the precarious descent (Dexterity Check)", check: { stat: 'dexterity', dc: 12, onFailure: 31 }, next: 30 }, { text: "Scan the cliff face for easier routes (Wisdom Check)", check: { stat: 'wisdom', dc: 11, onFailure: 32 }, next: 33 }, { text: "Return to the crossroads", next: 1 } ], illustration: "windy-sea-cliffs-crashing-waves-path-down" },
            "4": { title: "Hill Path Overlook", content: `<p>The path crests a hill... you see a small, overgrown shrine...</p>`, options: [ { text: "Investigate the overgrown shrine", next: 40 }, { text: "Continue towards the badlands", next: 41 }, { text: "Go back", next: 2 } ], illustration: "hilltop-view-overgrown-shrine-wildflowers" },
            "5": { title: "Shadowwood Entrance", content: `<p>Sunlight struggles to pierce the dense canopy... How do you proceed?</p>`, options: [ { text: "Follow the main, albeit overgrown, path", next: 6 }, { text: "Try to navigate through the lighter undergrowth", next: 7 }, { text: "Look for animal trails or signs of passage (Wisdom Check)", check: { stat: 'wisdom', dc: 10, onFailure: 6 }, next: 8 } ], illustration: "dark-forest-entrance-gnarled-roots-filtered-light" },
            "6": { title: "Overgrown Forest Path", content: `<p>The path is barely visible... You hear a twig snap nearby!</p>`, options: [ { text: "Ready your weapon and investigate", next: 10 }, { text: "Attempt to hide quietly (Dexterity Check)", check: { stat: 'dexterity', dc: 11, onFailure: 10 }, next: 11 }, { text: "Call out cautiously", next: 10 } ], illustration: "overgrown-forest-path-glowing-fungi-vines" },
            "7": { title: "Tangled Undergrowth", content: `<p>Pushing through ferns... You stumble upon a small clearing containing a moss-covered, weathered stone statue...</p>`, options: [ { text: "Examine the statue closely (Intelligence Check)", check: { stat: 'intelligence', dc: 13, onFailure: 71 }, next: 70 }, { text: "Ignore the statue and press on", next: 72 }, { text: "Leave a small offering (if possible)", next: 73 } ], illustration: "forest-clearing-mossy-statue-weathered-stone" },
            "8": { title: "Hidden Game Trail", content: `<p>Your sharp eyes spot a faint trail... It leads towards a ravine spanned by a rickety rope bridge.</p><p class="xp-gain">(+20 XP)</p>`, options: [ { text: "Risk crossing the rope bridge (Dexterity Check)", check: { stat: 'dexterity', dc: 10, onFailure: 81 }, next: 80 }, { text: "Search for another way across the ravine", next: 82 } ], illustration: "narrow-game-trail-forest-rope-bridge-ravine", reward: { xp: 20 } },
            "10": { title: "Goblin Ambush!", content: `<p>Two scraggly goblins leap out, brandishing crude spears!</p>`, options: [ { text: "Fight the goblins!", next: 12 }, { text: "Attempt to dodge past them (Dexterity Check)", check: { stat: 'dexterity', dc: 13, onFailure: 101 }, next: 13 } ], illustration: "two-goblins-ambush-forest-path-spears" },
            "11": { title: "Hidden Evasion", content: `<p>You melt into the shadows as the goblins blunder past.</p><p class="xp-gain">(+30 XP)</p>`, options: [ { text: "Continue cautiously", next: 14 } ], illustration: "forest-shadows-hiding-goblins-walking-past", reward: { xp: 30 } },
            "12": { title: "Ambush Victory!", content: `<p>You defeat the goblins! Found a Crude Dagger.</p><p class="xp-gain">(+50 XP)</p>`, options: [ { text: "Press onward", next: 14 } ], illustration: "defeated-goblins-forest-path-loot", reward: { xp: 50, addItem: "Crude Dagger" } },
            "13": { title: "Daring Escape", content: `<p>With surprising agility, you tumble past the goblins!</p><p class="xp-gain">(+25 XP)</p>`, options: [ { text: "Keep running!", next: 14 } ], illustration: "blurred-motion-running-past-goblins-forest", reward: { xp: 25 } },
            "14": { title: "Forest Stream Crossing", content: `<p>The path leads to a clear, shallow stream...</p>`, options: [ { text: "Wade across the stream", next: 16 }, { text: "Look for a drier crossing point (fallen log?)", next: 15 } ], illustration: "forest-stream-crossing-dappled-sunlight-stones"},
            "15": { title: "Log Bridge", content: `<p>Further upstream, a large, mossy log spans the stream.</p>`, options: [ { text: "Cross carefully on the log (Dexterity Check)", check: { stat: 'dexterity', dc: 9, onFailure: 151 }, next: 16 }, { text: "Go back and wade instead", next: 14 } ], illustration: "mossy-log-bridge-over-forest-stream" },
            "151": { title: "Splash!", content: `<p>You slip on the mossy log and tumble into the cold stream! You're soaked but unharmed.</p>`, options: [ { text: "Shake yourself off and continue", next: 16 } ], illustration: "character-splashing-into-stream-from-log" },
            "16": { title: "Edge of the Woods", content: `<p>You emerge from the Shadowwood... Before you lie rocky foothills...</p>`, options: [ { text: "Begin the ascent into the foothills", next: 17 }, { text: "Scan the fortress from afar (Wisdom Check)", check: { stat: 'wisdom', dc: 14, onFailure: 17 }, next: 18 } ], illustration: "forest-edge-view-rocky-foothills-distant-mountain-fortress" },
            "17": { title: "Rocky Foothills Path", content: `<p>The climb is arduous... The fortress looms larger now.</p>`, options: [ { text: "Continue the direct ascent", next: 19 }, { text: "Look for signs of a hidden trail (Wisdom Check)", check: { stat: 'wisdom', dc: 15, onFailure: 19 }, next: 20 } ], illustration: "climbing-rocky-foothills-path-fortress-closer" },
            "18": { title: "Distant Observation", content: `<p>You notice what might be a less-guarded approach along the western ridge...</p><p class="xp-gain">(+30 XP)</p>`, options: [ { text: "Take the main path into the foothills", next: 17 }, { text: "Attempt the western ridge approach", next: 21 } ], illustration: "zoomed-view-mountain-fortress-western-ridge", reward: { xp: 30 } },
            "19": { title: "Blocked Pass", content: `<p>The main path is blocked by a recent rockslide!</p>`, options: [ { text: "Try to climb over (Strength Check)", check: { stat: 'strength', dc: 14, onFailure: 191 }, next: 190 }, { text: "Search for another way around", next: 192 } ], illustration: "rockslide-blocking-mountain-path-boulders" },
            "20": { title: "Goat Trail", content: `<p>You discover a narrow trail barely wide enough for a mountain goat...</p><p class="xp-gain">(+40 XP)</p>`, options: [ { text: "Follow the precarious goat trail", next: 22 } ], illustration: "narrow-goat-trail-mountainside-fortress-view", reward: { xp: 40 } },
            "30": { title: "Hidden Cove", content: `<p>Your careful descent brings you to a secluded cove. A dark cave entrance is visible...</p><p class="xp-gain">(+25 XP)</p>`, options: [ { text: "Explore the dark cave", next: 35 } ], illustration: "hidden-cove-beach-dark-cave-entrance", reward: { xp: 25 } },
            "31": { title: "Tumbled Down", content: `<p>You lose your footing... landing hard... You lose 5 HP. A dark cave entrance beckons.</p>`, options: [ { text: "Gingerly explore the dark cave", next: 35 } ], illustration: "character-fallen-at-bottom-of-cliff-path-cove", hpLoss: 5 },
            "32": { title: "No Easier Path", content: `<p>You scan the cliffs intently but find no obviously easier routes.</p>`, options: [ { text: "Attempt the precarious descent (Dexterity Check)", check: { stat: 'dexterity', dc: 12, onFailure: 31 }, next: 30 }, { text: "Return to crossroads", next: 1} ], illustration: "scanning-sea-cliffs-no-other-paths-visible" },
            "33": { title: "Smuggler's Steps?", content: `<p>Your keen eyes spot a series of barely visible handholds and steps carved into the rock...</p><p class="xp-gain">(+15 XP)</p>`, options: [ { text: "Use the hidden steps (Easier Dex Check)", check: { stat: 'dexterity', dc: 8, onFailure: 31 }, next: 30 } ], illustration: "close-up-handholds-carved-in-cliff-face", reward: { xp: 15 } },
            "35": { title: "Dark Cave", content: `<p>The cave smells of salt and decay...</p>`, options: [{ text: "Press deeper into the darkness (End)", next: 99 } ], illustration: "dark-cave-entrance-dripping-water" },
            "40": { title: "Overgrown Shrine", content: `<p>Wildflowers grow thick around a small stone shrine...</p>`, options: [{ text: "Examine the carvings (Intelligence Check)", check:{stat:'intelligence', dc:11, onFailure: 401}, next: 400 }, { text: "Leave it be", next: 4 } ], illustration: "overgrown-stone-shrine-wildflowers-close" },
            "41": { title: "Rocky Badlands", content: `<p>The green hills give way to cracked earth...</p>`, options: [{ text: "Scout ahead (End)", next: 99 } ], illustration: "rocky-badlands-cracked-earth-harsh-sun" },
            "70": { title: "Statue Examined", content: `<p>The statue depicts a forgotten nature deity. You notice a small compartment at its base.</p><p class="xp-gain">(+10 XP)</p>`, options: [{text:"Try to open compartment (Strength Check?)", check:{stat:'strength', dc: 10, onFailure: 71}, next: 700}], illustration:"close-up-mossy-statue-compartment", reward:{xp:10}},
            "71": { title: "Statue Unyielding", content: `<p>You examine the statue but learn little. It remains an imposing enigma.</p>`, options: [{text:"Press onward", next: 72}], illustration:"forest-clearing-mossy-statue-weathered-stone-shrug"},
            "72": { title: "Deeper Woods", content: `<p>Leaving the statue behind, you push further into the increasingly dense woods.</p>`, options: [{text:"Continue", next: 14}], illustration:"dense-forest-undergrowth-shadows" },
            "73": { title: "Offering Made", content: `<p>You leave a small token. For a moment, you think you feel a sense of ancient approval.</p>`, options: [{text:"Press onward", next: 72}], illustration:"offering-at-base-of-mossy-statue"},
            "80": { title: "Bridge Crossed", content: `<p>You make it across the swaying bridge, your heart pounding.</p><p class="xp-gain">(+15 XP)</p>`, options: [{text:"Continue on the trail", next: 16}], illustration:"view-from-end-of-rope-bridge-forest", reward:{xp:15}},
            "81": { title: "Bridge Collapse!", content: `<p>A frayed rope snaps! You plummet into the shallow ravine below, losing 8 HP!</p>`, options: [{text:"Climb out and find another way", next: 82}], illustration:"character-falling-from-broken-rope-bridge", hpLoss:8},
            "82": { title: "Ravine Detour", content: `<p>You find a place where the ravine narrows and manage to climb down and back up the other side.</p><p class="xp-gain">(+5 XP)</p>`, options: [{text:"Continue on the trail", next: 16}], illustration:"climbing-out-of-shallow-ravine-forest", reward:{xp:5}},
            "101": { title:"Failed Dodge", content:"<p>You try to dodge, but a goblin spear trips you! You take 3 damage.</p>", options:[{text:"Get up and Fight!", next: 12}], illustration:"character-tripped-by-goblin-spear", hpLoss: 3},
            "190": { title: "Over the Rocks", content:"<p>With considerable effort, you clamber over the rockslide.</p><p class="xp-gain">(+35 XP)</p>`, options: [{text:"Continue up the path", next: 22}], illustration:"character-climbing-over-boulders", reward: {xp:35} },
            "191": { title: "Climb Fails", content:"<p>The boulders are too unstable. You cannot climb them safely.</p>`, options: [{text:"Search for another way around", next: 192}], illustration:"character-slipping-on-rockslide-boulders"},
            "192": { title: "Detour Found", content:"<p>After some searching, you find a rough path leading around the rockslide.</p>`, options: [{text:"Continue up the path", next: 22}], illustration:"rough-detour-path-around-rockslide"},
            "21": { title: "Western Ridge", content:"<p>The ridge path is narrow and exposed, with strong winds...</p>`, options: [{text:"Proceed carefully (Dexterity Check)", check:{stat:'dexterity', dc: 14, onFailure: 211}, next: 22 } ], illustration:"narrow-windy-mountain-ridge-path" },
            "22": { title: "Fortress Approach", content:"<p>You've navigated the treacherous paths and now stand near the outer walls...</p>`, options: [{text:"Look for an unguarded entrance (End of Demo)", next: 99}], illustration:"approaching-dark-fortress-walls-guards"},
            "211": {title:"Lost Balance", content:"<p>A strong gust sends you tumbling down a steep slope! (-10 HP)</p>", options:[{text:"Climb back up and find another way", next: 17}], illustration:"character-falling-off-windy-ridge", hpLoss: 10},
            "400": {title:"Shrine Secrets", content:"<p>The carvings depict ancient rituals. You find a loose stone revealing a Healing Poultice!</p><p class="xp-gain">(+20 XP)</p>", options:[{text:"Take the poultice and leave", next:4, addItem:"Healing Poultice"}], illustration:"close-up-shrine-carvings-hidden-compartment", reward:{xp:20}},
            "401": {title:"Mysterious Carvings", content:"<p>The carvings are worn and indecipherable.</p>", options:[{text:"Leave the shrine", next:4}], illustration:"worn-stone-carvings-shrine"},
            "700": {title:"Statue's Gift", content:"<p>The compartment clicks open, revealing a smooth, grey stone that feels strangely warm.</p><p class="xp-gain">(+30 XP)</p>", options:[{text:"Take the stone and press on", next: 72, addItem:"Warm Stone"}], illustration:"hand-holding-warm-grey-stone-statue-base", reward:{xp:30}},

            "99": { title: "Game Over / To Be Continued...", content: "<p>Your adventure ends here (for now).</p>", options: [{ text: "Restart", next: 1 }], illustration: "game-over-generic", gameOver: true }
        };

        // --- Game State ---
        let gameState = {
            currentPageId: 1,
            character: {
                name: "Hero", race: "Human", alignment: "Neutral Good", class: "Adventurer",
                level: 1, xp: 0, xpToNextLevel: 100, availableStatPoints: 0, // Stat points for leveling
                stats: { strength: 8, intelligence: 10, wisdom: 10, dexterity: 10, constitution: 10, charisma: 8, hp: 12, maxHp: 12 },
                inventory: []
            }
        };

        // --- Game Logic Functions ---
        function startGame() {
            const defaultChar = { name: "Hero", race: "Human", alignment: "Neutral Good", class: "Adventurer", level: 1, xp: 0, xpToNextLevel: 100, availableStatPoints: 0, stats: { strength: 8, intelligence: 10, wisdom: 10, dexterity: 10, constitution: 10, charisma: 8, hp: 12, maxHp: 12 }, inventory: [] };
            gameState = { currentPageId: 1, character: { ...defaultChar } };
            recalculateMaxHp();
            gameState.character.stats.hp = gameState.character.stats.maxHp;
            renderPage(gameState.currentPageId);
        }

        function levelUpCharacter() {
            const char = gameState.character;
            if (char.xp < char.xpToNextLevel) return false;
            char.level++;
            char.xp -= char.xpToNextLevel;
            char.xpToNextLevel = Math.floor(char.xpToNextLevel * 1.5 + 50);
            char.availableStatPoints += 1; // Gain 1 point
            recalculateMaxHp();
            char.stats.hp = char.stats.maxHp; // Full heal
            console.log(`LEVEL UP! Reached Level ${char.level}.`);
            // Later, add UI to spend points. For now, they just accumulate.
            updateStatsDisplay(); // Update UI to show new level/XP/HP/Points
            return true;
        }

        function recalculateMaxHp() {
             const char = gameState.character;
             const conModifier = Math.floor((char.stats.constitution - 10) / 2);
             char.stats.maxHp = 8 + (char.level * Math.max(1, 2 + conModifier)); // Recalculate Max HP
        }

        function handleChoiceClick(choiceData) {
            const optionNextPageId = parseInt(choiceData.nextPage);
            const itemToAdd = choiceData.addItem;
            let nextPageId = optionNextPageId;
            let rollResultMessage = "";
            let gainedXpThisTurn = 0;
            const check = choiceData.check;
            const effect = choiceData.effect;

             if (isNaN(optionNextPageId) && !check && !effect) { console.error("Invalid choice data:", choiceData); return; }

             // --- Apply direct effects (like resting) ---
             if (effect) {
                 if (effect.hpGain) {
                     const maxHeal = gameState.character.stats.maxHp - gameState.character.stats.hp;
                     const actualGain = Math.min(effect.hpGain, maxHeal);
                     if (actualGain > 0) {
                          gameState.character.stats.hp += actualGain;
                          rollResultMessage += `<p class="xp-gain">Rested and recovered ${actualGain} HP.</p>`;
                          console.log(`Recovered ${actualGain} HP.`);
                     } else {
                          rollResultMessage += `<p class="xp-gain">You rest, but gain no HP.</p>`;
                     }
                 }
                 // If effect determines next page, override default
                 if (effect.setNextPage !== undefined) nextPageId = effect.setNextPage;
                 // For simple rest, we still want to proceed, so no return here.
             }

            // --- Process Stat Check ---
            if (check) {
                const statValue = gameState.character.stats[check.stat] || 10;
                const modifier = Math.floor((statValue - 10) / 2);
                const roll = Math.floor(Math.random() * 20) + 1;
                const totalResult = roll + modifier;
                const dc = check.dc;
                const successMargin = totalResult - dc;
                console.log(`Check: ${check.stat} (DC ${dc}) | Roll: ${roll} + Mod: ${modifier} = ${totalResult}`);

                if (totalResult >= dc) { // Success
                    nextPageId = optionNextPageId;
                    rollResultMessage += `<p class="roll-success"><em>Check Success! (${totalResult} vs DC ${dc})</em></p>`;
                    const marginBonus = Math.max(0, Math.floor(successMargin * 1.0)); // XP per point over DC
                    const oddsBonus = Math.max(0, Math.floor(dc * 0.5)); // XP for difficulty
                    const checkBonusXp = marginBonus + oddsBonus;
                    if (checkBonusXp > 0) { gainedXpThisTurn += checkBonusXp; rollResultMessage += `<p class="xp-gain">+${checkBonusXp} bonus XP!</p>`; console.log(`Check bonus XP: ${checkBonusXp}`); }
                } else { // Failure
                    nextPageId = parseInt(check.onFailure);
                    rollResultMessage += `<p class="roll-failure"><em>Check Failed! (${totalResult} vs DC ${dc})</em></p>`;
                    if (isNaN(nextPageId)) { console.error("Invalid onFailure ID:", check.onFailure); nextPageId = 99; }
                }
            }

            // --- Add immediate item from option ---
            if (itemToAdd && !gameState.character.inventory.includes(itemToAdd)) {
                gameState.character.inventory.push(itemToAdd); console.log("Added item:", itemToAdd);
            }

            // --- Move to Next Page & Process Landing ---
            gameState.currentPageId = nextPageId;
            const nextPageData = gameData[nextPageId];

            if (nextPageData) {
                if (nextPageData.hpLoss) {
                    gameState.character.stats.hp -= nextPageData.hpLoss;
                    console.log(`Lost ${nextPageData.hpLoss} HP.`);
                    // HP check is done after potential level up healing
                }
                if (nextPageData.reward) {
                    if (nextPageData.reward.xp) { gainedXpThisTurn += nextPageData.reward.xp; console.log(`Base reward: +${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}.`); recalculateMaxHp(); } } // Recalc HP if CON changes
                    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 (gainedXpThisTurn > 0) {
                    gameState.character.xp += gainedXpThisTurn;
                    console.log(`Total XP Gained: ${gainedXpThisTurn}. Current XP: ${gameState.character.xp}`);
                    if (!rollResultMessage.includes("bonus XP")) { // Avoid double message if check bonus already added
                        rollResultMessage += `<p class="xp-gain">You gained ${gainedXpThisTurn} XP.</p>`;
                    }
                }

                 let leveledUp = false;
                 while (gameState.character.xp >= gameState.character.xpToNextLevel) {
                     if (levelUpCharacter()) { leveledUp = true; } else { break; }
                 }
                 if (leveledUp) { rollResultMessage += `<p class="level-up">LEVEL UP! You reached Level ${gameState.character.level}!</p>`; }

                 recalculateMaxHp(); // Ensure maxHP is current
                 gameState.character.stats.hp = Math.min(gameState.character.stats.hp, gameState.character.stats.maxHp); // Clamp HP
                 gameState.character.stats.hp = Math.max(0, gameState.character.stats.hp);

                 // Check for death *after* potential healing from level up
                 if (gameState.character.stats.hp <= 0) {
                    console.log("Player died!"); nextPageId = 99; // Force redirect
                    renderPageInternal(99, gameData[99], rollResultMessage + "<p class='roll-failure'><em>Your wounds overwhelm you...</em></p>"); return;
                 }

            } else { // Invalid next page ID
                console.error(`Data for page ${nextPageId} not found!`);
                renderPageInternal(99, gameData[99], "<p><em>Error: Next page data missing!</em></p>"); return;
            }
            renderPageInternal(nextPageId, gameData[nextPageId] || gameData["99"], rollResultMessage);
        }

        function renderPageInternal(pageId, pageData, message = "") {
            if (!pageData) { console.error(`Render Error: No data for page ${pageId}`); return; }
            storyTitleElement.textContent = pageData.title || "Untitled Page";
            storyContentElement.innerHTML = message + (pageData.content || "<p>...</p>");
            updateStatsDisplay(); updateInventoryDisplay();
            choicesElement.innerHTML = '';
            if (pageData.options && pageData.options.length > 0) {
                pageData.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, addItem: option.addItem, check: option.check, effect: option.effect }; button.onclick = () => handleChoiceClick(choiceData); } else { button.classList.add('disabled'); } choicesElement.appendChild(button); });
            } else { const button = document.createElement('button'); button.classList.add('choice-button'); button.textContent = pageData.gameOver ? "Restart Adventure" : "The End"; button.onclick = () => handleChoiceClick({ nextPage: pageData.gameOver ? 1 : 99 }); choicesElement.appendChild(button); if (!pageData.gameOver) choicesElement.insertAdjacentHTML('afterbegin', '<p><i>The path ends here.</i></p>'); }
            updateScene(pageData.illustration || 'default', pageData.sceneParams);
        }

        function renderPage(pageId) { renderPageInternal(pageId, gameData[pageId]); }

        function updateStatsDisplay() { const char=gameState.character; statsElement.innerHTML = `<strong>Stats:</strong> <span>Lvl: ${char.level}</span> <span>XP: ${char.xp}/${char.xpToNextLevel}</span> <span>HP: ${char.stats.hp}/${char.stats.maxHp}</span> <span title="Strength">Str: ${char.stats.strength}</span> <span title="Intelligence">Int: ${char.stats.intelligence}</span> <span title="Wisdom">Wis: ${char.stats.wisdom}</span> <span title="Dexterity">Dex: ${char.stats.dexterity}</span> <span title="Constitution">Con: ${char.stats.constitution}</span> <span title="Charisma">Cha: ${char.stats.charisma}</span>`; } // Added titles
        function updateInventoryDisplay() { let h='<strong>Inventory:</strong> '; if(gameState.character.inventory.length === 0){ h+='<em>Empty</em>'; } else { gameState.character.inventory.forEach(i=>{ const d=itemsData[i]||{type:'unknown',description:'???'}; const c=`item-${d.type||'unknown'}`; h+=`<span class="${c}" title="${d.description}">${i}</span>`; }); } inventoryElement.innerHTML = h; }

        function updateScene(illustrationKey, sceneParams = {}) {
            if (currentAssemblyGroup) { scene.remove(currentAssemblyGroup); }
            currentAssemblyGroup = null; let assemblyFunction;
            // Simple routing for now, using default for most new keys
            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': case 'dark-forest-entrance-gnarled-roots-filtered-light': case 'overgrown-forest-path-glowing-fungi-vines': case 'forest-clearing-mossy-statue-weathered-stone': case 'narrow-game-trail-forest-rope-bridge-ravine': case 'forest-stream-crossing-dappled-sunlight-stones': case 'mossy-log-bridge-over-forest-stream': case 'dense-forest-undergrowth-shadows': assemblyFunction = createForestAssembly; break; // Group forest keys
                case 'road-ambush': case 'two-goblins-ambush-forest-path-spears': assemblyFunction = createRoadAmbushAssembly; break; // Group ambush keys
                case 'forest-edge': case 'forest-edge-view-rocky-foothills-distant-mountain-fortress': assemblyFunction = createForestEdgeAssembly; break; // Group edge keys
                case 'prisoner-cell': assemblyFunction = createPrisonerCellAssembly; break;
                case 'game-over': case 'game-over-generic': assemblyFunction = createGameOverAssembly; break;
                case 'error': assemblyFunction = createErrorAssembly; break;
                // Add more specific assignments if needed
                default: assemblyFunction = createDefaultAssembly; break;
            }
            try { currentAssemblyGroup = assemblyFunction(sceneParams); scene.add(currentAssemblyGroup); } catch (error) { console.error(`Error creating assembly for ${illustrationKey}:`, error); currentAssemblyGroup = createErrorAssembly(); scene.add(currentAssemblyGroup); }
        }

        document.addEventListener('DOMContentLoaded', () => {
             try { initThreeJS(); startGame(); } catch (error) { console.error("Init failed:", error); storyTitleElement.textContent = "Error"; storyContentElement.innerHTML = `<p>Init Error. Check console.</p><pre>${error}</pre>`; }
        });

    </script>

</body>
</html>