awacke1 commited on
Commit
37f9b86
·
verified ·
1 Parent(s): 5a10444

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +128 -533
index.html CHANGED
@@ -3,29 +3,25 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Procedural World - D&D Style</title>
7
  <style>
8
- /* --- Base Styles --- */
9
  body { font-family: 'Courier New', monospace; background-color: #111; color: #eee; margin: 0; padding: 0; overflow: hidden; display: flex; flex-direction: column; height: 100vh; }
10
  #game-container { display: flex; flex-grow: 1; overflow: hidden; }
11
  #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: default; }
12
  #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; }
13
  #scene-container canvas { display: block; }
14
-
15
- /* --- UI Elements --- */
16
  #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; }
17
  #story-content { margin-bottom: 25px; line-height: 1.7; flex-grow: 1; font-size: 1.1em; }
18
  #stats-inventory-container { margin-bottom: 25px; padding: 15px; border: 1px solid #444; border-radius: 4px; background-color: #333; font-size: 0.95em; }
19
  #stats-display, #inventory-display { margin-bottom: 10px; line-height: 1.8; }
20
- #stats-display span { 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); }
21
- #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); cursor: default; } /* No placement click */
22
  #stats-display strong, #inventory-display strong { color: #ccc; margin-right: 6px; }
23
  #inventory-display em { color: #888; font-style: normal; }
 
24
  .item-weapon { background-color: #663030; border-color: #994848;}
 
25
  .item-consumable { background-color: #664430; border-color: #996648;}
26
  .item-unknown { background-color: #555; border-color: #777;}
27
-
28
- /* --- Choices & Messages --- */
29
  #choices-container { margin-top: auto; padding-top: 20px; border-top: 1px solid #555; }
30
  #choices-container h3 { margin-top: 0; margin-bottom: 12px; color: #ccc; font-size: 1.1em; }
31
  #choices { display: flex; flex-direction: column; gap: 12px; }
@@ -34,14 +30,6 @@
34
  .choice-button:disabled { background-color: #404040; color: #777; cursor: not-allowed; border-color: #555; opacity: 0.7; }
35
  .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); border-color: #666; color: #aaa;}
36
  .message-failure { color: #f88; border-left-color: #a44; }
37
- .message-success { color: #8f8; border-left-color: #4a4; }
38
- .message-info { color: #aaa; border-left-color: #666; }
39
- .message-item { color: #8bf; border-left-color: #46a; }
40
- .message-combat { color: #f98; border-left-color: #c64; font-weight: bold;}
41
- .combat-button { background-color: #a33; border-color: #c66; color: #fff; font-weight: bold; text-align: center;}
42
- .combat-button:hover:not(:disabled) { background-color: #d44; border-color: #f88;}
43
-
44
- /* --- Action Info --- */
45
  #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: block; z-index: 10;}
46
  </style>
47
  </head>
@@ -52,10 +40,10 @@
52
  </div>
53
  <div id="ui-container">
54
  <h2 id="story-title">Initializing...</h2>
55
- <div id="story-content"><p>Loading world...</p></div>
56
  <div id="stats-inventory-container">
57
- <div id="stats-display">Loading Stats...</div>
58
- <div id="inventory-display">Inventory: ...</div>
59
  </div>
60
  <div id="choices-container">
61
  <h3>What will you do?</h3>
@@ -74,12 +62,9 @@
74
  <script type="module">
75
  import * as THREE from 'three';
76
  import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
77
- import { FontLoader } from 'three/addons/loaders/FontLoader.js';
78
- import { TextGeometry } from 'three/addons/geometries/TextGeometry.js';
79
 
80
  console.log("Script module execution started.");
81
 
82
- // --- DOM Elements ---
83
  const sceneContainer = document.getElementById('scene-container');
84
  const storyTitleElement = document.getElementById('story-title');
85
  const storyContentElement = document.getElementById('story-content');
@@ -88,111 +73,86 @@
88
  const inventoryElement = document.getElementById('inventory-display');
89
  const actionInfoElement = document.getElementById('action-info');
90
 
91
- // --- Core Three.js Variables ---
92
- let scene, camera, renderer, clock, controls, raycaster, mouse;
93
- let worldGroup = null; // Parent for all zone content
94
- let zoneGroups = {}; // Stores loaded zone data: { zoneId: { group, lighting, title, ... } }
95
- let currentLights = [];
96
- let threeFont = null; // For 3D text
97
 
98
- // --- Game State ---
99
- let gameState = {}; // Initialized in startGame
100
- let currentMessage = ""; // Accumulates messages for UI update
101
- let activeTimeouts = []; // Track animation timeouts
 
102
 
103
- // --- Materials ---
104
  const MAT = {
105
  stone: new THREE.MeshStandardMaterial({ color: 0x777788, roughness: 0.85 }),
106
- dark_stone: new THREE.MeshStandardMaterial({ color: 0x444455, roughness: 0.9 }),
107
  wood: new THREE.MeshStandardMaterial({ color: 0x9F6633, roughness: 0.75 }),
108
- dark_wood: new THREE.MeshStandardMaterial({ color: 0x6F4E2D, roughness: 0.8 }),
109
  leaf: new THREE.MeshStandardMaterial({ color: 0x3E9B4E, roughness: 0.6, side: THREE.DoubleSide }),
110
  ground: new THREE.MeshStandardMaterial({ color: 0x556B2F, roughness: 0.95 }),
111
  dirt: new THREE.MeshStandardMaterial({ color: 0x8B5E3C, roughness: 0.9 }),
112
  grass: new THREE.MeshStandardMaterial({ color: 0x4CB781, roughness: 0.85 }),
113
  water: new THREE.MeshStandardMaterial({ color: 0x4682B4, roughness: 0.3, transparent: true, opacity: 0.85 }),
114
- metal: new THREE.MeshStandardMaterial({ color: 0xaaaaaa, metalness: 0.8, roughness: 0.4 }),
115
  simple: new THREE.MeshStandardMaterial({ color: 0xaaaaaa, roughness: 0.8 }),
116
- text: new THREE.MeshBasicMaterial({ color: 0xffddaa }),
117
- // Zone specific themes
118
- ruins_stone: new THREE.MeshStandardMaterial({ color: 0x888a8f, roughness: 0.9 }),
119
- town_wood: new THREE.MeshStandardMaterial({ color: 0xae8a63, roughness: 0.7 }),
120
- town_roof: new THREE.MeshStandardMaterial({ color: 0x8b4513, roughness: 0.8 }),
121
  };
122
 
123
- // --- Game Data ---
 
124
  const itemsData = {
125
  "Rusty Sword": {type:"weapon", description:"Old but sharp.", baseDamage: 3},
126
  "Health Potion": {type:"consumable", description:"Restores 10 HP.", effect: { hpGain: 10 }},
127
  "Goblin Ear": {type:"quest", description:"A gruesome trophy."},
128
  "Cave Crystal": {type:"unknown", description:"A faintly glowing crystal shard."},
129
- "Ancient Coin": {type:"unknown", description:"A worn coin from a forgotten era."}
130
- };
131
-
132
- const enemyData = {
133
- 'goblin': { name: "Goblin", hp: 8, defense: 11, attackDamage: 2, xp: 15, drops: ["Goblin Ear", "Health Potion"] },
134
- 'skeleton': { name: "Skeleton", hp: 10, defense: 12, attackDamage: 3, xp: 20, drops: ["Ancient Coin"] },
135
- 'spider': { name: "Giant Spider", hp: 12, defense: 13, attackDamage: 3, xp: 25, drops: ["Cave Crystal"] }
136
  };
137
 
138
  const zoneCreators = {};
139
  const MAP_ROWS = 3;
140
  const MAP_COLS = 4;
141
 
142
- // --- Core Functions ---
143
-
144
  function initThreeJS() {
145
  console.log("initThreeJS started.");
146
- scene = new THREE.Scene();
147
- scene.background = new THREE.Color(0x1a1a1a);
148
- clock = new THREE.Clock();
149
- raycaster = new THREE.Raycaster();
150
- mouse = new THREE.Vector2();
151
- worldGroup = new THREE.Group();
152
- scene.add(worldGroup);
153
-
154
- const width = sceneContainer.clientWidth || 300;
155
- const height = sceneContainer.clientHeight || 200;
156
- camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 1000);
157
- camera.position.set(0, 8, 15); // Start slightly further back/higher
158
-
159
- renderer = new THREE.WebGLRenderer({ antialias: true });
160
- renderer.setSize(width, height);
161
- renderer.shadowMap.enabled = true;
162
- renderer.shadowMap.type = THREE.PCFSoftShadowMap;
163
- sceneContainer.appendChild(renderer.domElement);
164
-
165
- controls = new OrbitControls(camera, renderer.domElement);
166
- controls.enableDamping = true;
167
- controls.dampingFactor = 0.1;
168
- controls.target.set(0, 1, 0);
169
- controls.maxPolarAngle = Math.PI / 2 - 0.05;
170
- controls.minDistance = 3;
171
- controls.maxDistance = 50;
172
-
173
- window.addEventListener('resize', onWindowResize, false);
174
- renderer.domElement.addEventListener('click', onMouseClick, false);
175
- setTimeout(onWindowResize, 100);
176
- animate();
177
- console.log("initThreeJS finished successfully.");
178
- }
179
-
180
- function loadFontAndStart() {
181
- console.log("Loading font...");
182
- const loader = new FontLoader();
183
- loader.load('https://unpkg.com/[email protected]/examples/fonts/helvetiker_regular.typeface.json', function (font) {
184
- threeFont = font;
185
- console.log("Font loaded.");
186
- startGame();
187
- }, undefined, function (error) {
188
- console.error('Font loading failed:', error);
189
- storyTitleElement.textContent = "Font Load Error";
190
- storyContentElement.innerHTML = `<p class="message message-failure">Could not load required font. Combat text disabled.</p>`;
191
- startGame(); // Start anyway, but combat text won't work
192
- });
193
  }
194
 
195
  function onWindowResize() {
 
196
  if (!renderer || !camera || !sceneContainer) return;
197
  const width = sceneContainer.clientWidth || 300;
198
  const height = sceneContainer.clientHeight || 200;
@@ -200,31 +160,29 @@
200
  camera.aspect = width / height;
201
  camera.updateProjectionMatrix();
202
  renderer.setSize(width, height);
 
 
 
203
  }
204
  }
205
 
206
- function onMouseClick( event ) {
207
- // Currently only used for picking up items
208
- pickupItem();
209
- }
210
-
211
  function animate() {
 
212
  requestAnimationFrame(animate);
213
- const delta = clock.getDelta();
214
- const time = clock.getElapsedTime();
215
-
216
  controls.update();
 
217
  // Animate objects within the *currently visible* zone group
218
  if (gameState.currentZoneId && zoneGroups[gameState.currentZoneId]) {
219
  zoneGroups[gameState.currentZoneId].group.traverse(obj => {
220
- if (obj.userData.update) obj.userData.update(time, delta);
221
  });
222
  }
223
 
224
  if (renderer && scene && camera) renderer.render(scene, camera);
225
  }
226
 
227
- // --- Geometry/Mesh Helpers ---
228
  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}) {
229
  const mesh = new THREE.Mesh(geometry, material);
230
  mesh.position.set(pos.x, pos.y, pos.z);
@@ -243,30 +201,6 @@
243
  return ground;
244
  }
245
 
246
- // --- Procedural Structure Helpers ---
247
- function createTowerSegment(radius = 1, height = 2, material = MAT.stone) {
248
- const geo = new THREE.CylinderGeometry(radius, radius, height, 8);
249
- return createMesh(geo, material, {y: height/2});
250
- }
251
-
252
- function createWallSection(width = 4, height = 2.5, depth = 0.5, material = MAT.stone) {
253
- const geo = new THREE.BoxGeometry(width, height, depth);
254
- return createMesh(geo, material, {y: height/2});
255
- }
256
-
257
- function createSimpleHouse(width=3, height=2, depth=4, woodMat=MAT.town_wood, roofMat=MAT.town_roof) {
258
- const group = new THREE.Group();
259
- const wallGeo = new THREE.BoxGeometry(width, height, depth);
260
- const walls = createMesh(wallGeo, woodMat, {y: height/2});
261
- group.add(walls);
262
- const roofGeo = new THREE.ConeGeometry(Math.max(width, depth) * 0.7, height * 0.6, 4); // Pyramid roof
263
- const roof = createMesh(roofGeo, roofMat, {y: height + height*0.3});
264
- roof.rotation.y = Math.PI / 4; // Align edges
265
- group.add(roof);
266
- return group;
267
- }
268
-
269
- // --- Lighting ---
270
  function setupLighting(type = 'default') {
271
  console.log(`Setting up lighting for type: ${type}`);
272
  currentLights.forEach(light => {
@@ -275,14 +209,14 @@
275
  });
276
  currentLights = [];
277
 
278
- let ambientIntensity = 0.4;
279
- let dirIntensity = 0.9;
280
  let dirColor = 0xffffff;
281
- let dirPosition = new THREE.Vector3(10, 15, 8);
282
 
283
- if (type === 'forest') { ambientIntensity = 0.3; dirIntensity = 0.7; dirColor = 0xccffcc; dirPosition = new THREE.Vector3(5, 10, 5); }
284
- if (type === 'cave') { ambientIntensity = 0.15; dirIntensity = 0; } // Rely on point light
285
- if (type === 'ruins') { ambientIntensity = 0.35; dirIntensity = 0.6; dirColor = 0xaaaaff; dirPosition = new THREE.Vector3(-8, 12, -5); }
286
  if (type === 'town') { ambientIntensity = 0.5; dirIntensity = 1.0; dirColor = 0xffeedd; dirPosition = new THREE.Vector3(12, 18, 10); }
287
 
288
  const ambientLight = new THREE.AmbientLight(0xffffff, ambientIntensity);
@@ -295,7 +229,7 @@
295
  directionalLight.castShadow = true;
296
  directionalLight.shadow.mapSize.set(1024, 1024);
297
  directionalLight.shadow.camera.near = 0.5; directionalLight.shadow.camera.far = 50;
298
- const sb = 25; // Wider shadow area for larger zones
299
  directionalLight.shadow.camera.left = -sb; directionalLight.shadow.camera.right = sb;
300
  directionalLight.shadow.camera.top = sb; directionalLight.shadow.camera.bottom = -sb;
301
  directionalLight.shadow.bias = -0.0005;
@@ -303,178 +237,57 @@
303
  currentLights.push(directionalLight);
304
  }
305
  if (type === 'cave') {
306
- const ptLight = new THREE.PointLight(0xffaa55, 1.5, 15, 1.5); // Increased range/decay
307
  ptLight.position.set(0, 3, 0);
308
  ptLight.castShadow = true;
309
  ptLight.shadow.mapSize.set(512, 512);
310
- currentLights.push(ptLight); // Will be added to group later
311
  }
312
  console.log(`Lighting setup complete. Lights tracked: ${currentLights.length}`);
313
  }
314
 
315
- // --- Zone Creation Functions (Enhanced) ---
316
  function createFieldZone(zoneId) {
317
  console.log(`Creating zone: ${zoneId} (Field)`);
318
  const group = new THREE.Group();
319
- group.add(createGround(MAT.grass, 40)); // Larger zone
320
- const rockGeo = new THREE.IcosahedronGeometry(0.5 + Math.random()*0.8, 0);
321
- for(let i=0; i<8; i++) {
322
- const rock = createMesh(rockGeo, MAT.stone, {x: (Math.random()-0.5)*35, y:0.4, z: (Math.random()-0.5)*35});
323
- rock.rotation.set(Math.random(), Math.random(), Math.random());
 
324
  group.add(rock);
 
 
 
 
 
 
325
  }
326
- // Add a simple floating/bobbing animation to rocks
327
- group.children.filter(c=>c.geometry.type === 'IcosahedronGeometry').forEach(rock => {
328
- rock.userData.startY = rock.position.y;
329
- rock.userData.update = (time) => {
330
- rock.position.y = rock.userData.startY + Math.sin(time * 1.5 + rock.id * 0.5) * 0.1; // Bobbing
331
- };
332
- });
333
  group.visible = false;
334
- return { group, lighting: 'default', title: "Rolling Fields", entryText: `The wind whispers through the tall grass of zone ${zoneId}. Scattered boulders dot the landscape.`, options: [], zoneId: zoneId };
335
- }
336
-
337
- function createForestZone(zoneId) {
338
- console.log(`Creating zone: ${zoneId} (Forest)`);
339
- const group = new THREE.Group();
340
- group.add(createGround(MAT.ground, 40));
341
- const trunkGeo = new THREE.CylinderGeometry(0.2, 0.3, 4 + Math.random()*2, 8);
342
- const leafGeo = new THREE.SphereGeometry(1.5 + Math.random(), 8, 6);
343
- for(let i=0; i<35; i++) { // Denser forest
344
- const x = (Math.random() - 0.5) * 38;
345
- const z = (Math.random() - 0.5) * 38;
346
- if(Math.sqrt(x*x+z*z) < 1) continue;
347
- const tree = new THREE.Group();
348
- const trunkHeight = 4 + Math.random()*2;
349
- const trunk = createMesh(trunkGeo.clone().scale(1, trunkHeight/4, 1), MAT.wood, {y: trunkHeight/2}); // Scale trunk height
350
- const leaves = createMesh(leafGeo.clone().scale(1+Math.random()*0.3, 1+Math.random()*0.3, 1+Math.random()*0.3), MAT.leaf, {y: trunkHeight + 0.5});
351
- tree.add(trunk); tree.add(leaves);
352
- tree.position.set(x, 0, z);
353
- tree.rotation.y = Math.random() * Math.PI * 2;
354
- // Add subtle sway animation
355
- tree.userData.swaySpeed = 0.1 + Math.random() * 0.1;
356
- tree.userData.swayAmount = 0.005 + Math.random() * 0.005;
357
- tree.userData.update = (time) => {
358
- tree.rotation.z = Math.sin(time * tree.userData.swaySpeed + tree.position.x) * tree.userData.swayAmount;
359
- tree.rotation.x = Math.cos(time * tree.userData.swaySpeed * 0.7 + tree.position.z) * tree.userData.swayAmount;
360
- };
361
- group.add(tree);
362
- }
363
- group.visible = false;
364
- return { group, lighting: 'forest', title: "Deep Forest", entryText: `Ancient trees form a dense canopy overhead in ${zoneId}. Strange sounds echo.`, options: [{text: "Search for Goblin Tracks", action: "triggerCombat", enemy: "goblin"}], zoneId: zoneId };
365
  }
 
 
 
 
 
366
 
367
- function createCaveZone(zoneId) {
368
- console.log(`Creating zone: ${zoneId} (Cave)`);
369
- const group = new THREE.Group();
370
- group.add(createGround(MAT.stone.clone().set({color: 0x4a4a55}), 25)); // Darker floor
371
- const wallGeo = new THREE.SphereGeometry(15, 32, 16, 0, Math.PI*2, 0, Math.PI*0.8); // Larger cave
372
- const walls = createMesh(wallGeo, MAT.dark_stone, {y: 5});
373
- walls.material.side = THREE.BackSide;
374
- group.add(walls);
375
- const coneGeo = new THREE.ConeGeometry(0.1 + Math.random()*0.2, 0.8 + Math.random()*1.5, 8); // Stalactites/mites
376
- for(let i=0; i<25; i++){
377
- const x = (Math.random()-0.5)*22;
378
- const z = (Math.random()-0.5)*22;
379
- const yUp = 7 + Math.random()*3;
380
- const yDown = 0.5 + Math.random();
381
- if (Math.random() > 0.5) group.add(createMesh(coneGeo.clone(), MAT.dark_stone, {x:x, y:yUp, z:z}, {x:Math.PI})); // Stalactite
382
- if (Math.random() > 0.5) group.add(createMesh(coneGeo.clone(), MAT.dark_stone, {x:x, y:yDown, z:z})); // Stalagmite
383
- }
384
- // Add glowing crystals
385
- const crystalGeo = new THREE.IcosahedronGeometry(0.3 + Math.random()*0.2, 0);
386
- for(let i=0; i<5; i++) {
387
- const crystal = createMesh(crystalGeo, MAT.simple.clone().set({color:0xaaaaff, emissive: 0x4444ff, emissiveIntensity: 0.8}),
388
- {x: (Math.random()-0.5)*15, y:0.3 + Math.random()*2, z: (Math.random()-0.5)*15}
389
- );
390
- crystal.userData = { isPickupable: true, itemName: "Cave Crystal", description: "A faintly glowing crystal shard."};
391
- // Add pulsing animation
392
- crystal.userData.baseScale = 1.0;
393
- crystal.userData.update = (time) => {
394
- const scale = crystal.userData.baseScale + Math.sin(time * 2 + crystal.id) * 0.1;
395
- crystal.scale.set(scale, scale, scale);
396
- };
397
- group.add(crystal);
398
- }
399
- group.visible = false;
400
- return { group, lighting: 'cave', title: "Echoing Cave", entryText: `Water drips in the darkness of ${zoneId}. Strange crystals pulse with faint light.`, options: [{text: "Disturb Spider Nest", action: "triggerCombat", enemy: "spider"}], zoneId: zoneId };
401
- }
402
-
403
- function createRuinsZone(zoneId) {
404
- console.log(`Creating zone: ${zoneId} (Ruins)`);
405
- const group = new THREE.Group();
406
- group.add(createGround(MAT.dirt, 35));
407
- // Stacked walls/pillars
408
- for(let i=0; i<12; i++) {
409
- const stackHeight = Math.floor(1 + Math.random() * 3); // 1 to 3 segments high
410
- const basePos = {x: (Math.random()-0.5)*30, y:0, z: (Math.random()-0.5)*30};
411
- const rotY = Math.random() * Math.PI;
412
- for (let j=0; j<stackHeight; j++) {
413
- const width = 1 + Math.random();
414
- const height = 0.8 + Math.random() * 0.4;
415
- const depth = 1 + Math.random();
416
- const segmentGeo = new THREE.BoxGeometry(width, height, depth);
417
- const segment = createMesh(segmentGeo, MAT.ruins_stone,
418
- {x: basePos.x, y: basePos.y + height/2, z: basePos.z},
419
- {x:(Math.random()-0.5)*0.1, y: rotY + (Math.random()-0.5)*0.1, z:(Math.random()-0.5)*0.1} // Slight random tilt
420
- );
421
- group.add(segment);
422
- basePos.y += height; // Move up for next segment
423
- }
424
- }
425
- group.visible = false;
426
- return { group, lighting: 'ruins', title: "Ancient Ruins", entryText: `The wind howls through the skeletal remains of ${zoneId}. Broken stones lie everywhere.`, options: [{text: "Disturb Grave", action: "triggerCombat", enemy: "skeleton"}], zoneId: zoneId };
427
- }
428
-
429
- function createTownZone(zoneId) { // New Zone Type
430
- console.log(`Creating zone: ${zoneId} (Town)`);
431
- const group = new THREE.Group();
432
- group.add(createGround(MAT.dirt.clone().set({color: 0xaa8866}), 30)); // Packed earth/cobble look
433
- for(let i=0; i<10; i++) {
434
- const house = createSimpleHouse(
435
- 2 + Math.random()*2, // width
436
- 1.8 + Math.random()*0.5, // height
437
- 3 + Math.random()*2, // depth
438
- MAT.town_wood, MAT.town_roof
439
- );
440
- house.position.set((Math.random()-0.5)*25, 0, (Math.random()-0.5)*25);
441
- house.rotation.y = Math.floor(Math.random()*4) * Math.PI / 2 + (Math.random()-0.5)*0.1; // Align roughly to grid + tilt
442
- group.add(house);
443
- }
444
- // Add a central well (example of combining primitives)
445
- const wellBaseGeo = new THREE.CylinderGeometry(0.8, 0.8, 0.5, 12);
446
- const wellBase = createMesh(wellBaseGeo, MAT.stone, {y: 0.25});
447
- group.add(wellBase);
448
- const wellWallGeo = new THREE.CylinderGeometry(0.7, 0.6, 1.0, 12);
449
- const wellWall = createMesh(wellWallGeo, MAT.stone, {y: 0.5 + 0.5});
450
- group.add(wellWall);
451
-
452
- group.visible = false;
453
- return { group, lighting: 'town', title: "Small Settlement", entryText: `You arrive at the small settlement in ${zoneId}. Smoke curls from a few chimneys.`, options: [{text:"Talk to Villager (TBC)", action:"talk"}], zoneId: zoneId };
454
- }
455
 
456
  function getZoneId(row, col) { return `zone_${row}_${col}`; }
457
 
458
  function populateZoneCreators() {
459
  console.log("Populating zone creators...");
460
- // zoneCreators = {}; // Removed const error
461
  for (let r = 0; r < MAP_ROWS; r++) {
462
  for (let c = 0; c < MAP_COLS; c++) {
463
  const zoneId = getZoneId(r, c);
464
  let creatorFunc;
465
- // More varied pattern
466
- if (r === 0 && c === 0) creatorFunc = createCaveZone;
467
- else if (r === 0) creatorFunc = createForestZone;
468
- else if (r === 1 && c === 1) creatorFunc = createTownZone; // Add town
469
- else if (r === 1 && c === 3) creatorFunc = createRuinsZone;
470
- else if (r === 2 && c === 0) creatorFunc = createRuinsZone;
471
- else if (r === 2 && c === 3) creatorFunc = createCaveZone;
472
- else creatorFunc = createFieldZone; // Default to field
473
-
474
  zoneCreators[zoneId] = () => creatorFunc(zoneId);
475
  }
476
  }
477
- console.log(`Zone creators populated with ${Object.keys(zoneCreators).length} entries.`);
478
  }
479
 
480
  function getZoneNeighbors(zoneId) {
@@ -494,13 +307,12 @@
494
  console.log("startGame called.");
495
  const defaultChar = {
496
  name: "Player",
497
- stats: { hp: 20, maxHp: 20, xp: 0, strength: 10, dexterity: 10, constitution: 10, intelligence: 10, wisdom: 10, charisma: 10 }, // Added D&D stats
498
  inventory: []
499
  };
500
  gameState = {
501
  currentZoneId: null,
502
- character: JSON.parse(JSON.stringify(defaultChar)),
503
- combat: null // Reset combat state
504
  };
505
  populateZoneCreators();
506
  zoneGroups = {};
@@ -508,14 +320,13 @@
508
  worldGroup.remove(worldGroup.children[0]);
509
  }
510
  console.log("Starting new game state:", gameState);
511
- transitionToZone(getZoneId(1, 1)); // Start in center
512
  console.log("startGame finished.");
513
  }
514
 
515
  function transitionToZone(newZoneId) {
516
  console.log(`Attempting transition to ${newZoneId}`);
517
  currentMessage = "";
518
- gameState.combat = null; // End combat on zone change
519
 
520
  if (gameState.currentZoneId && zoneGroups[gameState.currentZoneId]) {
521
  console.log(`Hiding old zone: ${gameState.currentZoneId}`);
@@ -534,6 +345,7 @@
534
  if (creator) {
535
  try {
536
  zoneInfo = creator();
 
537
  zoneGroups[newZoneId] = zoneInfo;
538
  worldGroup.add(zoneInfo.group);
539
  zoneInfo.group.visible = true;
@@ -553,19 +365,21 @@
553
  gameState.currentZoneId = newZoneId;
554
  setupLighting(zoneInfo.lighting || 'default');
555
 
556
- // Add tracked point lights to the current zone group
557
  const currentZoneGroup = zoneInfo.group;
558
  currentLights.forEach(light => {
559
- if (light && light.isPointLight && zoneInfo.lighting === 'cave') { // Only add point lights to caves
560
  if(light.parent) light.parent.remove(light);
561
  currentZoneGroup.add(light);
562
  console.log("Added point light to cave group:", newZoneId);
563
- } else if (light && !light.isPointLight && !light.parent) { // Ensure ambient/directional are in main scene
564
- scene.add(light);
 
 
 
565
  }
566
  });
567
 
568
- camera.position.set(0, 8, 15); // Reset camera view slightly higher
569
  controls.target.set(0, 1, 0);
570
  controls.update();
571
 
@@ -575,27 +389,30 @@
575
 
576
 
577
  function renderCurrentPageUI() {
578
- console.log(`renderCurrentPageUI for zone: ${gameState.currentZoneId}`);
579
  const zoneInfo = zoneGroups[gameState.currentZoneId];
580
  const zoneId = gameState.currentZoneId;
581
 
582
  if (!storyTitleElement || !storyContentElement || !choicesElement || !statsElement || !inventoryElement || !actionInfoElement) {
583
  console.error("Crucial UI element missing!"); return;
584
  }
 
585
  if (!zoneInfo || !zoneInfo.group) {
586
- console.error(`No zone info/group for ${zoneId}`);
587
- storyTitleElement.textContent = "Error"; storyContentElement.innerHTML = currentMessage + "<p>Cannot render zone data.</p>";
 
588
  choicesElement.innerHTML = `<button class="choice-button" onclick="transitionToZoneWrapper('${getZoneId(1, 1)}')">Return to Start</button>`;
589
  updateStatsDisplay(); updateInventoryDisplay(); updateActionInfo(); return;
590
  }
 
591
 
592
  storyTitleElement.textContent = zoneInfo.title || "Unknown Zone";
593
  storyContentElement.innerHTML = currentMessage + (zoneInfo.entryText ? `<p>${zoneInfo.entryText}</p>` : '');
594
  choicesElement.innerHTML = '';
595
 
596
- // --- Navigation Buttons (Always Show, Disable if no neighbor) ---
597
  const neighbors = getZoneNeighbors(zoneId);
598
  const directions = {'north': 'North', 'south': 'South', 'east': 'East', 'west': 'West'};
 
599
  console.log("Adding neighbor buttons:", neighbors);
600
  for(const dir in directions) {
601
  const neighborId = neighbors[dir];
@@ -605,36 +422,25 @@
605
  if (neighborId) {
606
  button.addEventListener('click', () => transitionToZone(neighborId));
607
  } else {
608
- button.disabled = true; // Disable if no neighbor in this direction
609
  }
610
  choicesElement.appendChild(button);
 
611
  }
612
 
613
- // --- Zone Specific Action Buttons ---
614
  if (zoneInfo.options && zoneInfo.options.length > 0) {
615
  console.log("Adding zone specific options");
616
  zoneInfo.options.forEach(option => {
617
  const button = document.createElement('button');
618
  button.classList.add('choice-button');
619
- if (option.action === 'triggerCombat') { // Style combat buttons differently
620
- button.classList.add('combat-button');
621
- }
622
  button.textContent = option.text;
623
- button.addEventListener('click', () => handleZoneAction(option));
624
  choicesElement.appendChild(button);
 
625
  });
626
  }
627
 
628
- // --- Combat UI ---
629
- if (gameState.combat?.active) {
630
- choicesElement.innerHTML += `
631
- <div id="combat-ui">
632
- <p class="message message-combat">Combat vs ${gameState.combat.enemyName}! (Enemy HP: ${gameState.combat.enemyHp})</p>
633
- <button class="choice-button combat-button" onclick="handleCombatAction('attack')">Roll to Attack!</button>
634
- </div>`;
635
- }
636
-
637
- if (choicesElement.innerHTML === '') { // Check if empty after adding everything
638
  choicesElement.innerHTML = '<p><i>No exits or actions defined here yet.</i></p>';
639
  }
640
 
@@ -645,45 +451,19 @@
645
  console.log("renderCurrentPageUI finished.");
646
  }
647
 
648
- // Wrapper for potential inline use later (though addEventListener is preferred)
649
  function transitionToZoneWrapper(zoneId) { transitionToZone(zoneId); }
650
  window.transitionToZoneWrapper = transitionToZoneWrapper;
651
 
652
- function handleZoneAction(option) {
653
- console.log("Handling zone action:", option);
654
- if (option.action === 'triggerCombat' && option.enemy) {
655
- startCombat(option.enemy);
656
- } else {
657
- currentMessage = `<p class="message message-info">Action '${option.action || option.text}' not fully implemented yet.</p>`;
658
- renderCurrentPageUI(); // Re-render to show message
659
- }
660
- }
661
- window.handleZoneAction = handleZoneAction; // Make accessible
662
 
663
  function updateStatsDisplay() {
664
  if (!gameState.character || !statsElement) return;
665
- const { hp, maxHp, xp, strength, dexterity, constitution, intelligence, wisdom, charisma } = gameState.character.stats;
666
  const hpColor = hp / maxHp < 0.3 ? '#f88' : (hp / maxHp < 0.6 ? '#fd5' : '#8f8');
667
- // Calculate basic modifiers (D&D style)
668
- const strMod = Math.floor((strength - 10) / 2);
669
- const dexMod = Math.floor((dexterity - 10) / 2);
670
- const conMod = Math.floor((constitution - 10) / 2);
671
- const intMod = Math.floor((intelligence - 10) / 2);
672
- const wisMod = Math.floor((wisdom - 10) / 2);
673
- const chaMod = Math.floor((charisma - 10) / 2);
674
-
675
- statsElement.innerHTML = `<strong>Stats:</strong>
676
- <span style="color:${hpColor}">HP: ${hp}/${maxHp}</span> <span>XP: ${xp}</span><br>
677
- <span>Str: ${strength} (${strMod>=0?'+':''}${strMod})</span>
678
- <span>Dex: ${dexterity} (${dexMod>=0?'+':''}${dexMod})</span>
679
- <span>Con: ${constitution} (${conMod>=0?'+':''}${conMod})</span>
680
- <span>Int: ${intelligence} (${intMod>=0?'+':''}${intMod})</span>
681
- <span>Wis: ${wisdom} (${wisMod>=0?'+':''}${wisMod})</span>
682
- <span>Cha: ${charisma} (${chaMod>=0?'+':''}${chaMod})</span>`;
683
  }
684
 
685
  function updateInventoryDisplay() {
686
- if (!gameState.character || !inventoryElement) return;
687
  let invHtml = '<strong>Inventory:</strong> ';
688
  if (gameState.character.inventory.length === 0) {
689
  invHtml += '<em>Empty</em>';
@@ -695,207 +475,22 @@
695
  });
696
  }
697
  inventoryElement.innerHTML = invHtml;
698
- // No placement listeners needed now
699
  }
700
 
701
  function updateActionInfo() {
702
  if (!actionInfoElement || !gameState ) return;
703
- const mode = gameState.combat?.active ? "Combat" : "Explore";
704
- actionInfoElement.textContent = `Zone: ${gameState.currentZoneId || 'None'} | Mode: ${mode}`;
705
- }
706
-
707
- // --- Combat & Item Functions ---
708
- function startCombat(enemyTypeId) {
709
- const enemyBase = enemyData[enemyTypeId];
710
- if (!enemyBase) {
711
- console.error("Unknown enemy type:", enemyTypeId);
712
- currentMessage = `<p class="message message-failure">Error: Unknown enemy encountered!</p>`;
713
- renderCurrentPageUI();
714
- return;
715
- }
716
- gameState.combat = {
717
- active: true,
718
- enemyId: enemyTypeId,
719
- enemyName: enemyBase.name,
720
- enemyHp: enemyBase.hp,
721
- enemyMaxHp: enemyBase.hp, // Store max HP
722
- enemyDefense: enemyBase.defense,
723
- enemyDamage: enemyBase.attackDamage,
724
- enemyXp: enemyBase.xp,
725
- enemyDrops: enemyBase.drops || []
726
- };
727
- currentMessage = `<p class="message message-combat">A wild ${enemyBase.name} appears!</p>`;
728
- renderCurrentPageUI();
729
- }
730
-
731
- function handleCombatAction(action) {
732
- if (!gameState.combat?.active) return;
733
- currentMessage = ""; // Clear previous combat messages
734
-
735
- if (action === 'attack') {
736
- // --- Player Turn ---
737
- const roll = Math.floor(Math.random() * 20) + 1;
738
- const attackBonus = Math.floor((gameState.character.stats.strength - 10) / 2); // Simple STR mod
739
- const totalAttack = roll + attackBonus;
740
- const hit = totalAttack >= gameState.combat.enemyDefense;
741
-
742
- displayDiceRoll(roll, hit); // Show the dice roll visually
743
-
744
- if (hit) {
745
- const weapon = gameState.character.inventory.find(i => itemsData[i]?.type === 'weapon');
746
- const baseDamage = itemsData[weapon]?.baseDamage || 1; // Use weapon or 1
747
- const damageRoll = Math.max(1, Math.floor(Math.random() * baseDamage) + 1 + attackBonus); // Add STR mod to damage
748
- gameState.combat.enemyHp -= damageRoll;
749
- currentMessage += `<p class="message message-success">You hit the ${gameState.combat.enemyName} for ${damageRoll} damage! (Rolled ${totalAttack} vs AC ${gameState.combat.enemyDefense})</p>`;
750
- } else {
751
- currentMessage += `<p class="message message-failure">You missed the ${gameState.combat.enemyName}. (Rolled ${totalAttack} vs AC ${gameState.combat.enemyDefense})</p>`;
752
- }
753
-
754
- // --- Check Enemy Defeat ---
755
- if (gameState.combat.enemyHp <= 0) {
756
- currentMessage += `<p class="message message-success"><b>You defeated the ${gameState.combat.enemyName}!</b></p>`;
757
- gameState.character.stats.xp += gameState.combat.enemyXp;
758
- currentMessage += `<p class="message message-info"><em>Gained ${gameState.combat.enemyXp} XP.</em></p>`;
759
- if (gameState.combat.enemyDrops.length > 0) {
760
- const droppedItemName = gameState.combat.enemyDrops[Math.floor(Math.random() * gameState.combat.enemyDrops.length)];
761
- dropItemInZone(droppedItemName, new THREE.Vector3(Math.random()*2-1, 0, Math.random()*2-1));
762
- currentMessage += `<p class="message message-item"><em>The ${gameState.combat.enemyName} dropped a ${droppedItemName}!</em></p>`;
763
- }
764
- gameState.combat = null; // End combat
765
- renderCurrentPageUI(); // Update UI completely
766
- return; // Exit function after enemy defeat
767
- }
768
-
769
- // --- Enemy Turn (if player didn't defeat it) ---
770
- const enemyAttackRoll = Math.floor(Math.random() * 20) + 1 + 3; // Enemy bonus = +3
771
- const playerDefense = 10 + Math.floor((gameState.character.stats.dexterity - 10) / 2); // Simple AC
772
- if (enemyAttackRoll >= playerDefense) {
773
- const enemyDamageDealt = Math.max(1, Math.floor(Math.random() * gameState.combat.enemyDamage) + 1);
774
- gameState.character.stats.hp -= enemyDamageDealt;
775
- currentMessage += `<p class="message message-failure">The ${gameState.combat.enemyName} hits you for ${enemyDamageDealt} damage! (Rolled ${enemyAttackRoll} vs AC ${playerDefense})</p>`;
776
- if (gameState.character.stats.hp <= 0) {
777
- currentMessage += `<p class="message message-failure"><b>You have been defeated!</b></p>`;
778
- gameState.combat = null; // End combat
779
- // TODO: Implement proper game over / respawn
780
- gameState.character.stats.hp = 1; // Temp: reset to 1 hp
781
- transitionToZone(getZoneId(1,1)); // Go back to start
782
- return; // Exit after player defeat
783
- }
784
- } else {
785
- currentMessage += `<p class="message message-info">The ${gameState.combat.enemyName} misses you. (Rolled ${enemyAttackRoll} vs AC ${playerDefense})</p>`;
786
- }
787
- renderCurrentPageUI(); // Update UI after both turns
788
- }
789
- }
790
- window.handleCombatAction = handleCombatAction; // Make accessible
791
-
792
- function displayDiceRoll(result, success) {
793
- if (!threeFont) return; // Can't display if font isn't loaded
794
-
795
- // Clear previous dice rolls immediately
796
- activeTimeouts.forEach(timeoutId => clearTimeout(timeoutId));
797
- activeTimeouts = [];
798
- worldGroup.children.filter(c => c.userData.isDiceRoll).forEach(c => worldGroup.remove(c));
799
-
800
- const textGeo = new TextGeometry(result.toString(), { font: threeFont, size: 0.8, height: 0.1, curveSegments: 4 });
801
- textGeo.computeBoundingBox();
802
- const textWidth = textGeo.boundingBox.max.x - textGeo.boundingBox.min.x;
803
- const textMat = MAT.text.clone();
804
- textMat.color.setHex(success ? 0x88ff88 : 0xff8888);
805
-
806
- const textMesh = new THREE.Mesh(textGeo, textMat);
807
- textMesh.userData.isDiceRoll = true; // Mark for cleanup
808
-
809
- const distance = 4; // How far in front of camera
810
- const textPos = camera.position.clone().add(camera.getWorldDirection(new THREE.Vector3()).multiplyScalar(distance));
811
- textMesh.position.copy(textPos);
812
- textMesh.position.y += 1.5; // Higher up
813
- textMesh.position.x -= textWidth / 2;
814
- textMesh.quaternion.copy(camera.quaternion);
815
-
816
- worldGroup.add(textMesh); // Add to world group, not scene directly
817
-
818
- // Animate and remove
819
- const duration = 2.0; // seconds
820
- const startTime = clock.getElapsedTime();
821
- textMesh.userData.startTime = startTime;
822
- textMesh.userData.update = (time) => {
823
- const elapsed = time - textMesh.userData.startTime;
824
- if (elapsed >= duration) {
825
- if (textMesh.parent) textMesh.parent.remove(textMesh);
826
- delete textMesh.userData.update;
827
- } else {
828
- textMesh.position.y += 0.01; // Float up slowly
829
- textMesh.material.opacity = 1.0 - (elapsed / duration); // Fade out (requires transparent=true on material)
830
- // textMesh.scale.setScalar(1 + elapsed * 0.5); // Optionally grow
831
- }
832
- };
833
- }
834
-
835
- function dropItemInZone(itemName, positionOffset = new THREE.Vector3(0, 0, 0)) {
836
- const currentGroup = zoneGroups[gameState.currentZoneId]?.group;
837
- if (!currentGroup || !itemsData[itemName]) return;
838
-
839
- const itemDef = itemsData[itemName];
840
- const itemGeo = new THREE.BoxGeometry(0.4, 0.4, 0.4);
841
- const itemMat = MAT.simple.clone();
842
- if(itemDef.type === 'weapon') itemMat.color.setHex(0xcc6666);
843
- else if(itemDef.type === 'consumable') itemMat.color.setHex(0xcc9966);
844
- else if(itemDef.type === 'quest') itemMat.color.setHex(0xcccc66);
845
- else itemMat.color.setHex(0xaaaaee);
846
-
847
- const dropPos = new THREE.Vector3(positionOffset.x, 0.2, positionOffset.z);
848
- const droppedMesh = createMesh(itemGeo, itemMat, dropPos);
849
- droppedMesh.userData = { isPickupable: true, itemName: itemName, description: `Dropped ${itemName}` };
850
- currentGroup.add(droppedMesh);
851
- console.log(`Dropped ${itemName} in zone ${gameState.currentZoneId}`);
852
  }
853
 
854
- function pickupItem() {
855
- if (gameState.combat?.active) return; // No pickup during combat
856
-
857
- raycaster.setFromCamera(mouse, camera);
858
- const currentGroup = zoneGroups[gameState.currentZoneId]?.group;
859
- if (!currentGroup) return;
860
-
861
- const pickupables = [];
862
- currentGroup.traverseVisible(child => {
863
- if (child.userData.isPickupable) pickupables.push(child);
864
- });
865
-
866
- const intersects = raycaster.intersectObjects(pickupables, false);
867
-
868
- if (intersects.length > 0) {
869
- const clickedObject = intersects[0].object;
870
- const itemName = clickedObject.userData.itemName;
871
-
872
- if (itemName && itemsData[itemName]) {
873
- console.log(`Picked up: ${itemName}`);
874
- currentMessage = `<p class="message message-item"><em>Picked up: ${itemName}</em></p>`;
875
-
876
- if (!gameState.character.inventory.includes(itemName)) {
877
- gameState.character.inventory.push(itemName);
878
- } else {
879
- currentMessage += `<p class="message message-info"><em>(Cannot carry more ${itemName})</em></p>`;
880
- }
881
-
882
- clickedObject.visible = false;
883
- clickedObject.userData.isPickupable = false;
884
- // Consider removing object fully: clickedObject.parent.remove(clickedObject);
885
-
886
- renderCurrentPageUI();
887
- }
888
- }
889
- }
890
 
891
- // --- Initialization ---
892
  document.addEventListener('DOMContentLoaded', () => {
893
- console.log("DOM Ready - Initializing World Grid + Interaction.");
894
  try {
895
  initThreeJS();
896
  if (!scene || !camera || !renderer) throw new Error("Three.js failed to initialize.");
897
- loadFontAndStart(); // Load font, then start game
898
- console.log("Initialization sequence started.");
899
  } catch (error) {
900
  console.error("Initialization failed:", error);
901
  storyTitleElement.textContent = "Initialization Error";
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>World Grid Test (Animation Fix)</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: default; }
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
  #stats-display strong, #inventory-display strong { color: #ccc; margin-right: 6px; }
19
  #inventory-display em { color: #888; font-style: normal; }
20
+ .item-quest { background-color: #666030; border-color: #999048;}
21
  .item-weapon { background-color: #663030; border-color: #994848;}
22
+ .item-armor { background-color: #306630; border-color: #489948;}
23
  .item-consumable { background-color: #664430; border-color: #996648;}
24
  .item-unknown { background-color: #555; border-color: #777;}
 
 
25
  #choices-container { margin-top: auto; padding-top: 20px; border-top: 1px solid #555; }
26
  #choices-container h3 { margin-top: 0; margin-bottom: 12px; color: #ccc; font-size: 1.1em; }
27
  #choices { display: flex; flex-direction: column; gap: 12px; }
 
30
  .choice-button:disabled { background-color: #404040; color: #777; cursor: not-allowed; border-color: #555; opacity: 0.7; }
31
  .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); border-color: #666; color: #aaa;}
32
  .message-failure { color: #f88; border-left-color: #a44; }
 
 
 
 
 
 
 
 
33
  #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: block; z-index: 10;}
34
  </style>
35
  </head>
 
40
  </div>
41
  <div id="ui-container">
42
  <h2 id="story-title">Initializing...</h2>
43
+ <div id="story-content"><p>Loading assets...</p></div>
44
  <div id="stats-inventory-container">
45
+ <div id="stats-display">HP: ?/? | XP: ?</div>
46
+ <div id="inventory-display">Inventory: Empty</div>
47
  </div>
48
  <div id="choices-container">
49
  <h3>What will you do?</h3>
 
62
  <script type="module">
63
  import * as THREE from 'three';
64
  import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
 
 
65
 
66
  console.log("Script module execution started.");
67
 
 
68
  const sceneContainer = document.getElementById('scene-container');
69
  const storyTitleElement = document.getElementById('story-title');
70
  const storyContentElement = document.getElementById('story-content');
 
73
  const inventoryElement = document.getElementById('inventory-display');
74
  const actionInfoElement = document.getElementById('action-info');
75
 
76
+ console.log("DOM elements obtained.");
 
 
 
 
 
77
 
78
+ let scene, camera, renderer, clock, controls;
79
+ let worldGroup = null;
80
+ let zoneGroups = {};
81
+ let currentMessage = "";
82
+ let currentLights = [];
83
 
 
84
  const MAT = {
85
  stone: new THREE.MeshStandardMaterial({ color: 0x777788, roughness: 0.85 }),
 
86
  wood: new THREE.MeshStandardMaterial({ color: 0x9F6633, roughness: 0.75 }),
 
87
  leaf: new THREE.MeshStandardMaterial({ color: 0x3E9B4E, roughness: 0.6, side: THREE.DoubleSide }),
88
  ground: new THREE.MeshStandardMaterial({ color: 0x556B2F, roughness: 0.95 }),
89
  dirt: new THREE.MeshStandardMaterial({ color: 0x8B5E3C, roughness: 0.9 }),
90
  grass: new THREE.MeshStandardMaterial({ color: 0x4CB781, roughness: 0.85 }),
91
  water: new THREE.MeshStandardMaterial({ color: 0x4682B4, roughness: 0.3, transparent: true, opacity: 0.85 }),
 
92
  simple: new THREE.MeshStandardMaterial({ color: 0xaaaaaa, roughness: 0.8 }),
 
 
 
 
 
93
  };
94
 
95
+ let gameState = {};
96
+
97
  const itemsData = {
98
  "Rusty Sword": {type:"weapon", description:"Old but sharp.", baseDamage: 3},
99
  "Health Potion": {type:"consumable", description:"Restores 10 HP.", effect: { hpGain: 10 }},
100
  "Goblin Ear": {type:"quest", description:"A gruesome trophy."},
101
  "Cave Crystal": {type:"unknown", description:"A faintly glowing crystal shard."},
102
+ "Rock": {type:"unknown", description:"A simple rock."}
 
 
 
 
 
 
103
  };
104
 
105
  const zoneCreators = {};
106
  const MAP_ROWS = 3;
107
  const MAP_COLS = 4;
108
 
 
 
109
  function initThreeJS() {
110
  console.log("initThreeJS started.");
111
+ try {
112
+ scene = new THREE.Scene();
113
+ scene.background = new THREE.Color(0x1a1a1a);
114
+ clock = new THREE.Clock();
115
+ worldGroup = new THREE.Group();
116
+ scene.add(worldGroup);
117
+ console.log("Scene and worldGroup created.");
118
+
119
+ const width = sceneContainer.clientWidth || 300;
120
+ const height = sceneContainer.clientHeight || 200;
121
+ console.log(`Renderer dimensions: ${width}x${height}`);
122
+
123
+ camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 1000);
124
+ camera.position.set(0, 6, 12);
125
+ camera.lookAt(0, 1, 0);
126
+ console.log("Camera created.");
127
+
128
+ renderer = new THREE.WebGLRenderer({ antialias: true });
129
+ renderer.setSize(width, height);
130
+ renderer.shadowMap.enabled = true;
131
+ renderer.shadowMap.type = THREE.PCFSoftShadowMap;
132
+ sceneContainer.appendChild(renderer.domElement);
133
+ console.log("Renderer created and appended.");
134
+
135
+ controls = new OrbitControls(camera, renderer.domElement);
136
+ controls.enableDamping = true;
137
+ controls.dampingFactor = 0.1;
138
+ controls.target.set(0, 1, 0);
139
+ controls.maxPolarAngle = Math.PI / 2 - 0.05;
140
+ controls.minDistance = 3;
141
+ controls.maxDistance = 50;
142
+ console.log("OrbitControls initialized.");
143
+
144
+ window.addEventListener('resize', onWindowResize, false);
145
+ setTimeout(onWindowResize, 100);
146
+ animate();
147
+ console.log("initThreeJS finished successfully.");
148
+ } catch (error) {
149
+ console.error("Error during initThreeJS:", error);
150
+ throw error;
151
+ }
 
 
 
 
 
 
152
  }
153
 
154
  function onWindowResize() {
155
+ console.log("onWindowResize called.");
156
  if (!renderer || !camera || !sceneContainer) return;
157
  const width = sceneContainer.clientWidth || 300;
158
  const height = sceneContainer.clientHeight || 200;
 
160
  camera.aspect = width / height;
161
  camera.updateProjectionMatrix();
162
  renderer.setSize(width, height);
163
+ console.log(`Resized renderer to ${width}x${height}`);
164
+ } else {
165
+ console.warn("Skipping resize, zero dimensions detected.");
166
  }
167
  }
168
 
169
+ let frameCount = 0;
 
 
 
 
170
  function animate() {
171
+ frameCount++;
172
  requestAnimationFrame(animate);
 
 
 
173
  controls.update();
174
+
175
  // Animate objects within the *currently visible* zone group
176
  if (gameState.currentZoneId && zoneGroups[gameState.currentZoneId]) {
177
  zoneGroups[gameState.currentZoneId].group.traverse(obj => {
178
+ if (obj.userData.update) obj.userData.update(clock.elapsedTime, clock.getDelta()); // Pass time & delta
179
  });
180
  }
181
 
182
  if (renderer && scene && camera) renderer.render(scene, camera);
183
  }
184
 
185
+
186
  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}) {
187
  const mesh = new THREE.Mesh(geometry, material);
188
  mesh.position.set(pos.x, pos.y, pos.z);
 
201
  return ground;
202
  }
203
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
  function setupLighting(type = 'default') {
205
  console.log(`Setting up lighting for type: ${type}`);
206
  currentLights.forEach(light => {
 
209
  });
210
  currentLights = [];
211
 
212
+ let ambientIntensity = 0.6;
213
+ let dirIntensity = 1.0;
214
  let dirColor = 0xffffff;
215
+ let dirPosition = new THREE.Vector3(5, 10, 7);
216
 
217
+ if (type === 'forest') { ambientIntensity = 0.4; dirIntensity = 0.8; dirColor = 0xccffcc; dirPosition = new THREE.Vector3(5, 10, 5); }
218
+ if (type === 'cave') { ambientIntensity = 0.2; dirIntensity = 0; }
219
+ if (type === 'ruins') { ambientIntensity = 0.4; dirIntensity = 0.7; dirColor = 0xaaaaff; dirPosition = new THREE.Vector3(-8, 12, -5); }
220
  if (type === 'town') { ambientIntensity = 0.5; dirIntensity = 1.0; dirColor = 0xffeedd; dirPosition = new THREE.Vector3(12, 18, 10); }
221
 
222
  const ambientLight = new THREE.AmbientLight(0xffffff, ambientIntensity);
 
229
  directionalLight.castShadow = true;
230
  directionalLight.shadow.mapSize.set(1024, 1024);
231
  directionalLight.shadow.camera.near = 0.5; directionalLight.shadow.camera.far = 50;
232
+ const sb = 25;
233
  directionalLight.shadow.camera.left = -sb; directionalLight.shadow.camera.right = sb;
234
  directionalLight.shadow.camera.top = sb; directionalLight.shadow.camera.bottom = -sb;
235
  directionalLight.shadow.bias = -0.0005;
 
237
  currentLights.push(directionalLight);
238
  }
239
  if (type === 'cave') {
240
+ const ptLight = new THREE.PointLight(0xffaa55, 1.5, 15, 1.5);
241
  ptLight.position.set(0, 3, 0);
242
  ptLight.castShadow = true;
243
  ptLight.shadow.mapSize.set(512, 512);
244
+ currentLights.push(ptLight);
245
  }
246
  console.log(`Lighting setup complete. Lights tracked: ${currentLights.length}`);
247
  }
248
 
249
+ // --- Zone Creation Functions ---
250
  function createFieldZone(zoneId) {
251
  console.log(`Creating zone: ${zoneId} (Field)`);
252
  const group = new THREE.Group();
253
+ group.add(createGround(MAT.grass, 30));
254
+ const rockGeo = new THREE.IcosahedronGeometry(0.8, 0); // Simpler rock geometry
255
+ for(let i=0; i<8; i++) { // Loop index 'i' is available here
256
+ const rock = createMesh(rockGeo, MAT.stone, {x: (Math.random()-0.5)*25, y:0.4, z: (Math.random()-0.5)*25});
257
+ rock.rotation.set(Math.random()*0.5, Math.random() * Math.PI * 2, Math.random()*0.5); // Random rotation
258
+ rock.userData = { isPickupable: false, itemName: "Rock", description: "A field rock." }; // Initially not pickupable
259
  group.add(rock);
260
+ // Add animation using index 'i' for offset
261
+ rock.userData.startY = rock.position.y;
262
+ rock.userData.update = (time) => {
263
+ // Use loop index 'i' for phase offset - THIS IS THE FIX
264
+ rock.position.y = rock.userData.startY + Math.sin(time * 1.5 + i * 0.5) * 0.1;
265
+ };
266
  }
 
 
 
 
 
 
 
267
  group.visible = false;
268
+ return { group, lighting: 'default', title: "Open Field", entryText: `You are in ${zoneId}. It's grassy. Boulders bob gently.`, options: [], zoneId: zoneId };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
269
  }
270
+ // Add other create...Zone functions here (Forest, Cave, Ruins, Town etc.) - using the corrected animation logic if needed.
271
+ function createForestZone(zoneId) { return createFieldZone(zoneId); } // Placeholder uses Field for now
272
+ function createCaveZone(zoneId) { return createFieldZone(zoneId); } // Placeholder uses Field for now
273
+ function createRuinsZone(zoneId) { return createFieldZone(zoneId); } // Placeholder uses Field for now
274
+ function createTownZone(zoneId) { return createFieldZone(zoneId); } // Placeholder uses Field for now
275
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
276
 
277
  function getZoneId(row, col) { return `zone_${row}_${col}`; }
278
 
279
  function populateZoneCreators() {
280
  console.log("Populating zone creators...");
 
281
  for (let r = 0; r < MAP_ROWS; r++) {
282
  for (let c = 0; c < MAP_COLS; c++) {
283
  const zoneId = getZoneId(r, c);
284
  let creatorFunc;
285
+ // Force FieldZone for all to ensure stability first
286
+ creatorFunc = createFieldZone;
 
 
 
 
 
 
 
287
  zoneCreators[zoneId] = () => creatorFunc(zoneId);
288
  }
289
  }
290
+ console.log(`Zone creators populated with ${Object.keys(zoneCreators).length} entries (all FieldZone for debug).`);
291
  }
292
 
293
  function getZoneNeighbors(zoneId) {
 
307
  console.log("startGame called.");
308
  const defaultChar = {
309
  name: "Player",
310
+ stats: { hp: 20, maxHp: 20, xp: 0 },
311
  inventory: []
312
  };
313
  gameState = {
314
  currentZoneId: null,
315
+ character: JSON.parse(JSON.stringify(defaultChar))
 
316
  };
317
  populateZoneCreators();
318
  zoneGroups = {};
 
320
  worldGroup.remove(worldGroup.children[0]);
321
  }
322
  console.log("Starting new game state:", gameState);
323
+ transitionToZone(getZoneId(1, 1));
324
  console.log("startGame finished.");
325
  }
326
 
327
  function transitionToZone(newZoneId) {
328
  console.log(`Attempting transition to ${newZoneId}`);
329
  currentMessage = "";
 
330
 
331
  if (gameState.currentZoneId && zoneGroups[gameState.currentZoneId]) {
332
  console.log(`Hiding old zone: ${gameState.currentZoneId}`);
 
345
  if (creator) {
346
  try {
347
  zoneInfo = creator();
348
+ if (!zoneInfo || !zoneInfo.group) throw new Error("Creator function did not return valid zone info object with group.");
349
  zoneGroups[newZoneId] = zoneInfo;
350
  worldGroup.add(zoneInfo.group);
351
  zoneInfo.group.visible = true;
 
365
  gameState.currentZoneId = newZoneId;
366
  setupLighting(zoneInfo.lighting || 'default');
367
 
 
368
  const currentZoneGroup = zoneInfo.group;
369
  currentLights.forEach(light => {
370
+ if (light && light.isPointLight && zoneInfo.lighting === 'cave') {
371
  if(light.parent) light.parent.remove(light);
372
  currentZoneGroup.add(light);
373
  console.log("Added point light to cave group:", newZoneId);
374
+ } else if (light && !light.isPointLight && light.parent !== scene) {
375
+ if(light.parent) light.parent.remove(light); // Remove from other groups
376
+ scene.add(light); // Add to main scene
377
+ } else if (light && !light.isPointLight && !light.parent) {
378
+ scene.add(light); // Ensure it's in the scene if it wasn't attached
379
  }
380
  });
381
 
382
+ camera.position.set(0, 6, 12);
383
  controls.target.set(0, 1, 0);
384
  controls.update();
385
 
 
389
 
390
 
391
  function renderCurrentPageUI() {
392
+ console.log(`renderCurrentPageUI called for zone: ${gameState.currentZoneId}`);
393
  const zoneInfo = zoneGroups[gameState.currentZoneId];
394
  const zoneId = gameState.currentZoneId;
395
 
396
  if (!storyTitleElement || !storyContentElement || !choicesElement || !statsElement || !inventoryElement || !actionInfoElement) {
397
  console.error("Crucial UI element missing!"); return;
398
  }
399
+ // Added check for zoneInfo.group
400
  if (!zoneInfo || !zoneInfo.group) {
401
+ console.error(`No zone info or group loaded for ${zoneId}`);
402
+ storyTitleElement.textContent = "Error";
403
+ storyContentElement.innerHTML = currentMessage + "<p>Cannot render current zone data.</p>";
404
  choicesElement.innerHTML = `<button class="choice-button" onclick="transitionToZoneWrapper('${getZoneId(1, 1)}')">Return to Start</button>`;
405
  updateStatsDisplay(); updateInventoryDisplay(); updateActionInfo(); return;
406
  }
407
+ console.log(`Rendering UI for zone ${zoneId} with title "${zoneInfo.title}"`);
408
 
409
  storyTitleElement.textContent = zoneInfo.title || "Unknown Zone";
410
  storyContentElement.innerHTML = currentMessage + (zoneInfo.entryText ? `<p>${zoneInfo.entryText}</p>` : '');
411
  choicesElement.innerHTML = '';
412
 
 
413
  const neighbors = getZoneNeighbors(zoneId);
414
  const directions = {'north': 'North', 'south': 'South', 'east': 'East', 'west': 'West'};
415
+ let addedChoices = 0;
416
  console.log("Adding neighbor buttons:", neighbors);
417
  for(const dir in directions) {
418
  const neighborId = neighbors[dir];
 
422
  if (neighborId) {
423
  button.addEventListener('click', () => transitionToZone(neighborId));
424
  } else {
425
+ button.disabled = true;
426
  }
427
  choicesElement.appendChild(button);
428
+ addedChoices++; // Count even disabled buttons for layout
429
  }
430
 
 
431
  if (zoneInfo.options && zoneInfo.options.length > 0) {
432
  console.log("Adding zone specific options");
433
  zoneInfo.options.forEach(option => {
434
  const button = document.createElement('button');
435
  button.classList.add('choice-button');
 
 
 
436
  button.textContent = option.text;
437
+ button.addEventListener('click', () => console.log("Zone action TBC:", option.action));
438
  choicesElement.appendChild(button);
439
+ addedChoices++;
440
  });
441
  }
442
 
443
+ if (addedChoices === 0 && choicesElement.innerHTML === '') {
 
 
 
 
 
 
 
 
 
444
  choicesElement.innerHTML = '<p><i>No exits or actions defined here yet.</i></p>';
445
  }
446
 
 
451
  console.log("renderCurrentPageUI finished.");
452
  }
453
 
 
454
  function transitionToZoneWrapper(zoneId) { transitionToZone(zoneId); }
455
  window.transitionToZoneWrapper = transitionToZoneWrapper;
456
 
 
 
 
 
 
 
 
 
 
 
457
 
458
  function updateStatsDisplay() {
459
  if (!gameState.character || !statsElement) return;
460
+ const { hp, maxHp, xp } = gameState.character.stats;
461
  const hpColor = hp / maxHp < 0.3 ? '#f88' : (hp / maxHp < 0.6 ? '#fd5' : '#8f8');
462
+ statsElement.innerHTML = `<strong>Stats:</strong> <span style="color:${hpColor}">HP: ${hp}/${maxHp}</span> <span>XP: ${xp}</span>`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
463
  }
464
 
465
  function updateInventoryDisplay() {
466
+ if (!gameState.character || !inventoryElement) return;
467
  let invHtml = '<strong>Inventory:</strong> ';
468
  if (gameState.character.inventory.length === 0) {
469
  invHtml += '<em>Empty</em>';
 
475
  });
476
  }
477
  inventoryElement.innerHTML = invHtml;
 
478
  }
479
 
480
  function updateActionInfo() {
481
  if (!actionInfoElement || !gameState ) return;
482
+ actionInfoElement.textContent = `Zone: ${gameState.currentZoneId || 'None'} | Mode: Explore`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
483
  }
484
 
485
+ function pickupItem() { console.log("Pickup disabled."); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
486
 
 
487
  document.addEventListener('DOMContentLoaded', () => {
488
+ console.log("DOM Ready - Initializing World Grid Test.");
489
  try {
490
  initThreeJS();
491
  if (!scene || !camera || !renderer) throw new Error("Three.js failed to initialize.");
492
+ startGame(); // Start game directly
493
+ console.log("Game world initialization sequence complete.");
494
  } catch (error) {
495
  console.error("Initialization failed:", error);
496
  storyTitleElement.textContent = "Initialization Error";