awacke1 commited on
Commit
ade1218
·
verified ·
1 Parent(s): 7ae9685

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +360 -437
index.html CHANGED
@@ -3,84 +3,21 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Procedural Adventure Reboot</title>
7
  <style>
8
- body {
9
- font-family: 'Courier New', monospace;
10
- background-color: #1a1a1a;
11
- color: #e0e0e0;
12
- margin: 0;
13
- padding: 0;
14
- overflow: hidden;
15
- display: flex;
16
- flex-direction: column;
17
- height: 100vh;
18
- }
19
- #game-container {
20
- display: flex;
21
- flex-grow: 1;
22
- overflow: hidden;
23
- }
24
- #scene-container {
25
- flex-grow: 3;
26
- position: relative;
27
- border-right: 2px solid #444;
28
- min-width: 250px;
29
- background-color: #000;
30
- height: 100%;
31
- box-sizing: border-box;
32
- overflow: hidden;
33
- }
34
- #ui-container {
35
- flex-grow: 2;
36
- padding: 25px;
37
- overflow-y: auto;
38
- background-color: #2b2b2b;
39
- min-width: 320px;
40
- height: 100%;
41
- box-sizing: border-box;
42
- display: flex;
43
- flex-direction: column;
44
- }
45
  #scene-container canvas { display: block; }
46
- #story-title {
47
- color: #f0c060;
48
- margin: 0 0 15px 0;
49
- padding-bottom: 10px;
50
- border-bottom: 1px solid #555;
51
- font-size: 1.6em;
52
- text-shadow: 1px 1px 1px #000;
53
- }
54
- #story-content {
55
- margin-bottom: 25px;
56
- line-height: 1.7;
57
- flex-grow: 1;
58
- font-size: 1.1em;
59
- }
60
- #story-content p { margin-bottom: 1.1em; }
61
- #story-content p:last-child { margin-bottom: 0; }
62
- #stats-inventory-container {
63
- margin-bottom: 25px;
64
- padding: 15px;
65
- border: 1px solid #444;
66
- border-radius: 4px;
67
- background-color: #333;
68
- font-size: 0.95em;
69
- }
70
- #stats-display, #inventory-display {
71
- margin-bottom: 10px;
72
- line-height: 1.8;
73
- }
74
- #stats-display span, #inventory-display span {
75
- display: inline-block;
76
- background-color: #484848;
77
- padding: 3px 9px;
78
- border-radius: 15px;
79
- margin: 0 8px 5px 0;
80
- border: 1px solid #6a6a6a;
81
- white-space: nowrap;
82
- box-shadow: inset 0 1px 2px rgba(0,0,0,0.3);
83
- }
84
  #stats-display strong, #inventory-display strong { color: #ccc; margin-right: 6px; }
85
  #inventory-display em { color: #888; font-style: normal; }
