awacke1 commited on
Commit
908fc8e
·
verified ·
1 Parent(s): 362ecb9

Create game.js

Browse files
Files changed (1) hide show
  1. game.js +634 -0
game.js ADDED
@@ -0,0 +1,634 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as THREE from 'three';
2
+ // import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; // Keep commented unless needed
3
+
4
+ // --- DOM Elements ---
5
+ const sceneContainer = document.getElementById('scene-container');
6
+ const storyTitleElement = document.getElementById('story-title');
7
+ const storyContentElement = document.getElementById('story-content');
8
+ const choicesElement = document.getElementById('choices');
9
+ const statsElement = document.getElementById('stats-display');
10
+ const inventoryElement = document.getElementById('inventory-display');
11
+
12
+ // --- Config ---
13
+ const ROOM_SIZE = 10; // Size of each map grid cell in 3D space
14
+ const WALL_HEIGHT = 4;
15
+ const WALL_THICKNESS = 0.5;
16
+ const CAMERA_HEIGHT = 15; // How high the overhead camera is
17
+
18
+ // --- Three.js Setup ---
19
+ let scene, camera, renderer;
20
+ let mapGroup; // A group to hold all map related objects
21
+ // let controls; // Optional OrbitControls
22
+
23
+ function initThreeJS() {
24
+ scene = new THREE.Scene();
25
+ scene.background = new THREE.Color(0x111111); // Darker background for contrast
26
+ scene.fog = new THREE.Fog(0x111111, CAMERA_HEIGHT * 1.5, CAMERA_HEIGHT * 3); // Add fog for depth
27
+
28
+ camera = new THREE.PerspectiveCamera(60, sceneContainer.clientWidth / sceneContainer.clientHeight, 0.1, 1000);
29
+ // Set initial overhead position (will be updated)
30
+ camera.position.set(0, CAMERA_HEIGHT, 0);
31
+ camera.lookAt(0, 0, 0);
32
+
33
+ renderer = new THREE.WebGLRenderer({ antialias: true });
34
+ renderer.setSize(sceneContainer.clientWidth, sceneContainer.clientHeight);
35
+ renderer.shadowMap.enabled = true; // Enable shadows if using lights that cast them
36
+ sceneContainer.appendChild(renderer.domElement);
37
+
38
+ // Lighting
39
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.5); // Softer ambient
40
+ scene.add(ambientLight);
41
+ // Light source directly above the center (acts like a player flashlight)
42
+ const playerLight = new THREE.PointLight(0xffffff, 1.5, ROOM_SIZE * 3); // Intensity, Distance
43
+ playerLight.position.set(0, CAMERA_HEIGHT - 2, 0); // Position slightly below camera
44
+ scene.add(playerLight);
45
+ // We will move this light with the camera/player later
46
+
47
+ // Map Group
48
+ mapGroup = new THREE.Group();
49
+ scene.add(mapGroup);
50
+
51
+ // Optional Controls (for debugging)
52
+ // controls = new OrbitControls(camera, renderer.domElement);
53
+ // controls.enableDamping = true;
54
+ // controls.target.set(0, 0, 0); // Ensure controls start looking at origin
55
+
56
+ window.addEventListener('resize', onWindowResize, false);
57
+ animate();
58
+ }
59
+
60
+ function onWindowResize() {
61
+ if (!renderer || !camera) return;
62
+ camera.aspect = sceneContainer.clientWidth / sceneContainer.clientHeight;
63
+ camera.updateProjectionMatrix();
64
+ renderer.setSize(sceneContainer.clientWidth, sceneContainer.clientHeight);
65
+ }
66
+
67
+ function animate() {
68
+ requestAnimationFrame(animate);
69
+ // if (controls) controls.update();
70
+ if (renderer && scene && camera) {
71
+ renderer.render(scene, camera);
72
+ }
73
+ }
74
+
75
+ // --- Primitive Creation Functions ---
76
+ const materials = {
77
+ floor_forest: new THREE.MeshLambertMaterial({ color: 0x228B22 }), // Green
78
+ floor_cave: new THREE.MeshLambertMaterial({ color: 0x696969 }), // Grey
79
+ floor_room: new THREE.MeshLambertMaterial({ color: 0xaaaaaa }), // Light Grey
80
+ floor_city: new THREE.MeshLambertMaterial({ color: 0xdeb887 }), // Tan/Pavement
81
+ wall: new THREE.MeshLambertMaterial({ color: 0x888888 }),
82
+ door: new THREE.MeshLambertMaterial({ color: 0xcd853f }), // Peru (brownish)
83
+ river: new THREE.MeshBasicMaterial({ color: 0x4682B4, transparent: true, opacity: 0.7 }), // Steel Blue
84
+ unexplored: new THREE.MeshBasicMaterial({ color: 0x555555, wireframe: true }),
85
+ visited_marker: new THREE.MeshBasicMaterial({ color: 0xaaaaaa, wireframe: true, transparent: true, opacity: 0.5 }), // Faint marker for visited
86
+ current_marker: new THREE.MeshBasicMaterial({ color: 0xffff00, wireframe: true }), // Yellow marker
87
+ };
88
+
89
+ const geometries = {
90
+ floor: new THREE.PlaneGeometry(ROOM_SIZE, ROOM_SIZE),
91
+ wall: new THREE.BoxGeometry(ROOM_SIZE, WALL_HEIGHT, WALL_THICKNESS),
92
+ wall_side: new THREE.BoxGeometry(WALL_THICKNESS, WALL_HEIGHT, ROOM_SIZE),
93
+ marker: new THREE.BoxGeometry(ROOM_SIZE * 0.8, 0.1, ROOM_SIZE * 0.8), // Flat marker
94
+ river: new THREE.PlaneGeometry(ROOM_SIZE * 0.3, ROOM_SIZE), // Thin river strip
95
+ };
96
+
97
+ function createFloor(type = 'room', x = 0, z = 0) {
98
+ const material = materials[`floor_${type}`] || materials.floor_room;
99
+ const floor = new THREE.Mesh(geometries.floor, material);
100
+ floor.rotation.x = -Math.PI / 2; // Rotate to be flat
101
+ floor.position.set(x * ROOM_SIZE, 0, z * ROOM_SIZE);
102
+ floor.receiveShadow = true;
103
+ return floor;
104
+ }
105
+
106
+ function createWall(direction, x = 0, z = 0) {
107
+ let wall;
108
+ const base_x = x * ROOM_SIZE;
109
+ const base_z = z * ROOM_SIZE;
110
+ const half_room = ROOM_SIZE / 2;
111
+ const wall_y = WALL_HEIGHT / 2; // Position center of wall at half height
112
+
113
+ switch (direction) {
114
+ case 'north':
115
+ wall = new THREE.Mesh(geometries.wall, materials.wall);
116
+ wall.position.set(base_x, wall_y, base_z - half_room);
117
+ break;
118
+ case 'south':
119
+ wall = new THREE.Mesh(geometries.wall, materials.wall);
120
+ wall.position.set(base_x, wall_y, base_z + half_room);
121
+ break;
122
+ case 'east':
123
+ wall = new THREE.Mesh(geometries.wall_side, materials.wall);
124
+ wall.position.set(base_x + half_room, wall_y, base_z);
125
+ break;
126
+ case 'west':
127
+ wall = new THREE.Mesh(geometries.wall_side, materials.wall);
128
+ wall.position.set(base_x - half_room, wall_y, base_z);
129
+ break;
130
+ default: return null;
131
+ }
132
+ wall.castShadow = true;
133
+ wall.receiveShadow = true;
134
+ return wall;
135
+ }
136
+
137
+ function createFeature(feature, x = 0, z = 0) {
138
+ const base_x = x * ROOM_SIZE;
139
+ const base_z = z * ROOM_SIZE;
140
+ let mesh = null;
141
+ // Example: Add a river feature
142
+ if (feature === 'river') {
143
+ mesh = new THREE.Mesh(geometries.river, materials.river);
144
+ mesh.rotation.x = -Math.PI / 2;
145
+ mesh.position.set(base_x, 0.05, base_z); // Slightly above floor
146
+ }
147
+ // Add more features: 'door_north', 'chest', 'tree' etc.
148
+ // For doors, you might modify/omit wall segments instead of adding an object
149
+ return mesh;
150
+ }
151
+
152
+
153
+ function createMarker(type, x = 0, z = 0) {
154
+ let material;
155
+ switch (type) {
156
+ case 'current': material = materials.current_marker; break;
157
+ case 'visited': material = materials.visited_marker; break;
158
+ case 'unexplored': material = materials.unexplored; break;
159
+ default: return null;
160
+ }
161
+ const marker = new THREE.Mesh(geometries.marker, material);
162
+ marker.position.set(x * ROOM_SIZE, 0.1, z * ROOM_SIZE); // Slightly above floor
163
+ return marker;
164
+ }
165
+
166
+ // --- Game Data (ADD MAP COORDINATES and TYPE/FEATURES) ---
167
+ const gameData = {
168
+ // Page ID: { title, content, options, illustration(optional), mapX, mapY, type, features? }
169
+ "1": {
170
+ title: "The Beginning", content: "<p>...</p>", illustration: "city-gates",
171
+ mapX: 0, mapY: 0, type: 'city', features: ['door_north'], // Example feature
172
+ options: [
173
+ { text: "Visit the local weaponsmith", next: 2 },
174
+ { text: "Seek wisdom at the temple", next: 3 },
175
+ { text: "Meet the resistance leader", next: 4 }
176
+ ]
177
+ },
178
+ "2": { // Weaponsmith - let's place it near the start
179
+ title: "The Weaponsmith", content: "<p>...</p>", illustration: "weaponsmith",
180
+ mapX: -1, mapY: 0, type: 'room', features: ['door_east'],
181
+ options: [
182
+ { text: "Take Flaming Sword", next: 5, addItem: "Flaming Sword" },
183
+ // ... other weapon choices ...
184
+ { text: "Return to city square", next: 1 }
185
+ ]
186
+ },
187
+ "3": { // Temple
188
+ title: "The Ancient Temple", content: "<p>...</p>", illustration: "temple",
189
+ mapX: 1, mapY: 0, type: 'room', features: ['door_west'],
190
+ options: [
191
+ { text: "Learn Healing Light", next: 5, addItem: "Healing Light Spell" },
192
+ // ... other spell choices ...
193
+ { text: "Return to city square", next: 1 }
194
+ ]
195
+ },
196
+ "4": { // Resistance Tavern
197
+ title: "The Resistance Leader", content: "<p>...</p>", illustration: "resistance-meeting",
198
+ mapX: 0, mapY: -1, type: 'room', features: ['door_north'],
199
+ options: [
200
+ { text: "Take Secret Tunnel Map", next: 5, addItem: "Secret Tunnel Map" },
201
+ // ... other item choices ...
202
+ { text: "Return to city square", next: 1 }
203
+ ]
204
+ },
205
+ "5": { // Start of Forest Path (North of City)
206
+ title: "The Journey Begins", content: "<p>You leave Silverhold...</p>", illustration: "shadowwood-forest",
207
+ mapX: 0, mapY: 1, type: 'forest', features: ['path_north', 'path_east', 'path_west', 'door_south'],
208
+ options: [
209
+ { text: "Take the main road (North)", next: 6 },
210
+ { text: "Follow the river path (East)", next: 7 },
211
+ { text: "Brave the ruins shortcut (West)", next: 8 },
212
+ { text: "Return to the City Gate", next: 1} // Link back
213
+ ]
214
+ },
215
+ "6": { // Forest Path North
216
+ title: "Ambush!", content: "<p>Scouts jump out!</p>", illustration: "road-ambush",
217
+ mapX: 0, mapY: 2, type: 'forest', features: ['path_north', 'path_south'],
218
+ // Challenge will determine next step (9 or 10)
219
+ challenge: { title: "Escape Ambush", stat: "courage", difficulty: 5, success: 9, failure: 10 },
220
+ options: [] // Options determined by challenge
221
+ },
222
+ "7": { // River Path East
223
+ title: "Misty River", content: "<p>A spirit appears...</p>", illustration: "river-spirit",
224
+ mapX: 1, mapY: 1, type: 'forest', features: ['river', 'path_west'],
225
+ challenge: { title: "River Riddle", stat: "wisdom", difficulty: 6, success: 11, failure: 12 },
226
+ options: []
227
+ },
228
+ "8": { // Ruins Path West
229
+ title: "Forgotten Ruins", content: "<p>Whispers echo...</p>", illustration: "ancient-ruins",
230
+ mapX: -1, mapY: 1, type: 'ruins', features: ['path_east'],
231
+ challenge: { title: "Navigate Ruins", stat: "wisdom", difficulty: 5, success: 13, failure: 14 },
232
+ options: []
233
+ },
234
+ // --- Corresponding Challenge Outcomes ---
235
+ "9": { // Success Ambush (Continue North)
236
+ title: "Breaking Through", content: "<p>You fought them off!</p>", illustration: "forest-edge",
237
+ mapX: 0, mapY: 3, type: 'forest', features: ['path_north', 'path_south'], // Edge of forest
238
+ options: [ {text: "Cross the plains", next: 15} ] // To page 15 (fortress approach)
239
+ },
240
+ "10": { // Failure Ambush (Captured) -> Leads to different location potentially? Let's put it nearby for now.
241
+ title: "Captured!", content: "<p>They drag you to an outpost.</p>", illustration: "prisoner-cell",
242
+ mapX: 1, mapY: 2, type: 'cave', // Represent outpost as cave/cell
243
+ options: [ { text: "Wait...", next: 20 } ] // Link to page 20 logic
244
+ },
245
+ "11": { // Success Riddle
246
+ title: "Spirit's Blessing", content: "<p>The spirit blesses you.</p>", illustration: "spirit-blessing",
247
+ mapX: 1, mapY: 1, type: 'forest', features: ['river', 'path_west'], // Stay in same spot, get item/stat
248
+ addItem: "Water Spirit's Blessing", statIncrease: { stat: "wisdom", amount: 2 },
249
+ options: [ { text: "Continue journey", next: 15 } ] // Option to move on
250
+ },
251
+ "12": { // Failure Riddle
252
+ title: "Spirit's Wrath", content: "<p>Pulled underwater!</p>", illustration: "river-danger",
253
+ mapX: 1, mapY: 1, type: 'forest', features: ['river', 'path_west'], // Stay in same spot, lose HP
254
+ hpLoss: 8,
255
+ options: [ { text: "Struggle onwards", next: 15 } ]
256
+ },
257
+ "13": { // Success Ruins
258
+ title: "Ancient Allies", content: "<p>The spirits offer help.</p>", illustration: "ancient-spirits",
259
+ mapX: -1, mapY: 1, type: 'ruins', features: ['path_east'],
260
+ addItem: "Ancient Amulet", statIncrease: { stat: "wisdom", amount: 1 },
261
+ options: [ { text: "Accept amulet and continue", next: 15 } ]
262
+ },
263
+ "14": { // Failure Ruins
264
+ title: "Lost in Time", content: "<p>Exhausted from the maze.</p>", illustration: "lost-ruins",
265
+ mapX: -1, mapY: 1, type: 'ruins', features: ['path_east'],
266
+ hpLoss: 10,
267
+ options: [ { text: "Push onwards, weakened", next: 15 } ]
268
+ },
269
+ "15": { // Fortress Approach (example)
270
+ title: "The Looming Fortress", content: "<p>The dark fortress rises...</p>", illustration: "evil-fortress",
271
+ mapX: 0, mapY: 4, type: 'plains', // Barren plains
272
+ options: [ /* ... options to infiltrate ... */ {text: "Survey the area", next: 16} ]
273
+ },
274
+ // ... Add ALL other pages with mapX, mapY, type, features ...
275
+ "99": {
276
+ title: "Game Over", content: "<p>Your adventure ends here.</p>",
277
+ mapX: 0, mapY: 0, type: 'gameover', // Special type
278
+ options: [{ text: "Restart", next: 1 }],
279
+ illustration: "game-over",
280
+ gameOver: true
281
+ }
282
+ };
283
+
284
+ const itemsData = { // (Keep this data as before)
285
+ "Flaming Sword": { type: "weapon", description: "A fiery blade. +3 Attack.", attackBonus: 3 },
286
+ "Whispering Bow": { type: "weapon", description: "A silent bow. +2 Attack.", attackBonus: 2 },
287
+ // ... other items ...
288
+ "Secret Tunnel Map": { type: "quest", description: "Shows a hidden path" },
289
+ "Master Key": { type: "quest", description: "Unlocks many doors" },
290
+ "Ancient Amulet": { type: "armor", description: "Wards off some dark magic. +1 Defense", defenseBonus: 1}, // Made armor type
291
+ };
292
+
293
+ // --- Game State ---
294
+ let gameState = {}; // Initialize in startGame
295
+ let visitedCoords = new Set(); // Track visited map coordinates as "x,y" strings
296
+
297
+ // --- Game Logic Functions ---
298
+
299
+ function startGame() {
300
+ gameState = { // Reset state
301
+ currentPageId: 1,
302
+ inventory: [],
303
+ stats: { courage: 7, wisdom: 5, strength: 6, hp: 30, maxHp: 30 }
304
+ };
305
+ visitedCoords = new Set(); // Reset visited locations
306
+ const startPage = gameData[gameState.currentPageId];
307
+ if (startPage && startPage.mapX !== undefined && startPage.mapY !== undefined) {
308
+ visitedCoords.add(`${startPage.mapX},${startPage.mapY}`); // Mark start as visited
309
+ }
310
+ renderPage(gameState.currentPageId);
311
+ }
312
+
313
+ function renderPage(pageId) {
314
+ const page = gameData[pageId];
315
+ if (!page) {
316
+ console.error(`Error: Page data not found for ID: ${pageId}`);
317
+ // Handle error state more gracefully if needed
318
+ storyTitleElement.textContent = "Error";
319
+ storyContentElement.innerHTML = "<p>Adventure lost in the void!</p>";
320
+ choicesElement.innerHTML = ''; // Clear choices
321
+ addChoiceButton("Restart", 1); // Add only restart
322
+ updateScene(null); // Clear the scene or show error state
323
+ return;
324
+ }
325
+
326
+ // Update UI Text
327
+ storyTitleElement.textContent = page.title || "Untitled Location";
328
+ storyContentElement.innerHTML = page.content || "<p>...</p>";
329
+ updateStatsDisplay();
330
+ updateInventoryDisplay();
331
+
332
+ // Update Choices based on requirements
333
+ choicesElement.innerHTML = ''; // Clear old choices
334
+ let hasAvailableOptions = false;
335
+ if (page.options && !page.challenge && !page.gameOver) { // Don't show choices if there's a challenge or game over
336
+ page.options.forEach(option => {
337
+ // Check requirements before adding the button
338
+ const requirementFailed = !checkChoiceRequirements(option, gameState.inventory);
339
+ const button = addChoiceButton(option.text, option.next, option.addItem, requirementFailed);
340
+ if (!requirementFailed) {
341
+ hasAvailableOptions = true;
342
+ }
343
+ });
344
+ }
345
+
346
+ // Handle Game Over / No Options / Challenge Page
347
+ if (page.gameOver) {
348
+ addChoiceButton("Restart Adventure", 1);
349
+ hasAvailableOptions = true; // Allow restart
350
+ } else if (page.challenge) {
351
+ choicesElement.innerHTML = `<p><i>A challenge blocks your path...</i></p>`;
352
+ // Challenge resolved in handleChoiceClick *after* rendering this page
353
+ } else if (!hasAvailableOptions && !page.gameOver && !page.challenge) {
354
+ choicesElement.innerHTML = '<p><i>No further options available from here.</i></p>';
355
+ addChoiceButton("Restart Adventure", 1);
356
+ }
357
+
358
+ // Update 3D Scene based on current page's coordinates
359
+ updateScene(pageId);
360
+ }
361
+
362
+ // Helper to add choice buttons
363
+ function addChoiceButton(text, nextPage, addItem = null, disabled = false, requireItem = null, requireAnyItem = null) {
364
+ const button = document.createElement('button');
365
+ button.classList.add('choice-button');
366
+ button.textContent = text;
367
+ button.disabled = disabled;
368
+
369
+ // Store data
370
+ button.dataset.nextPage = nextPage;
371
+ if (addItem) button.dataset.addItem = addItem;
372
+
373
+ // Create tooltip for disabled buttons based on requirement
374
+ if (disabled) {
375
+ if (requireItem) button.title = `Requires: ${requireItem}`;
376
+ else if (requireAnyItem) button.title = `Requires one of: ${requireAnyItem.join(', ')}`;
377
+ else button.title = `Cannot choose this option now.`;
378
+ }
379
+
380
+ if (!disabled) {
381
+ button.onclick = () => handleChoiceClick(button.dataset);
382
+ }
383
+ choicesElement.appendChild(button);
384
+ return button; // Return button in case needed
385
+ }
386
+
387
+
388
+ // Check choice requirements (returns true if requirements MET)
389
+ function checkChoiceRequirements(option, inventory) {
390
+ const reqItem = option.requireItem;
391
+ if (reqItem && !inventory.includes(reqItem)) {
392
+ return false; // Specific item required but not present
393
+ }
394
+ const reqAnyItem = option.requireAnyItem;
395
+ if (reqAnyItem && !reqAnyItem.some(item => inventory.includes(item))) {
396
+ return false; // Required one item from list, but none present
397
+ }
398
+ return true; // All requirements met or no requirements
399
+ }
400
+
401
+ function handleChoiceClick(dataset) {
402
+ const nextPageId = parseInt(dataset.nextPage);
403
+ const itemToAdd = dataset.addItem;
404
+
405
+ if (isNaN(nextPageId)) {
406
+ console.error("Invalid nextPageId:", dataset.nextPage);
407
+ return;
408
+ }
409
+
410
+ // --- Process Effects of Making the Choice ---
411
+ if (itemToAdd && !gameState.inventory.includes(itemToAdd)) {
412
+ gameState.inventory.push(itemToAdd);
413
+ console.log("Added item:", itemToAdd);
414
+ // Add feedback to player? Maybe via story text update?
415
+ }
416
+
417
+ // --- Move to Next Page ---
418
+ const previousPageId = gameState.currentPageId;
419
+ gameState.currentPageId = nextPageId;
420
+
421
+ const nextPageData = gameData[nextPageId];
422
+ if (!nextPageData) {
423
+ console.error(`Data for page ${nextPageId} not found!`);
424
+ renderPage(99); // Go to game over page as fallback
425
+ return;
426
+ }
427
+
428
+ // Add coordinates to visited set
429
+ if (nextPageData.mapX !== undefined && nextPageData.mapY !== undefined) {
430
+ visitedCoords.add(`${nextPageData.mapX},${nextPageData.mapY}`);
431
+ }
432
+
433
+
434
+ // --- Process Landing Effects & Challenges ---
435
+ let currentPageIdAfterEffects = nextPageId; // Start with the target page ID
436
+
437
+ // Apply HP loss defined on the *landing* page
438
+ if (nextPageData.hpLoss) {
439
+ gameState.stats.hp -= nextPageData.hpLoss;
440
+ console.log(`Lost ${nextPageData.hpLoss} HP. Current HP: ${gameState.stats.hp}`);
441
+ if (gameState.stats.hp <= 0) {
442
+ console.log("Player died from HP loss!");
443
+ gameState.stats.hp = 0;
444
+ renderPage(99); // Go to a specific game over page ID
445
+ return;
446
+ }
447
+ }
448
+ // Apply stat increase defined on the *landing* page
449
+ if (nextPageData.statIncrease) {
450
+ const stat = nextPageData.statIncrease.stat;
451
+ const amount = nextPageData.statIncrease.amount;
452
+ if (gameState.stats.hasOwnProperty(stat)) {
453
+ gameState.stats[stat] += amount;
454
+ console.log(`Stat ${stat} increased by ${amount}.`);
455
+ }
456
+ }
457
+
458
+ // Handle Challenge on the landing page
459
+ if (nextPageData.challenge) {
460
+ const challenge = nextPageData.challenge;
461
+ const reqStat = challenge.stat;
462
+ const difficulty = challenge.difficulty;
463
+ const successPage = challenge.success;
464
+ const failurePage = challenge.failure;
465
+
466
+ if (reqStat && difficulty !== undefined && successPage !== undefined && failurePage !== undefined) {
467
+ const currentStatValue = gameState.stats[reqStat] || 0;
468
+ const roll = Math.floor(Math.random() * 6) + 1;
469
+ const total = roll + currentStatValue;
470
+ const success = total >= difficulty;
471
+
472
+ console.log(`Challenge: ${challenge.title || 'Test'}. Roll(${roll}) + ${reqStat}(${currentStatValue}) = ${total} vs DC ${difficulty}. Success: ${success}`);
473
+
474
+ // Update page based on challenge result
475
+ currentPageIdAfterEffects = success ? successPage : failurePage;
476
+
477
+ // Apply stat change from challenge outcome (optional: only on success/failure?)
478
+ // This example applies basic +/- based on outcome
479
+ if(gameState.stats.hasOwnProperty(reqStat)) {
480
+ gameState.stats[reqStat] += success ? 1 : -1;
481
+ gameState.stats[reqStat] = Math.max(1, gameState.stats[reqStat]); // Don't go below 1
482
+ }
483
+
484
+ // Add challenge result to story? (Needs modification of renderPage or state)
485
+
486
+ // If challenge leads to non-existent page, handle error
487
+ if (!gameData[currentPageIdAfterEffects]) {
488
+ console.error(`Challenge outcome page ${currentPageIdAfterEffects} not found!`);
489
+ renderPage(99); return;
490
+ }
491
+ // Update visited coords for the challenge outcome page
492
+ const challengeOutcomePageData = gameData[currentPageIdAfterEffects];
493
+ if (challengeOutcomePageData.mapX !== undefined && challengeOutcomePageData.mapY !== undefined) {
494
+ visitedCoords.add(`${challengeOutcomePageData.mapX},${challengeOutcomePageData.mapY}`);
495
+ }
496
+
497
+
498
+ } else {
499
+ console.error("Malformed challenge data on page", nextPageId);
500
+ }
501
+ gameState.currentPageId = currentPageIdAfterEffects; // Update state with final page after challenge
502
+ }
503
+
504
+
505
+ // --- Render the final page after all effects/challenges ---
506
+ renderPage(gameState.currentPageId);
507
+ }
508
+
509
+
510
+ function updateStatsDisplay() {
511
+ let statsHTML = ''; // Start fresh
512
+ statsHTML += `<span>HP: ${gameState.stats.hp}/${gameState.stats.maxHp}</span>`;
513
+ statsHTML += `<span>Str: ${gameState.stats.strength}</span>`;
514
+ statsHTML += `<span>Wis: ${gameState.stats.wisdom}</span>`;
515
+ statsHTML += `<span>Cor: ${gameState.stats.courage}</span>`;
516
+ statsElement.innerHTML = statsHTML;
517
+ }
518
+
519
+ function updateInventoryDisplay() {
520
+ let inventoryHTML = ''; // Start fresh
521
+ if (gameState.inventory.length === 0) {
522
+ inventoryHTML += '<em>Empty</em>';
523
+ } else {
524
+ gameState.inventory.forEach(item => {
525
+ const itemInfo = itemsData[item] || { type: 'unknown', description: '???' };
526
+ const itemClass = `item-${itemInfo.type || 'unknown'}`;
527
+ inventoryHTML += `<span class="${itemClass}" title="${itemInfo.description}">${item}</span>`;
528
+ });
529
+ }
530
+ inventoryElement.innerHTML = inventoryHTML;
531
+ }
532
+
533
+ // --- Map and Scene Update Logic ---
534
+ function updateScene(currentPageId) {
535
+ if (!mapGroup) return;
536
+
537
+ // Clear previous map elements
538
+ while (mapGroup.children.length > 0) {
539
+ mapGroup.remove(mapGroup.children[0]);
540
+ }
541
+
542
+ const currentPageData = gameData[currentPageId];
543
+ if (!currentPageData || currentPageData.mapX === undefined || currentPageData.mapY === undefined) {
544
+ console.warn("Cannot update scene, current page has no map coordinates:", currentPageId);
545
+ // Maybe show a default "limbo" state?
546
+ camera.position.set(0, CAMERA_HEIGHT, 0);
547
+ camera.lookAt(0, 0, 0);
548
+ if (scene.getObjectByName("playerLight")) scene.getObjectByName("playerLight").position.set(0, CAMERA_HEIGHT - 2, 0);
549
+ return;
550
+ }
551
+
552
+ const currentX = currentPageData.mapX;
553
+ const currentY = currentPageData.mapY; // Using Y for Z in 3D
554
+
555
+ // Update camera and light position to center on current room
556
+ const targetX = currentX * ROOM_SIZE;
557
+ const targetZ = currentY * ROOM_SIZE;
558
+ camera.position.set(targetX, CAMERA_HEIGHT, targetZ + 0.1); // Offset slightly to avoid z-fighting if looking straight down
559
+ camera.lookAt(targetX, 0, targetZ);
560
+ const playerLight = scene.getObjectByName("playerLight"); // Get light added in init
561
+ if (playerLight) playerLight.position.set(targetX, CAMERA_HEIGHT - 2, targetZ);
562
+
563
+
564
+ // Define view distance (how many neighbors to render)
565
+ const viewDistance = 2;
566
+
567
+ // Render current room and neighbors
568
+ for (let dx = -viewDistance; dx <= viewDistance; dx++) {
569
+ for (let dy = -viewDistance; dy <= viewDistance; dy++) {
570
+ const checkX = currentX + dx;
571
+ const checkY = currentY + dy;
572
+ const coordString = `${checkX},${checkY}`;
573
+
574
+ // Find page ID for this coordinate (requires efficient lookup - maybe build map index?)
575
+ let pageIdAtCoord = null;
576
+ for (const id in gameData) {
577
+ if (gameData[id].mapX === checkX && gameData[id].mapY === checkY) {
578
+ pageIdAtCoord = id;
579
+ break;
580
+ }
581
+ }
582
+
583
+ if (pageIdAtCoord) {
584
+ const pageDataAtCoord = gameData[pageIdAtCoord];
585
+ const isVisited = visitedCoords.has(coordString);
586
+ const isCurrent = (checkX === currentX && checkY === currentY);
587
+
588
+ if (isCurrent || isVisited) {
589
+ // Render visited/current room
590
+ const floor = createFloor(pageDataAtCoord.type, checkX, checkY);
591
+ mapGroup.add(floor);
592
+
593
+ // Add walls (simplified: add all 4 unless a feature indicates door/opening)
594
+ // More complex: check connections to neighbors
595
+ const features = pageDataAtCoord.features || [];
596
+ if (!features.includes('door_north')) mapGroup.add(createWall('north', checkX, checkY));
597
+ if (!features.includes('door_south')) mapGroup.add(createWall('south', checkX, checkY));
598
+ if (!features.includes('door_east')) mapGroup.add(createWall('east', checkX, checkY));
599
+ if (!features.includes('door_west')) mapGroup.add(createWall('west', checkX, checkY));
600
+
601
+ // Add other features
602
+ features.forEach(feature => {
603
+ const featureMesh = createFeature(feature, checkX, checkY);
604
+ if (featureMesh) mapGroup.add(featureMesh);
605
+ });
606
+
607
+
608
+ // Add marker
609
+ if (isCurrent) {
610
+ mapGroup.add(createMarker('current', checkX, checkY));
611
+ } else {
612
+ mapGroup.add(createMarker('visited', checkX, checkY));
613
+ }
614
+
615
+ } else {
616
+ // Render unexplored marker for adjacent non-visited rooms
617
+ if(Math.abs(dx) <= 1 && Math.abs(dy) <= 1 && !(dx === 0 && dy ===0)) { // Only immediate neighbors
618
+ mapGroup.add(createMarker('unexplored', checkX, checkY));
619
+ }
620
+ }
621
+ } else {
622
+ // Optional: Render something if coordinate is empty but adjacent (like a boundary wall)
623
+ // Or just leave it empty (part of the fog)
624
+ }
625
+ }
626
+ }
627
+ // Update controls target if using OrbitControls
628
+ // if (controls) controls.target.set(targetX, 0, targetZ);
629
+ }
630
+
631
+
632
+ // --- Initialization ---
633
+ initThreeJS();
634
+ startGame(); // Start the game after setting up Three.js