awacke1 commited on
Commit
f22aa2f
·
verified ·
1 Parent(s): b3811d9

Create index.html

Browse files
Files changed (1) hide show
  1. index.html +733 -0
index.html ADDED
@@ -0,0 +1,733 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Procedural World Grid Combat</title>
7
+ <style>
8
+ /* Base styles from previous version */
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: crosshair; }
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
+ #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; }
15
+ #story-content { margin-bottom: 25px; line-height: 1.7; flex-grow: 1; font-size: 1.1em; }
16
+ #stats-inventory-container { margin-bottom: 25px; padding: 15px; border: 1px solid #444; border-radius: 4px; background-color: #333; font-size: 0.95em; }
17
+ #stats-display, #inventory-display { margin-bottom: 10px; line-height: 1.8; }
18
+ #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); }
19
+ #stats-display strong, #inventory-display strong { color: #ccc; margin-right: 6px; }
20
+ #inventory-display em { color: #888; font-style: normal; }
21
+ .item-quest { background-color: #666030; border-color: #999048;}
22
+ .item-weapon { background-color: #663030; border-color: #994848;}
23
+ .item-armor { background-color: #306630; border-color: #489948;}
24
+ .item-consumable { background-color: #664430; border-color: #996648;}
25
+ .item-unknown { background-color: #555; border-color: #777;}
26
+ #choices-container { margin-top: auto; padding-top: 20px; border-top: 1px solid #555; }
27
+ #choices-container h3 { margin-top: 0; margin-bottom: 12px; color: #ccc; font-size: 1.1em; }
28
+ #choices { display: flex; flex-direction: column; gap: 12px; }
29
+ .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; }
30
+ .choice-button:hover:not(:disabled) { background-color: #e0b050; color: #111; border-color: #c89040; box-shadow: 0 0 5px rgba(255, 200, 100, 0.5); }
31
+ .choice-button:disabled { background-color: #404040; color: #777; cursor: not-allowed; border-color: #555; opacity: 0.7; }
32
+ .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); }
33
+ .message-success { color: #8f8; border-left-color: #4a4; }
34
+ .message-failure { color: #f88; border-left-color: #a44; }
35
+ .message-info { color: #aaa; border-left-color: #666; }
36
+ .message-item { color: #8bf; border-left-color: #46a; }
37
+ .message-combat { color: #f98; border-left-color: #c64; font-weight: bold;}
38
+ #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;} /* Always visible */
39
+ #combat-ui { margin-top: 15px; padding-top: 15px; border-top: 1px dashed #666; }
40
+ .combat-button { background-color: #a33; border-color: #c66; color: #fff; font-weight: bold; text-align: center;}
41
+ .combat-button:hover:not(:disabled) { background-color: #d44; border-color: #f88;}
42
+ </style>
43
+ </head>
44
+ <body>
45
+ <div id="game-container">
46
+ <div id="scene-container">
47
+ <div id="action-info">Mode: Explore</div>
48
+ </div>
49
+ <div id="ui-container">
50
+ <h2 id="story-title">World Initializing...</h2>
51
+ <div id="story-content"><p>Establishing reality...</p></div>
52
+ <div id="stats-inventory-container">
53
+ <div id="stats-display"></div>
54
+ <div id="inventory-display"></div>
55
+ </div>
56
+ <div id="choices-container">
57
+ <h3>What will you do?</h3>
58
+ <div id="choices"></div>
59
+ </div>
60
+ </div>
61
+ </div>
62
+
63
+ <script type="importmap">
64
+ { "imports": {
65
+ "three": "https://unpkg.com/[email protected]/build/three.module.js",
66
+ "three/addons/": "https://unpkg.com/[email protected]/examples/jsm/"
67
+ }}
68
+ </script>
69
+
70
+ <script type="module">
71
+ import * as THREE from 'three';
72
+ import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
73
+ import { FontLoader } from 'three/addons/loaders/FontLoader.js';
74
+ import { TextGeometry } from 'three/addons/geometries/TextGeometry.js';
75
+
76
+ const sceneContainer = document.getElementById('scene-container');
77
+ const storyTitleElement = document.getElementById('story-title');
78
+ const storyContentElement = document.getElementById('story-content');
79
+ const choicesElement = document.getElementById('choices');
80
+ const statsElement = document.getElementById('stats-display');
81
+ const inventoryElement = document.getElementById('inventory-display');
82
+ const actionInfoElement = document.getElementById('action-info');
83
+
84
+ let scene, camera, renderer, clock, controls, raycaster, mouse;
85
+ let worldGroup = null;
86
+ let locationGroups = {}; // { zoneId: { group: THREE.Group, data: zoneData } }
87
+ let currentMessage = "";
88
+ let currentLights = [];
89
+ let threeFont = null; // To store loaded font
90
+
91
+ const MAT = {
92
+ stone: new THREE.MeshStandardMaterial({ color: 0x777788, roughness: 0.85, metalness: 0.1 }),
93
+ wood: new THREE.MeshStandardMaterial({ color: 0x9F6633, roughness: 0.75, metalness: 0 }),
94
+ leaf: new THREE.MeshStandardMaterial({ color: 0x3E9B4E, roughness: 0.6, metalness: 0, side: THREE.DoubleSide }),
95
+ ground: new THREE.MeshStandardMaterial({ color: 0x556B2F, roughness: 0.95, metalness: 0 }),
96
+ metal: new THREE.MeshStandardMaterial({ color: 0xaaaaaa, metalness: 0.85, roughness: 0.35 }),
97
+ dirt: new THREE.MeshStandardMaterial({ color: 0x8B5E3C, roughness: 0.9 }),
98
+ grass: new THREE.MeshStandardMaterial({ color: 0x4CB781, roughness: 0.85 }),
99
+ water: new THREE.MeshStandardMaterial({ color: 0x4682B4, roughness: 0.3, metalness: 0.2, transparent: true, opacity: 0.85 }),
100
+ error: new THREE.MeshStandardMaterial({ color: 0xff3300, roughness: 0.5, emissive: 0x551100 }),
101
+ gameOver: new THREE.MeshStandardMaterial({ color: 0xaa0000, roughness: 0.6, metalness: 0.2, emissive: 0x220000 }),
102
+ simple: new THREE.MeshStandardMaterial({ color: 0xaaaaaa, roughness: 0.8 }),
103
+ pickupHighlight: new THREE.MeshBasicMaterial({ color: 0xffff00, wireframe: true, depthTest: false }),
104
+ dice: new THREE.MeshStandardMaterial({ color: 0xeeeeff, roughness: 0.2, metalness: 0.1 }),
105
+ text: new THREE.MeshBasicMaterial({ color: 0xffddaa }),
106
+ };
107
+
108
+ const itemsData = {
109
+ "Rusty Sword": {type:"weapon", description:"Old but sharp.", baseDamage: 3},
110
+ "Health Potion": {type:"consumable", description:"Restores 10 HP.", effect: { hpGain: 10 }},
111
+ "Goblin Ear": {type:"quest", description:"A gruesome trophy."},
112
+ "Cave Crystal": {type:"unknown", description:"A faintly glowing crystal shard."}
113
+ };
114
+
115
+ const enemyData = {
116
+ 'goblin': { name: "Goblin", hp: 8, defense: 11, attackDamage: 2, xp: 15, drops: ["Goblin Ear", "Health Potion"] },
117
+ 'spider': { name: "Giant Spider", hp: 12, defense: 13, attackDamage: 3, xp: 25, drops: ["Cave Crystal"] }
118
+ };
119
+
120
+ // --- Game State Variable ---
121
+ let gameState = {}; // Initialized later
122
+
123
+ // --- Core Functions ---
124
+
125
+ function initThreeJS() {
126
+ scene = new THREE.Scene();
127
+ scene.background = new THREE.Color(0x1a1a1a);
128
+ clock = new THREE.Clock();
129
+ raycaster = new THREE.Raycaster();
130
+ mouse = new THREE.Vector2();
131
+ worldGroup = new THREE.Group();
132
+ scene.add(worldGroup);
133
+
134
+ const width = sceneContainer.clientWidth || 1;
135
+ const height = sceneContainer.clientHeight || 1;
136
+ camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 1000);
137
+ camera.position.set(0, 5, 10);
138
+
139
+ renderer = new THREE.WebGLRenderer({ antialias: true });
140
+ renderer.setSize(width, height);
141
+ renderer.shadowMap.enabled = true;
142
+ renderer.shadowMap.type = THREE.PCFSoftShadowMap;
143
+ renderer.toneMapping = THREE.ACESFilmicToneMapping;
144
+ renderer.outputColorSpace = THREE.SRGBColorSpace;
145
+ sceneContainer.appendChild(renderer.domElement);
146
+
147
+ controls = new OrbitControls(camera, renderer.domElement);
148
+ controls.enableDamping = true;
149
+ controls.dampingFactor = 0.1;
150
+ controls.target.set(0, 1, 0);
151
+ controls.maxPolarAngle = Math.PI / 2 - 0.05;
152
+
153
+ window.addEventListener('resize', onWindowResize, false);
154
+ renderer.domElement.addEventListener('mousemove', onMouseMove, false);
155
+ renderer.domElement.addEventListener('click', onMouseClick, false);
156
+ setTimeout(onWindowResize, 50);
157
+ animate();
158
+ }
159
+
160
+ function loadFontAndStart() {
161
+ const loader = new FontLoader();
162
+ loader.load('https://unpkg.com/[email protected]/examples/fonts/helvetiker_regular.typeface.json', function (font) {
163
+ threeFont = font;
164
+ console.log("Font loaded.");
165
+ startGame(); // Start game only after font is loaded
166
+ }, undefined, function (error) {
167
+ console.error('Font loading failed:', error);
168
+ storyTitleElement.textContent = "Font Load Error";
169
+ storyContentElement.innerHTML = `<p style="color:red;">Could not load required font.</p>`;
170
+ // Optionally try to start without font features or show a permanent error
171
+ });
172
+ }
173
+
174
+ function onWindowResize() {
175
+ if (!renderer || !camera || !sceneContainer) return;
176
+ const width = sceneContainer.clientWidth || 1;
177
+ const height = sceneContainer.clientHeight || 1;
178
+ camera.aspect = width / height;
179
+ camera.updateProjectionMatrix();
180
+ renderer.setSize(width, height);
181
+ }
182
+
183
+ function onMouseMove( event ) {
184
+ const rect = renderer.domElement.getBoundingClientRect();
185
+ mouse.x = ( (event.clientX - rect.left) / rect.width ) * 2 - 1;
186
+ mouse.y = - ( (event.clientY - rect.top) / rect.height ) * 2 + 1;
187
+ }
188
+
189
+ function onMouseClick( event ) {
190
+ // Click interaction is now primarily for picking up items
191
+ pickupItem();
192
+ }
193
+
194
+ function animate() {
195
+ requestAnimationFrame(animate);
196
+ const delta = clock.getDelta();
197
+ const time = clock.getElapsedTime();
198
+
199
+ controls.update();
200
+ worldGroup.traverse(obj => { if (obj.userData.update) obj.userData.update(time, delta); });
201
+
202
+ if (renderer && scene && camera) renderer.render(scene, camera);
203
+ }
204
+
205
+ 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}) {
206
+ const mesh = new THREE.Mesh(geometry, material);
207
+ mesh.position.set(pos.x, pos.y, pos.z);
208
+ mesh.rotation.set(rot.x, rot.y, rot.z);
209
+ mesh.scale.set(scale.x, scale.y, scale.z);
210
+ mesh.castShadow = true; mesh.receiveShadow = true;
211
+ return mesh;
212
+ }
213
+
214
+ function createGround(material = MAT.ground, size = 20) {
215
+ const geo = new THREE.PlaneGeometry(size, size);
216
+ const ground = new THREE.Mesh(geo, material);
217
+ ground.rotation.x = -Math.PI / 2; ground.position.y = 0;
218
+ ground.receiveShadow = true; ground.castShadow = false;
219
+ ground.userData.isGround = true;
220
+ return ground;
221
+ }
222
+
223
+ function setupLighting(type = 'default') {
224
+ currentLights.forEach(light => {
225
+ if (light) scene.remove(light);
226
+ });
227
+ currentLights = [];
228
+
229
+ let ambientIntensity = 0.4;
230
+ let dirIntensity = 0.9;
231
+ let dirColor = 0xffffff;
232
+ let dirPosition = new THREE.Vector3(10, 15, 8);
233
+
234
+ if (type === 'forest') {
235
+ ambientIntensity = 0.3; dirIntensity = 0.7; dirColor = 0xccffcc; dirPosition = new THREE.Vector3(5, 10, 5);
236
+ } else if (type === 'cave') {
237
+ ambientIntensity = 0.1; dirIntensity = 0;
238
+ const ptLight = new THREE.PointLight(0xffaa55, 1.5, 12, 1);
239
+ ptLight.position.set(0, 1.5, 1);
240
+ ptLight.castShadow = true;
241
+ ptLight.shadow.mapSize.set(512, 512);
242
+ scene.add(ptLight); currentLights.push(ptLight);
243
+ } else if (type === 'ruins') {
244
+ ambientIntensity = 0.25; dirIntensity = 0.6; dirColor = 0xaaaaff; dirPosition = new THREE.Vector3(-8, 12, -5);
245
+ } else if (type === 'gameover') {
246
+ ambientIntensity = 0.1; dirIntensity = 0.4; dirColor = 0xff6666;
247
+ }
248
+
249
+ const ambientLight = new THREE.AmbientLight(0xffffff, ambientIntensity);
250
+ scene.add(ambientLight);
251
+ currentLights.push(ambientLight);
252
+
253
+ if (dirIntensity > 0) {
254
+ const directionalLight = new THREE.DirectionalLight(dirColor, dirIntensity);
255
+ directionalLight.position.copy(dirPosition);
256
+ directionalLight.castShadow = true;
257
+ directionalLight.shadow.mapSize.set(1024, 1024);
258
+ directionalLight.shadow.camera.near = 0.5;
259
+ directionalLight.shadow.camera.far = 50;
260
+ const shadowBounds = 20;
261
+ directionalLight.shadow.camera.left = -shadowBounds;
262
+ directionalLight.shadow.camera.right = shadowBounds;
263
+ directionalLight.shadow.camera.top = shadowBounds;
264
+ directionalLight.shadow.camera.bottom = -shadowBounds;
265
+ directionalLight.shadow.bias = -0.0005;
266
+ scene.add(directionalLight);
267
+ currentLights.push(directionalLight);
268
+ }
269
+ }
270
+
271
+ // --- Zone Creation Functions ---
272
+ function createFieldZone(zoneId) {
273
+ const group = new THREE.Group();
274
+ group.add(createGround(MAT.grass, 30));
275
+ // Add some rocks or flowers
276
+ const rockGeo = new THREE.IcosahedronGeometry(0.5 + Math.random()*0.5, 0);
277
+ for(let i=0; i<5; i++) {
278
+ group.add(createMesh(rockGeo, MAT.stone, {x: (Math.random()-0.5)*25, y:0.3, z: (Math.random()-0.5)*25}));
279
+ }
280
+ group.visible = false;
281
+ return { group, lighting: 'default', entryText: "You stand in an open field.", zoneId: zoneId };
282
+ }
283
+
284
+ function createForestZone(zoneId) {
285
+ const group = new THREE.Group();
286
+ group.add(createGround(MAT.ground, 30));
287
+ const trunkGeo = new THREE.CylinderGeometry(0.2, 0.3, 4, 8);
288
+ const leafGeo = new THREE.SphereGeometry(1.5, 8, 6);
289
+ for(let i=0; i<25; i++) { // Denser forest
290
+ const x = (Math.random() - 0.5) * 28;
291
+ const z = (Math.random() - 0.5) * 28;
292
+ if(Math.sqrt(x*x+z*z) < 1) continue;
293
+ const tree = new THREE.Group();
294
+ const trunk = createMesh(trunkGeo, MAT.wood, {y: 2});
295
+ const leaves = createMesh(leafGeo, MAT.leaf, {y: 4.5});
296
+ tree.add(trunk); tree.add(leaves);
297
+ tree.position.set(x, 0, z);
298
+ tree.rotation.y = Math.random() * Math.PI * 2;
299
+ group.add(tree);
300
+ }
301
+ group.visible = false;
302
+ return { group, lighting: 'forest', entryText: "You are surrounded by trees.", zoneId: zoneId };
303
+ }
304
+
305
+ function createCaveZone(zoneId) {
306
+ const group = new THREE.Group();
307
+ group.add(createGround(MAT.stone.clone().set({color: 0x555560}), 18));
308
+ const wallGeo = new THREE.SphereGeometry(12, 32, 16, 0, Math.PI*2, 0, Math.PI*0.7);
309
+ const walls = createMesh(wallGeo, MAT.stone, {y: 4});
310
+ walls.material.side = THREE.BackSide;
311
+ group.add(walls);
312
+ const coneGeo = new THREE.ConeGeometry(0.2, 1.0, 8);
313
+ for(let i=0; i<15; i++){
314
+ 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});
315
+ group.add(st);
316
+ }
317
+ group.visible = false;
318
+ return { group, lighting: 'cave', entryText: "It's dark and damp in here.", zoneId: zoneId };
319
+ }
320
+
321
+ function createRuinsZone(zoneId) {
322
+ const group = new THREE.Group();
323
+ group.add(createGround(MAT.dirt, 25));
324
+ const wallGeo = new THREE.BoxGeometry(0.5, 2, 3);
325
+ for(let i=0; i<8; i++) {
326
+ const wall = createMesh(wallGeo, MAT.stone,
327
+ {x: (Math.random()-0.5)*20, y:1, z: (Math.random()-0.5)*20},
328
+ {y: Math.random() * Math.PI}
329
+ );
330
+ wall.scale.y = 0.5 + Math.random() * 0.8; // Vary height
331
+ wall.rotation.x = (Math.random()-0.5)*0.1; // Tilt slightly
332
+ wall.rotation.z = (Math.random()-0.5)*0.1;
333
+ group.add(wall);
334
+ }
335
+ group.visible = false;
336
+ return { group, lighting: 'ruins', entryText: "Crumbling walls stand sentinel.", zoneId: zoneId };
337
+ }
338
+
339
+ // --- Map Grid & Zone Definitions ---
340
+ const MAP_ROWS = 3;
341
+ const MAP_COLS = 4;
342
+ const zoneData = {}; // Will be populated: { "zone_0_0": { group, lighting, entryText, zoneId }, ... }
343
+
344
+ function getZoneId(row, col) { return `zone_${row}_${col}`; }
345
+
346
+ function populateZoneData() {
347
+ for (let r = 0; r < MAP_ROWS; r++) {
348
+ for (let c = 0; c < MAP_COLS; c++) {
349
+ const zoneId = getZoneId(r, c);
350
+ let zoneType;
351
+ // Simple pattern for variety, expand this logic
352
+ if (r === 0) zoneType = 'forest';
353
+ else if (r === 1 && c % 2 === 0) zoneType = 'field';
354
+ else if (r === 1 && c % 2 !== 0) zoneType = 'ruins';
355
+ else zoneType = 'cave';
356
+
357
+ let creatorFunc;
358
+ switch(zoneType) {
359
+ case 'forest': creatorFunc = createForestZone; break;
360
+ case 'cave': creatorFunc = createCaveZone; break;
361
+ case 'ruins': creatorFunc = createRuinsZone; break;
362
+ case 'field':
363
+ default: creatorFunc = createFieldZone; break;
364
+ }
365
+ locationData[zoneId] = { creator: () => creatorFunc(zoneId) }; // Store creator with ID
366
+ }
367
+ }
368
+ console.log("Zone data populated:", Object.keys(locationData));
369
+ }
370
+
371
+ function getZoneNeighbors(zoneId) {
372
+ const parts = zoneId.split('_');
373
+ const r = parseInt(parts[1]);
374
+ const c = parseInt(parts[2]);
375
+ const neighbors = {};
376
+ if (r > 0) neighbors.north = getZoneId(r - 1, c);
377
+ if (r < MAP_ROWS - 1) neighbors.south = getZoneId(r + 1, c);
378
+ if (c > 0) neighbors.west = getZoneId(r, c - 1);
379
+ if (c < MAP_COLS - 1) neighbors.east = getZoneId(r, c + 1);
380
+ return neighbors;
381
+ }
382
+
383
+ // --- Game State Initialization ---
384
+ function startGame() {
385
+ const defaultChar = {
386
+ name: "Player",
387
+ stats: { hp: 20, maxHp: 20, xp: 0, strength: 10, dexterity: 10 }, // Added stats
388
+ inventory: []
389
+ };
390
+ gameState = {
391
+ currentZoneId: null,
392
+ character: JSON.parse(JSON.stringify(defaultChar)),
393
+ combat: null // { active: true, enemyId: 'goblin', enemyHp: 8 }
394
+ };
395
+ populateZoneData(); // Create zone definitions
396
+ console.log("Starting new game:", gameState);
397
+ transitionToZone(getZoneId(1, 1)); // Start in a central zone
398
+ }
399
+
400
+ // --- Zone Transition ---
401
+ function transitionToZone(newZoneId) {
402
+ console.log(`Transitioning from ${gameState.currentZoneId} to ${newZoneId}`);
403
+ currentMessage = "";
404
+ gameState.combat = null; // End combat on zone transition
405
+
406
+ if (gameState.currentZoneId && locationGroups[gameState.currentZoneId]) {
407
+ locationGroups[gameState.currentZoneId].group.visible = false;
408
+ }
409
+
410
+ let zoneInfo;
411
+ if (locationGroups[newZoneId]) {
412
+ zoneInfo = locationGroups[newZoneId];
413
+ zoneInfo.group.visible = true;
414
+ } else {
415
+ if (locationData[newZoneId] && locationData[newZoneId].creator) {
416
+ zoneInfo = locationData[newZoneId].creator(); // Call the creator function
417
+ locationGroups[newZoneId] = zoneInfo; // Store { group, lighting, entryText, zoneId }
418
+ worldGroup.add(zoneInfo.group);
419
+ zoneInfo.group.visible = true;
420
+ } else {
421
+ console.error(`Zone data or creator missing for ID: ${newLocationId}, attempting fallback`);
422
+ const fallbackId = getZoneId(1, 1); // Fallback to center
423
+ if (!locationGroups[fallbackId]) {
424
+ locationGroups[fallbackId] = locationData[fallbackId].creator();
425
+ worldGroup.add(locationGroups[fallbackId].group);
426
+ }
427
+ zoneInfo = locationGroups[fallbackId];
428
+ zoneInfo.group.visible = true;
429
+ newZoneId = fallbackId;
430
+ currentMessage += `<p class="message message-failure">Error: Couldn't load target zone, returned to center.</p>`;
431
+ }
432
+ }
433
+
434
+ gameState.currentZoneId = newZoneId;
435
+ setupLighting(zoneInfo.lighting || 'default');
436
+
437
+ // Basic camera reset, could be customized per zone later
438
+ camera.position.set(0, 5, 10);
439
+ controls.target.set(0, 1, 0);
440
+ controls.update();
441
+
442
+ renderCurrentPageUI();
443
+ }
444
+
445
+ // --- UI Rendering ---
446
+ function renderCurrentPageUI() {
447
+ const zoneInfo = locationGroups[gameState.currentZoneId];
448
+ const zoneId = gameState.currentZoneId;
449
+
450
+ if (!zoneInfo) {
451
+ console.error(`No zone info for ${zoneId}`);
452
+ storyTitleElement.textContent = "Lost";
453
+ storyContentElement.innerHTML = currentMessage + "<p>You are in an undefined space.</p>";
454
+ choicesElement.innerHTML = `<button class="choice-button" onclick="handleTransition({ transitionTo: getZoneId(1, 1) })">Return to Center</button>`;
455
+ updateStatsDisplay();
456
+ updateInventoryDisplay();
457
+ updateActionInfo();
458
+ return;
459
+ }
460
+
461
+ storyTitleElement.textContent = zoneId.replace('_', ' ').replace('zone ', 'Zone '); // Simple title
462
+ storyContentElement.innerHTML = currentMessage + (zoneInfo.entryText ? `<p>${zoneInfo.entryText}</p>` : '');
463
+ choicesElement.innerHTML = '';
464
+
465
+ // Add Navigation Options
466
+ const neighbors = getZoneNeighbors(zoneId);
467
+ if (neighbors.north) choicesElement.innerHTML += `<button class="choice-button" onclick="handleTransition({ transitionTo: '${neighbors.north}' })">Go North</button>`;
468
+ if (neighbors.south) choicesElement.innerHTML += `<button class="choice-button" onclick="handleTransition({ transitionTo: '${neighbors.south}' })">Go South</button>`;
469
+ if (neighbors.east) choicesElement.innerHTML += `<button class="choice-button" onclick="handleTransition({ transitionTo: '${neighbors.east}' })">Go East</button>`;
470
+ if (neighbors.west) choicesElement.innerHTML += `<button class="choice-button" onclick="handleTransition({ transitionTo: '${neighbors.west}' })">Go West</button>`;
471
+
472
+ // Add Zone Specific Options / Combat Trigger Example
473
+ if (zoneId === getZoneId(0, 1)) { // Example: Forest zone triggers combat
474
+ choicesElement.innerHTML += `<button class="choice-button combat-button" onclick="startCombat('goblin')">Investigate Rustling</button>`;
475
+ }
476
+ if (zoneId === getZoneId(2, 2)) { // Example: Cave zone triggers combat
477
+ choicesElement.innerHTML += `<button class="choice-button combat-button" onclick="startCombat('spider')">Disturb Webs</button>`;
478
+ }
479
+
480
+ // Add Combat UI if active
481
+ if (gameState.combat?.active) {
482
+ choicesElement.innerHTML += `
483
+ <div id="combat-ui">
484
+ <p class="message message-combat">Combat vs ${gameState.combat.enemyName}! (Enemy HP: ${gameState.combat.enemyHp})</p>
485
+ <button class="choice-button combat-button" onclick="handleCombatAction('attack')">Roll to Attack!</button>
486
+ </div>`;
487
+ }
488
+
489
+ updateStatsDisplay();
490
+ updateInventoryDisplay();
491
+ updateActionInfo();
492
+ }
493
+
494
+ // Make functions globally accessible for inline onclick handlers
495
+ window.handleTransition = (option) => { if (option.transitionTo) transitionToZone(option.transitionTo); };
496
+ window.startCombat = startCombat;
497
+ window.handleCombatAction = handleCombatAction;
498
+ window.getZoneId = getZoneId; // Make helper accessible if needed
499
+
500
+ function updateStatsDisplay() {
501
+ const { hp, maxHp, xp } = gameState.character.stats;
502
+ const hpColor = hp / maxHp < 0.3 ? '#f88' : (hp / maxHp < 0.6 ? '#fd5' : '#8f8');
503
+ statsElement.innerHTML = `<strong>Stats:</strong> <span style="color:${hpColor}">HP: ${hp}/${maxHp}</span> <span>XP: ${xp}</span>`;
504
+ // Could add Str/Dex here too if desired
505
+ }
506
+
507
+ function updateInventoryDisplay() {
508
+ let invHtml = '<strong>Inventory:</strong> ';
509
+ if (gameState.character.inventory.length === 0) {
510
+ invHtml += '<em>Empty</em>';
511
+ } else {
512
+ gameState.character.inventory.forEach(item => {
513
+ const itemDef = itemsData[item] || { type: 'unknown', description: '???' };
514
+ const itemClass = `item-${itemDef.type || 'unknown'}`;
515
+ invHtml += `<span class="item-tag ${itemClass}" title="${itemDef.description}">${item}</span>`;
516
+ });
517
+ }
518
+ inventoryElement.innerHTML = invHtml;
519
+ // Removed placement click handler
520
+ }
521
+
522
+ function updateActionInfo() {
523
+ actionInfoElement.textContent = gameState.combat?.active ? `Mode: Combat` : `Mode: Explore (Click items)`;
524
+ actionInfoElement.style.display = 'block';
525
+ }
526
+
527
+ // --- Combat & Item Functions ---
528
+
529
+ function startCombat(enemyTypeId) {
530
+ const enemyBase = enemyData[enemyTypeId];
531
+ if (!enemyBase) {
532
+ console.error("Unknown enemy type:", enemyTypeId);
533
+ currentMessage = `<p class="message message-failure">Error: Unknown enemy encountered!</p>`;
534
+ renderCurrentPageUI();
535
+ return;
536
+ }
537
+ gameState.combat = {
538
+ active: true,
539
+ enemyId: enemyTypeId,
540
+ enemyName: enemyBase.name,
541
+ enemyHp: enemyBase.hp,
542
+ enemyDefense: enemyBase.defense,
543
+ enemyDamage: enemyBase.attackDamage,
544
+ enemyXp: enemyBase.xp,
545
+ enemyDrops: enemyBase.drops || []
546
+ };
547
+ currentMessage = `<p class="message message-combat">A wild ${enemyBase.name} appears!</p>`;
548
+ renderCurrentPageUI();
549
+ }
550
+
551
+ function handleCombatAction(action) {
552
+ if (!gameState.combat?.active) return;
553
+
554
+ if (action === 'attack') {
555
+ // Player attacks
556
+ const roll = Math.floor(Math.random() * 20) + 1;
557
+ const attackBonus = Math.floor((gameState.character.stats.strength - 10) / 2); // Simple STR mod
558
+ const totalAttack = roll + attackBonus;
559
+ const hit = totalAttack >= gameState.combat.enemyDefense;
560
+
561
+ displayDiceRoll(roll, hit); // Show the dice roll visually
562
+
563
+ if (hit) {
564
+ const baseDamage = itemsData[gameState.character.inventory.find(i => itemsData[i]?.type === 'weapon')]?.baseDamage || 1; // Basic weapon damage or 1
565
+ const damageRoll = Math.max(1, Math.floor(Math.random() * baseDamage) + 1); // Roll 1dX
566
+ gameState.combat.enemyHp -= damageRoll;
567
+ currentMessage = `<p class="message message-success">You hit the ${gameState.combat.enemyName} for ${damageRoll} damage! (Rolled ${totalAttack} vs DC ${gameState.combat.enemyDefense})</p>`;
568
+ } else {
569
+ currentMessage = `<p class="message message-failure">You missed the ${gameState.combat.enemyName}. (Rolled ${totalAttack} vs DC ${gameState.combat.enemyDefense})</p>`;
570
+ }
571
+
572
+ if (gameState.combat.enemyHp <= 0) {
573
+ // Enemy defeated
574
+ currentMessage += `<p class="message message-success"><b>You defeated the ${gameState.combat.enemyName}!</b></p>`;
575
+ gameState.character.stats.xp += gameState.combat.enemyXp;
576
+ currentMessage += `<p class="message message-info"><em>Gained ${gameState.combat.enemyXp} XP.</em></p>`;
577
+ // Handle drops
578
+ if (gameState.combat.enemyDrops.length > 0) {
579
+ const droppedItemName = gameState.combat.enemyDrops[Math.floor(Math.random() * gameState.combat.enemyDrops.length)];
580
+ dropItemInZone(droppedItemName, new THREE.Vector3(Math.random()*2-1, 0, Math.random()*2-1)); // Drop near center
581
+ currentMessage += `<p class="message message-item"><em>The ${gameState.combat.enemyName} dropped a ${droppedItemName}!</em></p>`;
582
+ }
583
+ gameState.combat = null; // End combat
584
+ renderCurrentPageUI();
585
+ } else {
586
+ // Enemy attacks
587
+ const enemyAttackRoll = Math.floor(Math.random() * 20) + 1 + 2; // Simple enemy attack bonus
588
+ const playerDefense = 10 + Math.floor((gameState.character.stats.dexterity - 10) / 2); // Simple AC
589
+ if (enemyAttackRoll >= playerDefense) {
590
+ const enemyDamageDealt = Math.max(1, Math.floor(Math.random() * gameState.combat.enemyDamage) + 1);
591
+ gameState.character.stats.hp -= enemyDamageDealt;
592
+ currentMessage += `<p class="message message-failure">The ${gameState.combat.enemyName} hits you for ${enemyDamageDealt} damage!</p>`;
593
+ if (gameState.character.stats.hp <= 0) {
594
+ currentMessage += `<p class="message message-failure"><b>You have been defeated!</b></p>`;
595
+ gameState.combat = null; // End combat
596
+ transitionToZone(getZoneId(1,1)); // Go back to start/center on death? Or game over page
597
+ // TODO: Implement proper game over state
598
+ }
599
+ } else {
600
+ currentMessage += `<p class="message message-info">The ${gameState.combat.enemyName} misses you.</p>`;
601
+ }
602
+ renderCurrentPageUI(); // Update UI after both attacks (if player survived)
603
+ }
604
+ }
605
+ }
606
+
607
+ function displayDiceRoll(result, success) {
608
+ if (!threeFont) {
609
+ console.warn("Font not loaded, cannot display dice roll text.");
610
+ return;
611
+ }
612
+
613
+ const textGeo = new TextGeometry(result.toString(), {
614
+ font: threeFont,
615
+ size: 0.8,
616
+ height: 0.1,
617
+ curveSegments: 4,
618
+ });
619
+ textGeo.computeBoundingBox();
620
+ const textWidth = textGeo.boundingBox.max.x - textGeo.boundingBox.min.x;
621
+ const textMat = MAT.text.clone();
622
+ textMat.color.setHex(success ? 0x88ff88 : 0xff8888); // Green for success, red for fail
623
+
624
+ const textMesh = new THREE.Mesh(textGeo, textMat);
625
+ // Position above center, facing camera
626
+ const distance = 5;
627
+ const textPos = camera.position.clone().add(camera.getWorldDirection(new THREE.Vector3()).multiplyScalar(distance));
628
+ textMesh.position.copy(textPos);
629
+ textMesh.position.y += 1; // Slightly higher
630
+ textMesh.position.x -= textWidth / 2; // Center horizontally approx
631
+ textMesh.quaternion.copy(camera.quaternion); // Face camera
632
+
633
+ scene.add(textMesh);
634
+
635
+ // Fade out and remove
636
+ let opacity = 1;
637
+ const fadeDuration = 1.5; // seconds
638
+ const fadeStartTime = clock.getElapsedTime();
639
+
640
+ textMesh.userData.update = (time) => {
641
+ const elapsed = time - fadeStartTime;
642
+ if (elapsed >= fadeDuration) {
643
+ scene.remove(textMesh);
644
+ delete textMesh.userData.update;
645
+ } else {
646
+ opacity = 1.0 - (elapsed / fadeDuration);
647
+ // textMat.opacity = opacity; // Requires material.transparent = true; Might look odd for basic mesh
648
+ textMesh.scale.setScalar(1 + (elapsed / fadeDuration)); // Grow slightly as it fades
649
+ textMesh.position.y += 0.01; // Float up
650
+ }
651
+ };
652
+ }
653
+
654
+ function dropItemInZone(itemName, positionOffset = new THREE.Vector3(0, 0, 0)) {
655
+ const currentGroup = locationGroups[gameState.currentZoneId]?.group;
656
+ if (!currentGroup || !itemsData[itemName]) return;
657
+
658
+ const itemDef = itemsData[itemName];
659
+ const itemGeo = new THREE.BoxGeometry(0.4, 0.4, 0.4); // Simple box for dropped item
660
+ const itemMat = MAT.simple.clone();
661
+ if(itemDef.type === 'weapon') itemMat.color.setHex(0xcc6666);
662
+ else if(itemDef.type === 'consumable') itemMat.color.setHex(0xcc9966);
663
+ else if(itemDef.type === 'quest') itemMat.color.setHex(0xcccc66);
664
+ else itemMat.color.setHex(0xaaaaee);
665
+
666
+ const dropPos = new THREE.Vector3(positionOffset.x, 0.2, positionOffset.z); // Drop slightly above ground near offset
667
+ const droppedMesh = createMesh(itemGeo, itemMat, dropPos);
668
+ droppedMesh.userData = { isPickupable: true, itemName: itemName, description: `Dropped ${itemName}` };
669
+ currentGroup.add(droppedMesh);
670
+ console.log(`Dropped ${itemName} in zone ${gameState.currentZoneId}`);
671
+ }
672
+
673
+
674
+ function pickupItem() {
675
+ if (gameState.combat?.active) return; // No pickup during combat
676
+
677
+ raycaster.setFromCamera(mouse, camera);
678
+ const currentGroup = locationGroups[gameState.currentZoneId]?.group;
679
+ if (!currentGroup) return;
680
+
681
+ const pickupables = [];
682
+ currentGroup.traverseVisible(child => {
683
+ if (child.userData.isPickupable) {
684
+ pickupables.push(child);
685
+ }
686
+ });
687
+
688
+ const intersects = raycaster.intersectObjects(pickupables, false);
689
+
690
+ if (intersects.length > 0) {
691
+ const clickedObject = intersects[0].object;
692
+ const itemName = clickedObject.userData.itemName;
693
+
694
+ if (itemName && itemsData[itemName]) {
695
+ console.log(`Picked up: ${itemName}`);
696
+ currentMessage = `<p class="message message-item"><em>Picked up: ${itemName}</em></p>`;
697
+
698
+ if (!gameState.character.inventory.includes(itemName)) {
699
+ gameState.character.inventory.push(itemName);
700
+ } else {
701
+ // Handle stacking or just ignore if already have? For now, ignore.
702
+ currentMessage += `<p class="message message-info"><em>(You already have one/can't carry more)</em></p>`;
703
+ }
704
+
705
+ clickedObject.visible = false;
706
+ clickedObject.userData.isPickupable = false; // Mark as picked up
707
+
708
+ renderCurrentPageUI(); // Update inventory and clear message
709
+ }
710
+ }
711
+ }
712
+
713
+ // --- Initialization ---
714
+ document.addEventListener('DOMContentLoaded', () => {
715
+ console.log("DOM Ready - Initializing Persistent World Adventure.");
716
+ try {
717
+ initThreeJS();
718
+ if (!scene || !camera || !renderer) throw new Error("Three.js failed to initialize.");
719
+ loadFontAndStart(); // Load font, then start game
720
+ console.log("Initialization sequence started.");
721
+ } catch (error) {
722
+ console.error("Initialization failed:", error);
723
+ storyTitleElement.textContent = "Initialization Error";
724
+ storyContentElement.innerHTML = `<p style="color:red;">Failed to start game:</p><pre style="color:red; white-space: pre-wrap;">${error.stack || error}</pre>`;
725
+ if(sceneContainer) sceneContainer.innerHTML = '<p style="color:red; padding: 20px;">3D Scene Failed</p>';
726
+ document.getElementById('stats-inventory-container').style.display = 'none';
727
+ document.getElementById('choices-container').style.display = 'none';
728
+ }
729
+ });
730
+
731
+ </script>
732
+ </body>
733
+ </html>