86
  .item-quest { background-color: #666030; border-color: #999048;}
@@ -88,50 +25,29 @@
88
  .item-armor { background-color: #306630; border-color: #489948;}
89
  .item-consumable { background-color: #664430; border-color: #996648;}
90
  .item-unknown { background-color: #555; border-color: #777;}
91
-
92
- #choices-container {
93
- margin-top: auto;
94
- padding-top: 20px;
95
- border-top: 1px solid #555;
96
- }
97
  #choices-container h3 { margin-top: 0; margin-bottom: 12px; color: #ccc; font-size: 1.1em; }
98
  #choices { display: flex; flex-direction: column; gap: 12px; }
99
- .choice-button {
100
- display: block; width: 100%; padding: 12px 15px;
101
- margin-bottom: 0;
102
- background-color: #555; color: #eee; border: 1px solid #777;
103
- border-radius: 4px; cursor: pointer; text-align: left;
104
- font-family: 'Courier New', monospace; font-size: 1.05em;
105
- transition: background-color 0.2s, border-color 0.2s, box-shadow 0.1s;
106
- box-sizing: border-box;
107
- }
108
- .choice-button:hover:not(:disabled) {
109
- background-color: #e0b050; color: #111; border-color: #c89040;
110
- box-shadow: 0 0 5px rgba(255, 200, 100, 0.5);
111
- }
112
- .choice-button:disabled {
113
- background-color: #404040; color: #777; cursor: not-allowed; border-color: #555;
114
- opacity: 0.7;
115
- }
116
- .choice-button[title]:disabled::after {
117
- content: ' (' attr(title) ')'; font-style: italic; color: #999; font-size: 0.9em; margin-left: 5px;
118
- }
119
- .message {
120
- padding: 8px 12px; margin-bottom: 1em; border-left-width: 3px; border-left-style: solid;
121
- font-size: 0.95em; background-color: rgba(255, 255, 255, 0.05);
122
- }
123
  .message-success { color: #8f8; border-left-color: #4a4; }
124
  .message-failure { color: #f88; border-left-color: #a44; }
125
  .message-info { color: #aaa; border-left-color: #666; }
126
  .message-item { color: #8bf; border-left-color: #46a; }
 
 
127
  </style>
128
  </head>
129
  <body>
130
  <div id="game-container">
131
- <div id="scene-container"></div>
 
 
132
  <div id="ui-container">
133
- <h2 id="story-title">Initiating...</h2>
134
- <div id="story-content"><p>Stand by...</p></div>
135
  <div id="stats-inventory-container">
136
  <div id="stats-display"></div>
137
  <div id="inventory-display"></div>
@@ -152,6 +68,7 @@
152
 
153
  <script type="module">
154
  import * as THREE from 'three';
 
155
 
156
  const sceneContainer = document.getElementById('scene-container');
157
  const storyTitleElement = document.getElementById('story-title');
@@ -159,10 +76,14 @@
159
  const choicesElement = document.getElementById('choices');
160
  const statsElement = document.getElementById('stats-display');
161
  const inventoryElement = document.getElementById('inventory-display');
 
 
162
 
163
- let scene, camera, renderer, clock;
164
- let currentSceneGroup = null;
165
- let currentLights = [];
 
 
166
 
167
  const MAT = {
168
  stone: new THREE.MeshStandardMaterial({ color: 0x777788, roughness: 0.85, metalness: 0.1 }),
@@ -176,18 +97,23 @@
176
  error: new THREE.MeshStandardMaterial({ color: 0xff3300, roughness: 0.5, emissive: 0x551100 }),
177
  gameOver: new THREE.MeshStandardMaterial({ color: 0xaa0000, roughness: 0.6, metalness: 0.2, emissive: 0x220000 }),
178
  simple: new THREE.MeshStandardMaterial({ color: 0xaaaaaa, roughness: 0.8 }),
 
 
179
  };
180
 
181
  function initThreeJS() {
182
  scene = new THREE.Scene();
183
  scene.background = new THREE.Color(0x1a1a1a);
184
  clock = new THREE.Clock();
 
 
 
 
185
 
186
  const width = sceneContainer.clientWidth || 1;
187
  const height = sceneContainer.clientHeight || 1;
188
  camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 1000);
189
- camera.position.set(0, 3, 9);
190
- camera.lookAt(0, 0.5, 0);
191
 
192
  renderer = new THREE.WebGLRenderer({ antialias: true });
193
  renderer.setSize(width, height);
@@ -197,7 +123,15 @@
197
  renderer.outputColorSpace = THREE.SRGBColorSpace;
198
  sceneContainer.appendChild(renderer.domElement);
199
 
 
 
 
 
 
 
200
  window.addEventListener('resize', onWindowResize, false);
 
 
201
  setTimeout(onWindowResize, 50);
202
  animate();
203
  }
@@ -211,92 +145,112 @@
211
  renderer.setSize(width, height);
212
  }
213
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
  function animate() {
215
  requestAnimationFrame(animate);
216
  const delta = clock.getDelta();
217
  const time = clock.getElapsedTime();
218
- scene.traverse(obj => { if (obj.userData.update) obj.userData.update(time, delta); });
 
 
 
219
  if (renderer && scene && camera) renderer.render(scene, camera);
220
  }
221
 
222
  function createMesh(geometry, material, pos = {x:0,y:0,z:0}, rot = {x:0,y:0,z:0}, scale = {x:1,y:1,z:1}) {
223
- const mesh = new THREE.Mesh(geometry, material);
224
- mesh.position.set(pos.x, pos.y, pos.z);
225
- mesh.rotation.set(rot.x, rot.y, rot.z);
226
- mesh.scale.set(scale.x, scale.y, scale.z);
227
- mesh.castShadow = true; mesh.receiveShadow = true;
228
- return mesh;
229
- }
230
 
231
  function createGround(material = MAT.ground, size = 20) {
232
- const geo = new THREE.PlaneGeometry(size, size);
233
- const ground = new THREE.Mesh(geo, material);
234
- ground.rotation.x = -Math.PI / 2; ground.position.y = 0;
235
- ground.receiveShadow = true; ground.castShadow = false;
236
- return ground;
 
237
  }
238
 
239
  function setupLighting(type = 'default') {
240
- currentLights.forEach(light => scene.remove(light));
241
- currentLights = [];
242
-
243
- let ambientIntensity = 0.3;
244
- let dirIntensity = 0.8;
245
- let dirColor = 0xffffff;
246
- let dirPosition = new THREE.Vector3(10, 15, 8);
247
-
248
- if (type === 'forest') {
249
- ambientIntensity = 0.2; dirIntensity = 0.6; dirColor = 0xccffcc; dirPosition = new THREE.Vector3(5, 10, 5);
250
- } else if (type === 'cave') {
251
- ambientIntensity = 0.1; dirIntensity = 0;
252
- const ptLight = new THREE.PointLight(0xffaa55, 1.5, 12, 1);
253
- ptLight.position.set(0, 1.5, 1);
254
- ptLight.castShadow = true;
255
- ptLight.shadow.mapSize.set(512, 512);
256
- scene.add(ptLight); currentLights.push(ptLight);
257
- } else if (type === 'gameover') {
258
- ambientIntensity = 0.1; dirIntensity = 0.4; dirColor = 0xff6666;
259
- }
260
 
261
- const ambientLight = new THREE.AmbientLight(0xffffff, ambientIntensity);
262
- scene.add(ambientLight);
263
- currentLights.push(ambientLight);
264
-
265
- if (dirIntensity > 0) {
266
- const directionalLight = new THREE.DirectionalLight(dirColor, dirIntensity);
267
- directionalLight.position.copy(dirPosition);
268
- directionalLight.castShadow = true;
269
- directionalLight.shadow.mapSize.set(1024, 1024);
270
- directionalLight.shadow.camera.near = 0.5;
271
- directionalLight.shadow.camera.far = 50;
272
- const shadowBounds = 15;
273
- directionalLight.shadow.camera.left = -shadowBounds;
274
- directionalLight.shadow.camera.right = shadowBounds;
275
- directionalLight.shadow.camera.top = shadowBounds;
276
- directionalLight.shadow.camera.bottom = -shadowBounds;
277
- directionalLight.shadow.bias = -0.0005;
278
- scene.add(directionalLight);
279
- currentLights.push(directionalLight);
280
- }
281
  }
282
 
283
- function createDefaultScene() {
284
  const group = new THREE.Group();
285
- group.add(createGround(MAT.dirt, 15));
286
  const boxGeo = new THREE.BoxGeometry(1, 1, 1);
287
- group.add(createMesh(boxGeo, MAT.stone, {y: 0.5}));
288
- return { group, lighting: 'default' };
 
 
 
289
  }
290
 
291
- function createForestScene() {
292
  const group = new THREE.Group();
293
- group.add(createGround(MAT.ground, 25));
294
  const trunkGeo = new THREE.CylinderGeometry(0.2, 0.3, 4, 8);
295
  const leafGeo = new THREE.SphereGeometry(1.5, 8, 6);
296
- for(let i=0; i<15; i++) {
297
- const x = (Math.random() - 0.5) * 20;
298
- const z = (Math.random() - 0.5) * 20;
299
- if(Math.sqrt(x*x+z*z) < 2) continue;
300
  const tree = new THREE.Group();
301
  const trunk = createMesh(trunkGeo, MAT.wood, {y: 2});
302
  const leaves = createMesh(leafGeo, MAT.leaf, {y: 4.5});
@@ -305,305 +259,169 @@
305
  tree.rotation.y = Math.random() * Math.PI * 2;
306
  group.add(tree);
307
  }
308
- return { group, lighting: 'forest' };
 
 
 
 
 
309
  }
310
 
311
- function createGameOverScene() {
312
  const group = new THREE.Group();
313
- group.add(createGround(MAT.stone.clone().set({color: 0x333333}), 10));
314
- const boxGeo = new THREE.BoxGeometry(2, 2, 2);
315
- group.add(createMesh(boxGeo, MAT.gameOver, {y: 1}));
316
- return { group, lighting: 'gameover' };
317
- }
318
-
319
- function createCaveScene() {
320
- const group = new THREE.Group();
321
- group.add(createGround(MAT.stone, 15));
322
- const wallGeo = new THREE.SphereGeometry(10, 32, 16, 0, Math.PI*2, 0, Math.PI*0.7);
323
- const walls = createMesh(wallGeo, MAT.stone, {y: 3});
324
  walls.material.side = THREE.BackSide;
325
  group.add(walls);
326
  const coneGeo = new THREE.ConeGeometry(0.2, 1.0, 8);
327
- for(let i=0; i<10; i++){
328
- const st = createMesh(coneGeo, MAT.stone, {x: (Math.random()-0.5)*12, y: 5 + Math.random(), z: (Math.random()-0.5)*12}, {x:Math.PI});
329
  group.add(st);
330
  }
331
- return { group, lighting: 'cave' };
332
- }
333
-
334
-
335
- function updateScene(illustrationKey = 'default') {
336
- if (currentSceneGroup) {
337
- currentSceneGroup.traverse(child => {
338
- if (child.isMesh) child.geometry.dispose();
339
- });
340
- scene.remove(currentSceneGroup);
341
- currentSceneGroup = null;
342
- }
343
-
344
- let sceneData;
345
- switch (illustrationKey) {
346
- case 'forest': case 'overgrown-path': case 'goblin-ambush':
347
- sceneData = createForestScene(); break;
348
- case 'game-over':
349
- sceneData = createGameOverScene(); break;
350
- case 'cave':
351
- sceneData = createCaveScene(); break;
352
- default:
353
- sceneData = createDefaultScene(); break;
354
- }
355
-
356
- currentSceneGroup = sceneData.group;
357
- scene.add(currentSceneGroup);
358
- setupLighting(sceneData.lighting);
359
- }
360
-
361
- const itemsData = {
362
- "Rusty Sword": {type:"weapon", description:"Old but sharp."},
363
- "Torch": {type:"consumable", description:"Provides light.", use: "light"},
364
- "Key": {type:"quest", description:"A simple iron key."}
365
  };
366
 
367
- const gameData = {
368
- 1: {
369
- title: "A Fork in the Path",
370
- content: "<p>The dirt path splits. To the north, it enters a dark forest. To the east, it winds towards distant hills.</p>",
371
- options: [
372
- { text: "Enter the Forest", next: 2 },
373
- { text: "Head towards the Hills", next: 3 }
374
- ],
375
- illustration: "forest"
376
- },
377
- 2: {
378
- title: "Deep Forest",
379
- content: "<p>The trees loom overhead, blocking most light. It's eerily quiet. You spot a faint glimmer ahead.</p>",
380
- options: [
381
- { text: "Investigate the glimmer", next: 4 },
382
- { text: "Turn back to the path", next: 1 }
383
- ],
384
- illustration: "forest"
385
- },
386
- 3: {
387
- title: "Rolling Hills",
388
- content: "<p>The hills are gentle under an open sky. You see a small cave entrance in the side of one hill.</p>",
389
- options: [
390
- { text: "Explore the cave", next: 5 },
391
- { text: "Continue over the hills", next: 99 },
392
- { text: "Go back to the path", next: 1 }
393
- ],
394
- illustration: "default"
395
- },
396
- 4: {
397
- title: "Forest Glimmer",
398
- content: "<p>The glimmer comes from a Rusty Sword half-buried in the leaves.</p>",
399
- options: [ { text: "Take the sword", next: 1, reward: {addItem: "Rusty Sword"} } ],
400
- illustration: "forest"
401
- },
402
- 5: {
403
- title: "Small Cave",
404
- content: "<p>The cave is dark and damp. You can barely see. Something skitters in the darkness.</p>",
405
- options: [
406
- { text: "Light Torch (if you have one)", requireItem: "Torch", consumeItem: true, next: 6},
407
- { text: "Try to fight in the dark (Dexterity Check DC 13)", check: {stat: "dexterity", dc: 13, onFailure: 7}, next: 8},
408
- { text: "Flee the cave", next: 3 }
409
- ],
410
- illustration: "cave"
411
  },
412
- 6: {
413
- title: "Cave - Lit",
414
- content: "<p>Your torch illuminates the cave. You see giant spiders! One lunges.</p>",
415
- options: [ { text: "Fight!", next: 8 } ],
416
- illustration: "cave"
417
  },
418
- 7: {
419
- title: "Cave - Failure",
420
- content: "<p>Thrashing blindly, you feel a sharp pain as something bites you! (-5 HP)</p>",
421
- options: [ { text: "Try to flee!", next: 3, hpLoss: 5 } ],
422
- illustration: "cave"
423
- },
424
- 8: {
425
- title: "Cave - Victory",
426
- content: "<p>You manage to defeat the creatures! You find an old Key.</p>",
427
- options: [ { text: "Leave the cave", next: 3, reward: {addItem: "Key", xp: 50} } ],
428
- illustration: "cave"
429
- },
430
- 99: {
431
- title: "The End?",
432
- content: "<p>Your journey ends here... for now.</p>",
433
- options: [ { text: "Restart", next: 1 } ],
434
- illustration: "game-over",
435
- gameOver: true
436
- }
437
  };
438
 
439
  let gameState = {
440
- currentPageId: 1,
441
  character: {
442
- name: "Hero",
443
- stats: { strength: 10, dexterity: 10, constitution: 10, hp: 15, maxHp: 15, xp: 0 },
444
  inventory: []
445
  }
446
  };
447
 
448
  function startGame() {
449
  const defaultChar = {
450
- name: "Hero",
451
- stats: { strength: 10, dexterity: 10, constitution: 10, hp: 15, maxHp: 15, xp: 0 },
452
  inventory: []
453
  };
454
  gameState = {
455
- currentPageId: 1,
456
  character: JSON.parse(JSON.stringify(defaultChar))
457
  };
458
  console.log("Starting new game:", gameState);
459
- renderPage(gameState.currentPageId);
460
  }
461
 
462
- function handleChoiceClick(choiceData) {
463
- console.log("Choice:", choiceData);
464
- let messageLog = "";
465
-
466
- let consumedItemName = null;
467
- if (choiceData.consumeItem && choiceData.requireItem) {
468
- if (gameState.character.inventory.includes(choiceData.requireItem)) {
469
- gameState.character.inventory = gameState.character.inventory.filter(i => i !== choiceData.requireItem);
470
- consumedItemName = choiceData.requireItem;
471
- messageLog += `<p class="message message-item"><em>Used: ${consumedItemName}</em></p>`;
472
- } else {
473
- console.error("Tried to consume unavailable item:", choiceData.requireItem);
474
- return;
475
- }
476
  }
477
 
478
- let nextPageId = parseInt(choiceData.nextPage);
479
- const check = choiceData.check;
480
-
481
- if (check) {
482
- const baseStatValue = gameState.character.stats[check.stat] || 10;
483
- const roll = Math.floor(Math.random() * 20) + 1;
484
- const modifier = Math.floor((baseStatValue - 10) / 2);
485
- const totalResult = roll + modifier;
486
- const dc = check.dc;
487
- console.log(`Check: ${check.stat} (Base: ${baseStatValue}, Mod: ${modifier}) | Roll: ${roll} -> Total: ${totalResult} vs DC ${dc}`);
488
-
489
- if (totalResult >= dc) {
490
- messageLog += `<p class="message message-success"><em>${check.stat.charAt(0).toUpperCase() + check.stat.slice(1)} Check Success! (${totalResult} vs DC ${dc})</em></p>`;
491
- nextPageId = parseInt(choiceData.nextPage);
492
- } else {
493
- messageLog += `<p class="message message-failure"><em>${check.stat.charAt(0).toUpperCase() + check.stat.slice(1)} Check Failed! (${totalResult} vs DC ${dc})</em></p>`;
494
- nextPageId = parseInt(check.onFailure);
495
- if (isNaN(nextPageId)) {
496
- console.error("Invalid onFailure ID:", check.onFailure);
497
- nextPageId = 99;
498
- messageLog += `<p class="message message-failure">Error: Invalid failure path!</p>`;
499
- }
500
- }
501
- }
 
 
502
 
503
- const targetPageData = gameData[nextPageId];
504
- if (!targetPageData) {
505
- console.error(`Page data missing for ID: ${nextPageId}`);
506
- renderPageInternal(99, gameData[99], messageLog + `<p class="message message-failure">Error: Page data missing!</p>`);
507
- return;
508
- }
509
 
510
- let hpChange = 0;
511
- if (targetPageData.hpLoss) {
512
- hpChange = -targetPageData.hpLoss;
513
- messageLog += `<p class="message message-failure"><em>Lost ${targetPageData.hpLoss} HP.</em></p>`;
514
- }
515
- if (targetPageData.reward?.hpGain) {
516
- hpChange = targetPageData.reward.hpGain;
517
- messageLog += `<p class="message message-success"><em>Recovered ${targetPageData.reward.hpGain} HP.</em></p>`;
518
- }
519
- if(hpChange !== 0) {
520
- gameState.character.stats.hp += hpChange;
521
- gameState.character.stats.hp = Math.max(0, Math.min(gameState.character.stats.hp, gameState.character.stats.maxHp));
522
- }
523
 
 
 
524
 
525
- if (targetPageData.reward?.xp) {
526
- gameState.character.stats.xp += targetPageData.reward.xp;
527
- messageLog += `<p class="message message-info"><em>Gained ${targetPageData.reward.xp} XP.</em></p>`;
528
- }
529
- if (targetPageData.reward?.addItem) {
530
- const item = targetPageData.reward.addItem;
531
- if (itemsData[item] && !gameState.character.inventory.includes(item)) {
532
- gameState.character.inventory.push(item);
533
- messageLog += `<p class="message message-item"><em>Acquired: ${item}</em></p>`;
534
- }
535
- }
536
 
537
- if (gameState.character.stats.hp <= 0) {
538
- console.log("Player died!");
539
- nextPageId = 99;
540
- messageLog += `<p class="message message-failure"><b>You have succumbed!</b></p>`;
541
- renderPageInternal(nextPageId, gameData[nextPageId] || gameData[99], messageLog);
 
 
 
 
 
 
542
  return;
543
  }
544
 
545
- gameState.currentPageId = nextPageId;
546
- renderPageInternal(nextPageId, gameData[nextPageId], messageLog);
547
- }
 
 
 
 
 
 
 
 
 
 
 
 
548
 
549
- function renderPageInternal(pageId, pageData, message = "") {
550
- if (!pageData) {
551
- console.error(`Render Error: No data for page ${pageId}!`);
552
- pageData = gameData[99];
553
- message += `<p class="message message-failure">Render Error: Page ${pageId} not found!</p>`;
554
- pageId = 99;
555
- }
556
 
557
- storyTitleElement.textContent = pageData.title || "Untitled";
558
- storyContentElement.innerHTML = message + (pageData.content || "<p>...</p>");
559
-
560
- updateStatsDisplay();
561
- updateInventoryDisplay();
562
- choicesElement.innerHTML = '';
563
-
564
- if (pageData.options && pageData.options.length > 0) {
565
- pageData.options.forEach(option => {
566
- const button = document.createElement('button');
567
- button.classList.add('choice-button');
568
- button.textContent = option.text;
569
- let requirementMet = true;
570
- let reqText = [];
571
-
572
- if (option.requireItem && !gameState.character.inventory.includes(option.requireItem)) {
573
- requirementMet = false;
574
- reqText.push(`Requires: ${option.requireItem}`);
575
- }
576
-
577
- button.disabled = !requirementMet;
578
- if (!requirementMet) {
579
- button.title = reqText.join(', ');
580
- } else {
581
- const choiceData = {
582
- nextPage: option.next,
583
- check: option.check,
584
- onFailure: option.onFailure,
585
- reward: option.reward,
586
- hpLoss: option.hpLoss,
587
- requireItem: option.requireItem,
588
- consumeItem: option.consumeItem,
589
- };
590
- button.onclick = () => handleChoiceClick(choiceData);
591
- }
592
- choicesElement.appendChild(button);
593
- });
594
  } else {
595
- const button = document.createElement('button');
596
- button.classList.add('choice-button');
597
- button.textContent = "Restart";
598
- button.onclick = () => handleChoiceClick({ nextPage: 1 });
599
- choicesElement.appendChild(button);
600
  }
601
- updateScene(pageData.illustration);
602
- }
603
-
604
- function renderPage(pageId) {
605
- renderPageInternal(pageId, gameData[pageId]);
606
  }
 
607
 
608
  function updateStatsDisplay() {
609
  const { hp, maxHp, xp } = gameState.character.stats;
@@ -619,28 +437,133 @@
619
  gameState.character.inventory.forEach(item => {
620
  const itemDef = itemsData[item] || { type: 'unknown', description: '???' };
621
  const itemClass = `item-${itemDef.type || 'unknown'}`;
622
- invHtml += `<span class="${itemClass}" title="${itemDef.description}">${item}</span>`;
 
623
  });
624
  }
625
  inventoryElement.innerHTML = invHtml;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
626
  }
627
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
628
 
629
  document.addEventListener('DOMContentLoaded', () => {
630
- console.log("DOM Ready - Initializing Adventure Reboot.");
631
  try {
632
  initThreeJS();
633
  if (!scene || !camera || !renderer) throw new Error("Three.js failed to initialize.");
634
  startGame();
635
- console.log("Game started successfully.");
636
  } catch (error) {
637
  console.error("Initialization failed:", error);
638
  storyTitleElement.textContent = "Initialization Error";
639
  storyContentElement.innerHTML = `<p style="color:red;">Failed to start game:</p><pre style="color:red; white-space: pre-wrap;">${error.stack || error}</pre>`;
640
- choicesElement.innerHTML = '';
641
  if(sceneContainer) sceneContainer.innerHTML = '<p style="color:red; padding: 20px;">3D Scene Failed</p>';
642
- statsInventoryContainer.style.display = 'none';
643
- choicesContainer.style.display = 'none';
644
  }
645
  });
646
 
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Persistent Procedural World</title>
7
  <style>
8
+ body { font-family: 'Courier New', monospace; background-color: #111; color: #eee; margin: 0; padding: 0; overflow: hidden; display: flex; flex-direction: column; height: 100vh; }
9
+ #game-container { display: flex; flex-grow: 1; overflow: hidden; }
10
+ #scene-container { flex-grow: 3; position: relative; border-right: 2px solid #444; min-width: 250px; background-color: #000; height: 100%; box-sizing: border-box; overflow: hidden; cursor: crosshair; }
11
+ #ui-container { flex-grow: 2; padding: 25px; overflow-y: auto; background-color: #2b2b2b; min-width: 320px; height: 100%; box-sizing: border-box; display: flex; flex-direction: column; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  #scene-container canvas { display: block; }
13
+ #story-title { color: #f0c060; margin: 0 0 15px 0; padding-bottom: 10px; border-bottom: 1px solid #555; font-size: 1.6em; text-shadow: 1px 1px 1px #000; }
14
+ #story-content { margin-bottom: 25px; line-height: 1.7; flex-grow: 1; font-size: 1.1em; }
15
+ #stats-inventory-container { margin-bottom: 25px; padding: 15px; border: 1px solid #444; border-radius: 4px; background-color: #333; font-size: 0.95em; }
16
+ #stats-display, #inventory-display { margin-bottom: 10px; line-height: 1.8; }
17
+ #stats-display span, #inventory-display .item-tag { display: inline-block; background-color: #484848; padding: 3px 9px; border-radius: 15px; margin: 0 8px 5px 0; border: 1px solid #6a6a6a; white-space: nowrap; box-shadow: inset 0 1px 2px rgba(0,0,0,0.3); }
18
+ #inventory-display .item-tag { cursor: pointer; transition: background-color 0.2s; }
19
+ #inventory-display .item-tag:hover { background-color: #6a6a6a; }
20
+ #inventory-display .item-tag.placing { background-color: #a07030; border-color: #c89040; color: #fff; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  #stats-display strong, #inventory-display strong { color: #ccc; margin-right: 6px; }
22
  #inventory-display em { color: #888; font-style: normal; }
23
  .item-quest { background-color: #666030; border-color: #999048;}
 
25
  .item-armor { background-color: #306630; border-color: #489948;}
26
  .item-consumable { background-color: #664430; border-color: #996648;}
27
  .item-unknown { background-color: #555; border-color: #777;}
28
+ #choices-container { margin-top: auto; padding-top: 20px; border-top: 1px solid #555; }
 
 
 
 
 
29
  #choices-container h3 { margin-top: 0; margin-bottom: 12px; color: #ccc; font-size: 1.1em; }
30
  #choices { display: flex; flex-direction: column; gap: 12px; }
31
+ .choice-button { display: block; width: 100%; padding: 12px 15px; margin-bottom: 0; background-color: #555; color: #eee; border: 1px solid #777; border-radius: 4px; cursor: pointer; text-align: left; font-family: 'Courier New', monospace; font-size: 1.05em; transition: background-color 0.2s, border-color 0.2s, box-shadow 0.1s; box-sizing: border-box; }
32
+ .choice-button:hover:not(:disabled) { background-color: #e0b050; color: #111; border-color: #c89040; box-shadow: 0 0 5px rgba(255, 200, 100, 0.5); }
33
+ .choice-button:disabled { background-color: #404040; color: #777; cursor: not-allowed; border-color: #555; opacity: 0.7; }
34
+ .message { padding: 8px 12px; margin-bottom: 1em; border-left-width: 3px; border-left-style: solid; font-size: 0.95em; background-color: rgba(255, 255, 255, 0.05); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  .message-success { color: #8f8; border-left-color: #4a4; }
36
  .message-failure { color: #f88; border-left-color: #a44; }
37
  .message-info { color: #aaa; border-left-color: #666; }
38
  .message-item { color: #8bf; border-left-color: #46a; }
39
+ #action-info { position: absolute; bottom: 10px; left: 10px; background-color: rgba(0,0,0,0.7); color: #ffcc66; padding: 5px 10px; border-radius: 3px; font-size: 0.9em; display: none; }
40
+
41
  </style>
42
  </head>
43
  <body>
44
  <div id="game-container">
45
+ <div id="scene-container">
46
+ <div id="action-info">Mode: Explore</div>
47
+ </div>
48
  <div id="ui-container">
49
+ <h2 id="story-title">World Initializing...</h2>
50
+ <div id="story-content"><p>Establishing reality...</p></div>
51
  <div id="stats-inventory-container">
52
  <div id="stats-display"></div>
53
  <div id="inventory-display"></div>
 
68
 
69
  <script type="module">
70
  import * as THREE from 'three';
71
+ import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; // Using controls for navigation now
72
 
73
  const sceneContainer = document.getElementById('scene-container');
74
  const storyTitleElement = document.getElementById('story-title');
 
76
  const choicesElement = document.getElementById('choices');
77
  const statsElement = document.getElementById('stats-display');
78
  const inventoryElement = document.getElementById('inventory-display');
79
+ const actionInfoElement = document.getElementById('action-info');
80
+
81
 
82
+ let scene, camera, renderer, clock, controls, raycaster, mouse;
83
+ let worldGroup = null; // Parent for all world objects
84
+ let locationGroups = {}; // Store groups for each location/zone { locationId: group }
85
+ let currentMessage = ""; // To accumulate UI messages
86
+ let currentPlacingItem = null; // Item name being placed
87
 
88
  const MAT = {
89
  stone: new THREE.MeshStandardMaterial({ color: 0x777788, roughness: 0.85, metalness: 0.1 }),
 
97
  error: new THREE.MeshStandardMaterial({ color: 0xff3300, roughness: 0.5, emissive: 0x551100 }),
98
  gameOver: new THREE.MeshStandardMaterial({ color: 0xaa0000, roughness: 0.6, metalness: 0.2, emissive: 0x220000 }),
99
  simple: new THREE.MeshStandardMaterial({ color: 0xaaaaaa, roughness: 0.8 }),
100
+ pickupHighlight: new THREE.MeshBasicMaterial({ color: 0xffff00, wireframe: true, depthTest: false }),
101
+ placementPreview: new THREE.MeshBasicMaterial({ color: 0x00ff00, transparent: true, opacity: 0.5, wireframe: true }),
102
  };
103
 
104
  function initThreeJS() {
105
  scene = new THREE.Scene();
106
  scene.background = new THREE.Color(0x1a1a1a);
107
  clock = new THREE.Clock();
108
+ raycaster = new THREE.Raycaster();
109
+ mouse = new THREE.Vector2();
110
+ worldGroup = new THREE.Group();
111
+ scene.add(worldGroup);
112
 
113
  const width = sceneContainer.clientWidth || 1;
114
  const height = sceneContainer.clientHeight || 1;
115
  camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 1000);
116
+ camera.position.set(0, 5, 10);
 
117
 
118
  renderer = new THREE.WebGLRenderer({ antialias: true });
119
  renderer.setSize(width, height);
 
123
  renderer.outputColorSpace = THREE.SRGBColorSpace;
124
  sceneContainer.appendChild(renderer.domElement);
125
 
126
+ controls = new OrbitControls(camera, renderer.domElement);
127
+ controls.enableDamping = true;
128
+ controls.dampingFactor = 0.1;
129
+ controls.target.set(0, 1, 0);
130
+ controls.maxPolarAngle = Math.PI / 2 - 0.05; // Prevent looking below ground
131
+
132
  window.addEventListener('resize', onWindowResize, false);
133
+ renderer.domElement.addEventListener('mousemove', onMouseMove, false);
134
+ renderer.domElement.addEventListener('click', onMouseClick, false);
135
  setTimeout(onWindowResize, 50);
136
  animate();
137
  }
 
145
  renderer.setSize(width, height);
146
  }
147
 
148
+ function onMouseMove( event ) {
149
+ mouse.x = ( event.clientX / renderer.domElement.clientWidth ) * 2 - 1;
150
+ mouse.y = - ( event.clientY / renderer.domElement.clientHeight ) * 2 + 1;
151
+ }
152
+
153
+ function onMouseClick( event ) {
154
+ if (currentPlacingItem) {
155
+ placeItem();
156
+ } else {
157
+ pickupItem();
158
+ }
159
+ }
160
+
161
  function animate() {
162
  requestAnimationFrame(animate);
163
  const delta = clock.getDelta();
164
  const time = clock.getElapsedTime();
165
+
166
+ controls.update();
167
+ worldGroup.traverse(obj => { if (obj.userData.update) obj.userData.update(time, delta); });
168
+
169
  if (renderer && scene && camera) renderer.render(scene, camera);
170
  }
171
 
172
  function createMesh(geometry, material, pos = {x:0,y:0,z:0}, rot = {x:0,y:0,z:0}, scale = {x:1,y:1,z:1}) {
173
+ const mesh = new THREE.Mesh(geometry, material);
174
+ mesh.position.set(pos.x, pos.y, pos.z);
175
+ mesh.rotation.set(rot.x, rot.y, rot.z);
176
+ mesh.scale.set(scale.x, scale.y, scale.z);
177
+ mesh.castShadow = true; mesh.receiveShadow = true;
178
+ return mesh;
179
+ }
180
 
181
  function createGround(material = MAT.ground, size = 20) {
182
+ const geo = new THREE.PlaneGeometry(size, size);
183
+ const ground = new THREE.Mesh(geo, material);
184
+ ground.rotation.x = -Math.PI / 2; ground.position.y = 0;
185
+ ground.receiveShadow = true; ground.castShadow = false;
186
+ ground.userData.isGround = true; // Mark for raycasting
187
+ return ground;
188
  }
189
 
190
  function setupLighting(type = 'default') {
191
+ currentLights.forEach(light => scene.remove(light));
192
+ currentLights = [];
193
+
194
+ let ambientIntensity = 0.4;
195
+ let dirIntensity = 0.9;
196
+ let dirColor = 0xffffff;
197
+ let dirPosition = new THREE.Vector3(10, 15, 8);
198
+
199
+ if (type === 'forest') {
200
+ ambientIntensity = 0.3; dirIntensity = 0.7; dirColor = 0xccffcc; dirPosition = new THREE.Vector3(5, 10, 5);
201
+ } else if (type === 'cave') {
202
+ ambientIntensity = 0.1; dirIntensity = 0;
203
+ const ptLight = new THREE.PointLight(0xffaa55, 1.5, 12, 1);
204
+ ptLight.position.set(0, 1.5, 1);
205
+ ptLight.castShadow = true;
206
+ ptLight.shadow.mapSize.set(512, 512);
207
+ scene.add(ptLight); currentLights.push(ptLight);
208
+ } else if (type === 'gameover') {
209
+ ambientIntensity = 0.1; dirIntensity = 0.4; dirColor = 0xff6666;
210
+ }
211
 
212
+ const ambientLight = new THREE.AmbientLight(0xffffff, ambientIntensity);
213
+ scene.add(ambientLight);
214
+ currentLights.push(ambientLight);
215
+
216
+ if (dirIntensity > 0) {
217
+ const directionalLight = new THREE.DirectionalLight(dirColor, dirIntensity);
218
+ directionalLight.position.copy(dirPosition);
219
+ directionalLight.castShadow = true;
220
+ directionalLight.shadow.mapSize.set(1024, 1024);
221
+ directionalLight.shadow.camera.near = 0.5;
222
+ directionalLight.shadow.camera.far = 50;
223
+ const shadowBounds = 20;
224
+ directionalLight.shadow.camera.left = -shadowBounds;
225
+ directionalLight.shadow.camera.right = shadowBounds;
226
+ directionalLight.shadow.camera.top = shadowBounds;
227
+ directionalLight.shadow.camera.bottom = -shadowBounds;
228
+ directionalLight.shadow.bias = -0.0005;
229
+ scene.add(directionalLight);
230
+ currentLights.push(directionalLight);
231
+ }
232
  }
233
 
234
+ function createDefaultZone() {
235
  const group = new THREE.Group();
236
+ group.add(createGround(MAT.dirt, 20));
237
  const boxGeo = new THREE.BoxGeometry(1, 1, 1);
238
+ const interactBox = createMesh(boxGeo, MAT.stone, {y: 0.5, x: 2, z: 2});
239
+ interactBox.userData = { isPickupable: true, itemName: "Mysterious Cube", description: "A plain stone cube."};
240
+ group.add(interactBox);
241
+ group.visible = false; // Start hidden
242
+ return { group, lighting: 'default', entryText: "You are in a default, featureless area.", cameraPos: {x:0, y:5, z:10}, targetPos: {x:0, y:1, z:0} };
243
  }
244
 
245
+ function createForestZone() {
246
  const group = new THREE.Group();
247
+ group.add(createGround(MAT.ground, 30));
248
  const trunkGeo = new THREE.CylinderGeometry(0.2, 0.3, 4, 8);
249
  const leafGeo = new THREE.SphereGeometry(1.5, 8, 6);
250
+ for(let i=0; i<20; i++) {
251
+ const x = (Math.random() - 0.5) * 28;
252
+ const z = (Math.random() - 0.5) * 28;
253
+ if(Math.sqrt(x*x+z*z) < 3) continue;
254
  const tree = new THREE.Group();
255
  const trunk = createMesh(trunkGeo, MAT.wood, {y: 2});
256
  const leaves = createMesh(leafGeo, MAT.leaf, {y: 4.5});
 
259
  tree.rotation.y = Math.random() * Math.PI * 2;
260
  group.add(tree);
261
  }
262
+ const swordGeo = new THREE.BoxGeometry(0.1, 1, 0.05);
263
+ const sword = createMesh(swordGeo, MAT.metal, {x: 3, y: 0.5, z: 4}, {z: Math.PI / 4});
264
+ sword.userData = { isPickupable: true, itemName: "Rusty Sword", description: "An old sword sticking out of the ground."};
265
+ group.add(sword);
266
+ group.visible = false;
267
+ return { group, lighting: 'forest', entryText: "You enter the dark forest. Sunlight struggles to get through.", cameraPos: {x:-5, y:6, z:12}, targetPos: {x:0, y:1, z:0} };
268
  }
269
 
270
+ function createCaveZone() {
271
  const group = new THREE.Group();
272
+ group.add(createGround(MAT.stone.clone().set({color: 0x555560}), 18));
273
+ const wallGeo = new THREE.SphereGeometry(12, 32, 16, 0, Math.PI*2, 0, Math.PI*0.7);
274
+ const walls = createMesh(wallGeo, MAT.stone, {y: 4});
 
 
 
 
 
 
 
 
275
  walls.material.side = THREE.BackSide;
276
  group.add(walls);
277
  const coneGeo = new THREE.ConeGeometry(0.2, 1.0, 8);
278
+ for(let i=0; i<15; i++){
279
+ const st = createMesh(coneGeo, MAT.stone, {x: (Math.random()-0.5)*16, y: 6 + Math.random()*2, z: (Math.random()-0.5)*16}, {x:Math.PI});
280
  group.add(st);
281
  }
282
+ const torchGeo = new THREE.CylinderGeometry(0.05, 0.05, 0.6, 6);
283
+ const torch = createMesh(torchGeo, MAT.wood, {x: -2, y: 0.3, z: 3}, {z: -Math.PI / 6});
284
+ torch.userData = { isPickupable: true, itemName: "Torch", description: "An unlit torch leans against the wall."};
285
+ group.add(torch);
286
+ group.visible = false;
287
+ return { group, lighting: 'cave', entryText: "It's dark and damp in here.", cameraPos: {x:0, y:4, z:8}, targetPos: {x:0, y:1, z:0} };
288
+ }
289
+
290
+
291
+ const locationData = {
292
+ 'start': { creator: createDefaultZone },
293
+ 'forest': { creator: createForestZone },
294
+ 'cave': { creator: createCaveZone }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
295
  };
296
 
297
+ const pageGraph = {
298
+ 'start': {
299
+ title: "The Crossroads",
300
+ options: [ { text: "Enter Forest", transitionTo: 'forest' } ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
301
  },
302
+ 'forest': {
303
+ title: "Dark Forest",
304
+ options: [ { text: "Return to Crossroads", transitionTo: 'start' }, { text: "Explore Deeper (TBC)", transitionTo: 'forest' }]
 
 
305
  },
306
+ 'cave': {
307
+ title: "Dim Cave",
308
+ options: [ { text: "Leave Cave", transitionTo: 'start' } ]
309
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
310
  };
311
 
312
  let gameState = {
313
+ currentLocationId: null,
314
  character: {
315
+ name: "Player",
316
+ stats: { hp: 20, maxHp: 20, xp: 0 },
317
  inventory: []
318
  }
319
  };
320
 
321
  function startGame() {
322
  const defaultChar = {
323
+ name: "Player",
324
+ stats: { hp: 20, maxHp: 20, xp: 0 },
325
  inventory: []
326
  };
327
  gameState = {
328
+ currentLocationId: null,
329
  character: JSON.parse(JSON.stringify(defaultChar))
330
  };
331
  console.log("Starting new game:", gameState);
332
+ transitionToLocation('start');
333
  }
334
 
335
+ function transitionToLocation(newLocationId) {
336
+ console.log(`Transitioning from ${gameState.currentLocationId} to ${newLocationId}`);
337
+ currentMessage = "";
338
+ currentPlacingItem = null; // Cancel placement on transition
339
+
340
+ if (gameState.currentLocationId && locationGroups[gameState.currentLocationId]) {
341
+ locationGroups[gameState.currentLocationId].visible = false;
 
 
 
 
 
 
 
342
  }
343
 
344
+ let newGroup;
345
+ let locationInfo;
346
+ if (locationGroups[newLocationId]) {
347
+ newGroup = locationGroups[newLocationId];
348
+ newGroup.visible = true;
349
+ locationInfo = locationData[newLocationId].cachedInfo;
350
+ } else {
351
+ if (locationData[newLocationId] && locationData[newLocationId].creator) {
352
+ locationInfo = locationData[newLocationId].creator();
353
+ newGroup = locationInfo.group;
354
+ locationGroups[newLocationId] = newGroup;
355
+ locationData[newLocationId].cachedInfo = locationInfo;
356
+ worldGroup.add(newGroup);
357
+ newGroup.visible = true;
358
+ } else {
359
+ console.error(`Location data or creator missing for ID: ${newLocationId}`);
360
+ locationInfo = locationData['start'].creator();
361
+ newGroup = locationInfo.group;
362
+ locationGroups['start'] = newGroup;
363
+ locationData['start'].cachedInfo = locationInfo;
364
+ worldGroup.add(newGroup);
365
+ newGroup.visible = true;
366
+ newLocationId = 'start';
367
+ currentMessage += `<p class="message message-failure">Error: Couldn't load target location, returned to start.</p>`;
368
+ }
369
+ }
370
 
371
+ gameState.currentLocationId = newLocationId;
372
+ setupLighting(locationInfo.lighting || 'default');
 
 
 
 
373
 
374
+ if (locationInfo.cameraPos) camera.position.set(locationInfo.cameraPos.x, locationInfo.cameraPos.y, locationInfo.cameraPos.z);
375
+ if (locationInfo.targetPos) controls.target.set(locationInfo.targetPos.x, locationInfo.targetPos.y, locationInfo.targetPos.z);
376
+ controls.update();
 
 
 
 
 
 
 
 
 
 
377
 
378
+ renderCurrentPageUI();
379
+ }
380
 
 
 
 
 
 
 
 
 
 
 
 
381
 
382
+ function renderCurrentPageUI() {
383
+ const page = pageGraph[gameState.currentLocationId];
384
+ const locationInfo = locationData[gameState.currentLocationId]?.cachedInfo;
385
+
386
+ if (!page) {
387
+ console.error(`No page graph data for location ${gameState.currentLocationId}`);
388
+ storyTitleElement.textContent = "Lost";
389
+ storyContentElement.innerHTML = currentMessage + "<p>You seem to be in an unknown place.</p>";
390
+ choicesElement.innerHTML = `<button class="choice-button" onclick="handleTransition({transitionTo: 'start'})">Return to Start</button>`;
391
+ updateStatsDisplay();
392
+ updateInventoryDisplay();
393
  return;
394
  }
395
 
396
+ storyTitleElement.textContent = page.title;
397
+ storyContentElement.innerHTML = currentMessage + (locationInfo?.entryText ? `<p>${locationInfo.entryText}</p>` : '');
398
+ choicesElement.innerHTML = '';
399
+
400
+ if (page.options && page.options.length > 0) {
401
+ page.options.forEach(option => {
402
+ const button = document.createElement('button');
403
+ button.classList.add('choice-button');
404
+ button.textContent = option.text;
405
+ button.onclick = () => handleTransition(option);
406
+ choicesElement.appendChild(button);
407
+ });
408
+ } else {
409
+ choicesElement.innerHTML = '<p><i>No transitions available from here yet.</i></p>';
410
+ }
411
 
412
+ updateStatsDisplay();
413
+ updateInventoryDisplay();
414
+ updateActionInfo();
415
+ }
 
 
 
416
 
417
+ function handleTransition(option) {
418
+ if (option.transitionTo) {
419
+ transitionToLocation(option.transitionTo);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
420
  } else {
421
+ console.warn("Choice option has no transitionTo property:", option);
 
 
 
 
422
  }
 
 
 
 
 
423
  }
424
+ window.handleTransition = handleTransition; // Make accessible from inline onclick
425
 
426
  function updateStatsDisplay() {
427
  const { hp, maxHp, xp } = gameState.character.stats;
 
437
  gameState.character.inventory.forEach(item => {
438
  const itemDef = itemsData[item] || { type: 'unknown', description: '???' };
439
  const itemClass = `item-${itemDef.type || 'unknown'}`;
440
+ const placingClass = (item === currentPlacingItem) ? ' placing' : '';
441
+ invHtml += `<span class="item-tag ${itemClass}${placingClass}" title="${itemDef.description}" data-itemname="${item}">${item}</span>`;
442
  });
443
  }
444
  inventoryElement.innerHTML = invHtml;
445
+
446
+ inventoryElement.querySelectorAll('.item-tag').forEach(tag => {
447
+ tag.onclick = () => { togglePlacementMode(tag.dataset.itemname); };
448
+ });
449
+ }
450
+
451
+ function updateActionInfo() {
452
+ if(currentPlacingItem) {
453
+ actionInfoElement.textContent = `Placing: ${currentPlacingItem} (Click ground)`;
454
+ actionInfoElement.style.display = 'block';
455
+ } else {
456
+ actionInfoElement.textContent = `Mode: Explore (Click items)`;
457
+ actionInfoElement.style.display = 'block'; // Keep it visible
458
+ }
459
+ }
460
+
461
+
462
+ function pickupItem() {
463
+ raycaster.setFromCamera(mouse, camera);
464
+ const currentGroup = locationGroups[gameState.currentLocationId];
465
+ if (!currentGroup) return;
466
+
467
+ const pickupables = [];
468
+ currentGroup.traverse(child => {
469
+ if (child.userData.isPickupable) {
470
+ pickupables.push(child);
471
+ }
472
+ });
473
+
474
+ const intersects = raycaster.intersectObjects(pickupables, false);
475
+
476
+ if (intersects.length > 0) {
477
+ const clickedObject = intersects[0].object;
478
+ const itemName = clickedObject.userData.itemName;
479
+
480
+ if (itemName && itemsData[itemName]) {
481
+ console.log(`Picked up: ${itemName}`);
482
+ currentMessage = `<p class="message message-item"><em>Picked up: ${itemName}</em></p>`;
483
+
484
+ if (!gameState.character.inventory.includes(itemName)) {
485
+ gameState.character.inventory.push(itemName);
486
+ } else {
487
+ currentMessage += `<p class="message message-info"><em>(You already had one)</em></p>`;
488
+ }
489
+
490
+ clickedObject.visible = false; // Hide it instead of removing, simpler state
491
+ clickedObject.userData.isPickupable = false; // Prevent re-picking
492
+
493
+ renderCurrentPageUI();
494
+ }
495
+ }
496
  }
497
 
498
+ function togglePlacementMode(itemName) {
499
+ if (currentPlacingItem === itemName) {
500
+ currentPlacingItem = null; // Cancel placement
501
+ console.log("Placement cancelled.");
502
+ } else {
503
+ currentPlacingItem = itemName;
504
+ console.log(`Ready to place: ${itemName}`);
505
+ }
506
+ updateInventoryDisplay(); // Update highlighting
507
+ updateActionInfo();
508
+ }
509
+
510
+ function placeItem() {
511
+ if (!currentPlacingItem) return;
512
+
513
+ raycaster.setFromCamera(mouse, camera);
514
+ const currentGroup = locationGroups[gameState.currentLocationId];
515
+ if (!currentGroup) return;
516
+
517
+ const grounds = [];
518
+ currentGroup.traverse(child => { if(child.userData.isGround) grounds.push(child); });
519
+
520
+ const intersects = raycaster.intersectObjects(grounds);
521
+
522
+ if (intersects.length > 0) {
523
+ const point = intersects[0].point;
524
+ const itemName = currentPlacingItem;
525
+ const itemDef = itemsData[itemName];
526
+
527
+ console.log(`Placing ${itemName} at ${point.x.toFixed(1)}, ${point.z.toFixed(1)}`);
528
+
529
+ const itemGeo = new THREE.BoxGeometry(0.5, 0.5, 0.5); // Simple representation
530
+ const itemMat = MAT.simple.clone();
531
+ if(itemDef.type === 'weapon') itemMat.color.setHex(0xaa4444);
532
+ else if(itemDef.type === 'consumable') itemMat.color.setHex(0xaa7744);
533
+ else itemMat.color.setHex(0x8888aa);
534
+
535
+ const placedMesh = createMesh(itemGeo, itemMat, {x: point.x, y: 0.25, z: point.z});
536
+ placedMesh.userData = { isPlacedItem: true, itemName: itemName };
537
+ currentGroup.add(placedMesh);
538
+
539
+ gameState.character.inventory = gameState.character.inventory.filter(i => i !== itemName);
540
+ currentMessage = `<p class="message message-item"><em>Placed ${itemName}.</em></p>`;
541
+ currentPlacingItem = null; // Exit placement mode
542
+ renderCurrentPageUI();
543
+
544
+ } else {
545
+ console.log("Placement click missed ground.");
546
+ currentMessage = `<p class="message message-failure"><em>Cannot place item there.</em></p>`;
547
+ currentPlacingItem = null; // Exit placement mode on miss
548
+ renderCurrentPageUI();
549
+ }
550
+ }
551
+
552
 
553
  document.addEventListener('DOMContentLoaded', () => {
554
+ console.log("DOM Ready - Initializing Persistent World Adventure.");
555
  try {
556
  initThreeJS();
557
  if (!scene || !camera || !renderer) throw new Error("Three.js failed to initialize.");
558
  startGame();
559
+ console.log("Game world initialized and started.");
560
  } catch (error) {
561
  console.error("Initialization failed:", error);
562
  storyTitleElement.textContent = "Initialization Error";
563
  storyContentElement.innerHTML = `<p style="color:red;">Failed to start game:</p><pre style="color:red; white-space: pre-wrap;">${error.stack || error}</pre>`;
 
564
  if(sceneContainer) sceneContainer.innerHTML = '<p style="color:red; padding: 20px;">3D Scene Failed</p>';
565
+ document.getElementById('stats-inventory-container').style.display = 'none';
566
+ document.getElementById('choices-container').style.display = 'none';
567
  }
568
  });
569