Spaces:
Running
Running
Update index.html
Browse files- index.html +245 -254
index.html
CHANGED
@@ -38,7 +38,7 @@
|
|
38 |
padding: 20px;
|
39 |
overflow-y: auto;
|
40 |
background-color: #333;
|
41 |
-
min-width: 280px;
|
42 |
height: 100%;
|
43 |
box-sizing: border-box;
|
44 |
display: flex;
|
@@ -59,11 +59,14 @@
|
|
59 |
#story-content {
|
60 |
margin-bottom: 20px;
|
61 |
line-height: 1.6;
|
62 |
-
flex-grow: 1;
|
63 |
}
|
64 |
#story-content p { margin-bottom: 1em; }
|
65 |
#story-content p:last-child { margin-bottom: 0; }
|
66 |
-
#story-content .
|
|
|
|
|
|
|
67 |
|
68 |
#stats-inventory-container {
|
69 |
margin-bottom: 20px;
|
@@ -97,7 +100,7 @@
|
|
97 |
#inventory-display .item-unknown { background-color: #555; border-color: #777;}
|
98 |
|
99 |
#choices-container {
|
100 |
-
margin-top: auto;
|
101 |
padding-top: 15px;
|
102 |
border-top: 1px solid #555;
|
103 |
}
|
@@ -115,15 +118,8 @@
|
|
115 |
.choice-button:hover:not(:disabled) { background-color: #d4a017; color: #222; border-color: #b8860b; }
|
116 |
.choice-button:disabled { background-color: #444; color: #888; cursor: not-allowed; border-color: #666; opacity: 0.7; }
|
117 |
|
118 |
-
|
119 |
-
.sell-button {
|
120 |
-
background-color: #4a4a4a;
|
121 |
-
border-color: #6a6a6a;
|
122 |
-
}
|
123 |
-
.sell-button:hover:not(:disabled) {
|
124 |
-
background-color: #a07017; /* Different hover for sell */
|
125 |
-
border-color: #80500b;
|
126 |
-
}
|
127 |
|
128 |
.roll-success { color: #7f7; border-left: 3px solid #4a4; padding-left: 8px; margin-bottom: 1em; font-size: 0.9em; }
|
129 |
.roll-failure { color: #f77; border-left: 3px solid #a44; padding-left: 8px; margin-bottom: 1em; font-size: 0.9em; }
|
@@ -160,6 +156,7 @@
|
|
160 |
<script type="module">
|
161 |
import * as THREE from 'three';
|
162 |
|
|
|
163 |
const sceneContainer = document.getElementById('scene-container');
|
164 |
const storyTitleElement = document.getElementById('story-title');
|
165 |
const storyContentElement = document.getElementById('story-content');
|
@@ -167,10 +164,11 @@
|
|
167 |
const statsElement = document.getElementById('stats-display');
|
168 |
const inventoryElement = document.getElementById('inventory-display');
|
169 |
|
|
|
170 |
let scene, camera, renderer;
|
171 |
let currentAssemblyGroup = null;
|
172 |
|
173 |
-
//
|
174 |
const stoneMaterial = new THREE.MeshStandardMaterial({ color: 0x888888, roughness: 0.8, metalness: 0.1 });
|
175 |
const woodMaterial = new THREE.MeshStandardMaterial({ color: 0x8B4513, roughness: 0.7, metalness: 0 });
|
176 |
const darkWoodMaterial = new THREE.MeshStandardMaterial({ color: 0x5C3D20, roughness: 0.7, metalness: 0 });
|
@@ -187,26 +185,37 @@
|
|
187 |
const wetStoneMaterial = new THREE.MeshStandardMaterial({ color: 0x2F4F4F, roughness: 0.7 });
|
188 |
const glowMaterial = new THREE.MeshStandardMaterial({ color: 0x00FFAA, emissive: 0x00FFAA, emissiveIntensity: 0.5 });
|
189 |
|
190 |
-
// --- Three.js Setup --- (
|
191 |
function initThreeJS() {
|
192 |
-
if (!sceneContainer) { console.error("Scene container not found!"); return; }
|
193 |
-
|
194 |
-
|
195 |
-
|
196 |
-
|
197 |
-
|
198 |
-
|
199 |
-
|
200 |
-
|
201 |
-
|
202 |
-
|
203 |
-
|
204 |
-
|
205 |
-
|
206 |
-
|
207 |
-
|
208 |
-
|
209 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
210 |
}
|
211 |
|
212 |
function onWindowResize() {
|
@@ -217,18 +226,31 @@
|
|
217 |
camera.aspect = width / height;
|
218 |
camera.updateProjectionMatrix();
|
219 |
renderer.setSize(width, height);
|
|
|
|
|
220 |
}
|
221 |
}
|
222 |
|
223 |
function animate() {
|
224 |
-
|
225 |
-
|
226 |
-
|
227 |
-
|
228 |
-
|
229 |
-
|
230 |
-
|
231 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
232 |
}
|
233 |
|
234 |
function createMesh(geometry, material, position = { x: 0, y: 0, z: 0 }, rotation = { x: 0, y: 0, z: 0 }, scale = { x: 1, y: 1, z: 1 }) {
|
@@ -248,9 +270,8 @@
|
|
248 |
return ground;
|
249 |
}
|
250 |
|
251 |
-
|
252 |
-
//
|
253 |
-
// [Keep all the create...Assembly functions from the previous listing here]
|
254 |
// ... (createDefaultAssembly, createCityGatesAssembly, ..., createDarkCaveAssembly) ...
|
255 |
function createDefaultAssembly() { const group = new THREE.Group(); const sphereGeo = new THREE.SphereGeometry(0.5, 16, 16); group.add(createMesh(sphereGeo, stoneMaterial, { x: 0, y: 0.5, z: 0 })); group.add(createGroundPlane()); return group; }
|
256 |
function createCityGatesAssembly() { const group = new THREE.Group(); const gh = 4, gw = 1.5, gd = 0.8, ah = 1, aw = 3; const tlGeo = new THREE.BoxGeometry(gw, gh, gd); group.add(createMesh(tlGeo, stoneMaterial, { x: -(aw / 2 + gw / 2), y: gh / 2, z: 0 })); const trGeo = new THREE.BoxGeometry(gw, gh, gd); group.add(createMesh(trGeo, stoneMaterial, { x: (aw / 2 + gw / 2), y: gh / 2, z: 0 })); const aGeo = new THREE.BoxGeometry(aw, ah, gd); group.add(createMesh(aGeo, stoneMaterial, { x: 0, y: gh - ah / 2, z: 0 })); const cs = 0.4; const cg = new THREE.BoxGeometry(cs, cs, gd * 1.1); for (let i = -1; i <= 1; i += 2) { group.add(createMesh(cg.clone(), stoneMaterial, { x: -(aw / 2 + gw / 2) + i * cs * 0.7, y: gh + cs / 2, z: 0 })); group.add(createMesh(cg.clone(), stoneMaterial, { x: (aw / 2 + gw / 2) + i * cs * 0.7, y: gh + cs / 2, z: 0 })); } group.add(createMesh(cg.clone(), stoneMaterial, { x: 0, y: gh + ah - cs / 2, z: 0 })); group.add(createGroundPlane(stoneMaterial)); return group; }
|
@@ -273,9 +294,8 @@
|
|
273 |
function createHiddenCoveAssembly() { const group = new THREE.Group(); group.add(createGroundPlane(sandMaterial, 15)); const caveGeo = new THREE.BoxGeometry(3, 2.5, 3); const caveMat = new THREE.MeshStandardMaterial({ color: 0x111111 }); group.add(createMesh(caveGeo, caveMat, { z: -6, y: 1.25 })); const rockGeo = new THREE.SphereGeometry(0.5, 6, 6); const rockMat = wetStoneMaterial.clone(); for (let i = 0; i < 15; i++) { group.add(createMesh(rockGeo, rockMat, { x: (Math.random() - 0.5) * 12, y: 0.25, z: (Math.random() - 0.5) * 12 }, { y: Math.random() * Math.PI })); } const seaweedGeo = new THREE.ConeGeometry(0.2, 1.2, 6); const seaweedMat = leafMaterial.clone().set({ color: 0x1E4D2B }); for (let i = 0; i < 10; i++) { group.add(createMesh(seaweedGeo, seaweedMat, { x: (Math.random() - 0.5) * 10, y: 0.6, z: (Math.random() - 0.5) * 10 + 2 }, { x: (Math.random() - 0.5) * 0.2, z: (Math.random() - 0.5) * 0.2 })); } return group; }
|
274 |
function createDarkCaveAssembly() { const group = new THREE.Group(); const caveRadius = 5; const caveHeight = 4; group.add(createGroundPlane(wetStoneMaterial, caveRadius * 2)); const wallGeo = new THREE.SphereGeometry(caveRadius, 32, 16, 0, Math.PI * 2, 0, Math.PI / 1.5); const wallMat = wetStoneMaterial.clone(); wallMat.side = THREE.BackSide; const wall = new THREE.Mesh(wallGeo, wallMat); wall.position.y = caveHeight * 0.6; group.add(wall); const stalactiteGeo = new THREE.ConeGeometry(0.1, 0.8, 8); const stalagmiteGeo = new THREE.ConeGeometry(0.15, 0.5, 8); for (let i = 0; i < 15; i++) { const x = (Math.random() - 0.5) * caveRadius * 1.5; const z = (Math.random() - 0.5) * caveRadius * 1.5; if (Math.random() > 0.5) { group.add(createMesh(stalactiteGeo, wetStoneMaterial, { x: x, y: caveHeight - 0.4, z: z })) } else { group.add(createMesh(stalagmiteGeo, wetStoneMaterial, { x: x, y: 0.25, z: z })) } } const dripGeo = new THREE.SphereGeometry(0.05, 8, 8); for (let i = 0; i < 5; i++) { const drip = createMesh(dripGeo, oceanMaterial, { x: (Math.random() - 0.5) * caveRadius, y: caveHeight - 0.2, z: (Math.random() - 0.5) * caveRadius }); drip.userData.startY = caveHeight - 0.2; drip.userData.update = (time) => { drip.position.y -= 0.1; if (drip.position.y < 0) { drip.position.y = drip.userData.startY; drip.position.x = (Math.random() - 0.5) * caveRadius; drip.position.z = (Math.random() - 0.5) * caveRadius; } }; group.add(drip); } return group; }
|
275 |
|
276 |
-
|
277 |
// ========================================
|
278 |
-
// Game Data
|
279 |
// ========================================
|
280 |
const itemsData = {
|
281 |
"Flaming Sword": {type:"weapon", description:"A legendary blade, wreathed in magical fire.", goldValue: 500},
|
@@ -284,153 +304,125 @@
|
|
284 |
"Healing Light Spell":{type:"spell", description:"A scroll containing the incantation to mend minor wounds.", goldValue: 50},
|
285 |
"Shield of Faith Spell":{type:"spell",description:"A scroll containing a prayer that grants temporary magical protection.", goldValue: 75},
|
286 |
"Binding Runes Scroll":{type:"spell", description:"Complex runes scribbled on parchment, said to temporarily immobilize a foe.", goldValue: 100},
|
287 |
-
"Secret Tunnel Map": {type:"quest", description:"A crudely drawn map showing a hidden path, perhaps into the fortress?", goldValue:
|
288 |
"Poison Daggers": {type:"weapon", description:"A pair of wicked-looking daggers coated in a fast-acting toxin.", goldValue: 150},
|
289 |
-
"Master Key": {type:"quest", description:"An ornate key rumored to unlock many doors, though perhaps not all.", goldValue:
|
290 |
"Crude Dagger": {type:"weapon", description:"A roughly made dagger, chipped and stained.", goldValue: 10},
|
291 |
-
"Scout's Pouch": {type:"quest", description:"A small leather pouch containing flint & steel, jerky, and some odd coins.", goldValue: 20} //
|
292 |
-
// TODO: Add more items
|
293 |
};
|
294 |
|
295 |
-
const gameData = {
|
296 |
-
//
|
297 |
-
|
298 |
-
|
299 |
-
|
300 |
-
|
301 |
-
|
302 |
-
|
303 |
-
|
304 |
-
|
305 |
-
|
306 |
-
|
307 |
-
|
308 |
-
|
309 |
-
|
310 |
-
|
311 |
-
|
312 |
-
|
313 |
-
|
314 |
-
|
315 |
-
|
316 |
-
|
317 |
-
|
318 |
-
|
319 |
-
|
320 |
-
|
321 |
-
|
322 |
-
|
323 |
-
|
324 |
-
|
325 |
-
|
326 |
-
|
327 |
-
|
328 |
-
|
329 |
-
|
330 |
-
|
331 |
-
|
332 |
-
|
333 |
-
|
334 |
-
|
335 |
-
|
336 |
-
|
337 |
-
|
338 |
-
|
339 |
-
|
340 |
-
|
341 |
-
|
342 |
-
|
343 |
-
|
344 |
-
|
345 |
-
|
346 |
-
|
347 |
-
|
348 |
-
|
349 |
-
"21": { title: "Western Ridge", content:"<p>The path along the western ridge is dangerously narrow and exposed. Loose gravel shifts underfoot, and strong gusts of wind whip around you, threatening to push you off the edge into the dizzying drop below.</p>", options: [{text:"Proceed carefully along the ridge (Dexterity Check)", check:{stat:'dexterity', dc: 14, onFailure: 211}, next: 22 } ], illustration:"narrow-windy-mountain-ridge-path" },
|
350 |
-
"211": {title:"Lost Balance", content:"<p>A particularly strong gust of wind catches you at a bad moment! You lose your balance and stumble, tumbling down a steep, rocky slope before managing to arrest your fall. You lose 10 HP.</p>", options:[{text:"Climb back up and reconsider the main path", next: 17}], illustration:"character-falling-off-windy-ridge", hpLoss: 10},
|
351 |
-
// Approaching/At the Fortress
|
352 |
-
"22": { title: "Fortress Approach", content:"<p>You've navigated the treacherous paths and finally stand near the imposing outer walls of the dark mountain fortress. Stern-faced guards patrol the battlements, their eyes scanning the approaches. The main gate looks heavily fortified.</p>", options: [
|
353 |
-
{text:"Search for a less obvious entrance (Wisdom Check)", check:{stat:'wisdom', dc: 16, onFailure: 221}, next: 220}, // TODO: Success leads to secret entrance page
|
354 |
-
{text:"Attempt to bluff your way past the gate guards (Charisma Check)", check:{stat:'charisma', dc: 15, onFailure: 222}, next: 223}, // TODO: Success/Failure pages for bluff
|
355 |
-
{text:"Try to sneak past the gate guards (Dexterity Check)", check:{stat:'dexterity', dc: 17, onFailure: 222}, next: 224}, // TODO: Success/Failure pages for sneak
|
356 |
-
{text:"Retreat for now", next: 16} // Option to go back
|
357 |
-
], illustration:"approaching-dark-fortress-walls-guards"},
|
358 |
-
"220": { title: "Secret Passage?", content:"<p>Your careful search reveals loose stones near the base of the wall, potentially hiding a passage!</p>", options: [{text:"Investigate the loose stones", next: 99}], illustration:"approaching-dark-fortress-walls-guards"}, // TODO: Expand secret passage
|
359 |
-
"221": { title: "No Obvious Weakness", content:"<p>The fortress walls look solid and well-maintained. You find no obvious weak points or hidden entrances from this vantage point.</p>", options: [{text:"Reconsider your approach", next: 22}], illustration:"approaching-dark-fortress-walls-guards"},
|
360 |
-
"222": { title: "Caught!", content:"<p>Your attempt fails miserably! The guards spot you immediately and raise the alarm! You are captured.</p>", options: [{text:"To the dungeons...", next: 99}], illustration:"approaching-dark-fortress-walls-guards"}, // TODO: Lead to prisoner cell?
|
361 |
-
"223": { title: "Bluff Success?", content:"<p>Amazingly, your story seems plausible enough for the guards to let you pass through the gate!</p>", options: [{text:"Enter the fortress courtyard", next: 99}], illustration:"approaching-dark-fortress-walls-guards"}, // TODO: Expand fortress interior
|
362 |
-
"224": { title: "Sneak Success?", content:"<p>Moving like a shadow, you manage to slip past the gate guards unnoticed!</p>", options: [{text:"Enter the fortress courtyard", next: 99}], illustration:"approaching-dark-fortress-walls-guards"}, // TODO: Expand fortress interior
|
363 |
-
|
364 |
-
// --- Game Over / Error State ---
|
365 |
"99": {
|
366 |
title: "Game Over / To Be Continued...",
|
367 |
-
content: "<p>Your adventure ends here... for now. You can sell unwanted items for gold before starting again.</p>",
|
368 |
-
|
369 |
-
options: [
|
370 |
-
// The restart button will be added last by renderPageInternal logic
|
371 |
-
],
|
372 |
illustration: "game-over-generic",
|
373 |
-
gameOver: true,
|
374 |
-
allowSell: true //
|
375 |
}
|
376 |
};
|
377 |
|
378 |
-
|
379 |
// ========================================
|
380 |
-
// Game State
|
381 |
// ========================================
|
382 |
-
// Define default character state - used for first launch
|
383 |
const defaultCharacterState = {
|
384 |
name: "Hero", race: "Human", alignment: "Neutral Good", class: "Adventurer",
|
385 |
-
level: 1, xp: 0, xpToNextLevel: 100, gold: 0,
|
386 |
stats: { strength: 8, intelligence: 10, wisdom: 10, dexterity: 10, constitution: 10, charisma: 8, hp: 12, maxHp: 12 },
|
387 |
inventory: []
|
388 |
};
|
389 |
-
|
390 |
-
// Initialize gameState
|
391 |
let gameState = {
|
392 |
currentPageId: 1,
|
393 |
-
//
|
394 |
-
character: JSON.parse(JSON.stringify(defaultCharacterState)),
|
395 |
-
// Store sell feedback message temporarily
|
396 |
-
lastSellMessage: ""
|
397 |
};
|
398 |
|
399 |
-
|
400 |
// ========================================
|
401 |
-
// Game Logic Functions
|
402 |
// ========================================
|
403 |
|
404 |
-
// Function to start a brand new game (resets everything)
|
405 |
function startNewGame() {
|
406 |
console.log("Starting brand new game...");
|
407 |
-
// Reset state completely using the default
|
408 |
gameState = {
|
409 |
currentPageId: 1,
|
410 |
-
character: JSON.parse(JSON.stringify(defaultCharacterState))
|
411 |
-
lastSellMessage: ""
|
412 |
};
|
413 |
-
renderPage(gameState.currentPageId);
|
414 |
}
|
415 |
|
416 |
-
// Function to restart, keeping character progress ("New Game Plus")
|
417 |
function restartGamePlus() {
|
418 |
console.log("Restarting game (keeping progress)...");
|
419 |
-
|
420 |
-
gameState.
|
421 |
-
renderPage(gameState.currentPageId);
|
422 |
}
|
423 |
|
424 |
function handleChoiceClick(choiceData) {
|
425 |
console.log("Choice clicked:", choiceData);
|
|
|
426 |
|
427 |
-
// --- Special Actions
|
428 |
if (choiceData.action === 'restart_plus') {
|
429 |
restartGamePlus();
|
430 |
return;
|
431 |
}
|
432 |
if (choiceData.action === 'sell_item') {
|
433 |
-
handleSellItem
|
|
|
|
|
|
|
434 |
return;
|
435 |
}
|
436 |
|
@@ -438,20 +430,17 @@
|
|
438 |
const optionNextPageId = parseInt(choiceData.next);
|
439 |
const itemToAdd = choiceData.addItem;
|
440 |
let nextPageId = optionNextPageId;
|
441 |
-
let rollResultMessage = gameState.lastSellMessage || ""; // Carry over sell message if any
|
442 |
-
gameState.lastSellMessage = ""; // Clear sell message after use
|
443 |
const check = choiceData.check;
|
444 |
|
445 |
-
// --- Basic Input Validation ---
|
446 |
if (isNaN(optionNextPageId) && !check) {
|
447 |
console.error("Invalid choice data: Missing 'next' page ID and no check defined.", choiceData);
|
448 |
-
|
449 |
-
renderPageInternal(gameState.currentPageId,
|
450 |
choicesElement.querySelectorAll('button').forEach(b => b.disabled = true);
|
451 |
return;
|
452 |
}
|
453 |
|
454 |
-
// --- Skill Check
|
455 |
if (check) {
|
456 |
const statValue = gameState.character.stats[check.stat] || 10;
|
457 |
const modifier = Math.floor((statValue - 10) / 2);
|
@@ -460,17 +449,16 @@
|
|
460 |
const dc = check.dc;
|
461 |
const statName = check.stat.charAt(0).toUpperCase() + check.stat.slice(1);
|
462 |
console.log(`Check: ${statName} (DC ${dc}) | Roll: ${roll} + Mod: ${modifier} = ${totalResult}`);
|
463 |
-
|
464 |
-
if (totalResult >= dc) { // Success
|
465 |
nextPageId = optionNextPageId;
|
466 |
-
|
467 |
-
} else {
|
468 |
nextPageId = parseInt(check.onFailure);
|
469 |
-
|
470 |
if (isNaN(nextPageId)) {
|
471 |
console.error("Invalid onFailure ID:", check.onFailure);
|
472 |
nextPageId = 99;
|
473 |
-
|
474 |
}
|
475 |
}
|
476 |
}
|
@@ -479,11 +467,11 @@
|
|
479 |
const targetPageData = gameData[nextPageId];
|
480 |
if (!targetPageData) {
|
481 |
console.error(`Data for target page ${nextPageId} not found!`);
|
482 |
-
|
|
|
483 |
return;
|
484 |
}
|
485 |
|
486 |
-
// Apply consequences/rewards defined on the *target* page
|
487 |
let hpLostThisTurn = 0;
|
488 |
if (targetPageData.hpLoss) {
|
489 |
hpLostThisTurn = targetPageData.hpLoss;
|
@@ -496,23 +484,20 @@
|
|
496 |
console.log(`Gained ${hpGained} HP.`);
|
497 |
}
|
498 |
|
499 |
-
// Check for death *after* applying HP changes
|
500 |
if (gameState.character.stats.hp <= 0) {
|
501 |
gameState.character.stats.hp = 0;
|
502 |
console.log("Player died!");
|
503 |
-
nextPageId = 99;
|
504 |
-
|
505 |
-
|
506 |
-
|
507 |
-
return; // Stop processing
|
508 |
}
|
509 |
|
510 |
-
// Apply other rewards if alive
|
511 |
if (targetPageData.reward) {
|
512 |
if (targetPageData.reward.xp) {
|
513 |
gameState.character.xp += targetPageData.reward.xp;
|
514 |
console.log(`Gained ${targetPageData.reward.xp} XP! Total: ${gameState.character.xp}`);
|
515 |
-
// checkLevelUp();
|
516 |
}
|
517 |
if (targetPageData.reward.statIncrease) {
|
518 |
const stat = targetPageData.reward.statIncrease.stat;
|
@@ -524,131 +509,141 @@
|
|
524 |
}
|
525 |
}
|
526 |
if (targetPageData.reward.addItem && !gameState.character.inventory.includes(targetPageData.reward.addItem)) {
|
527 |
-
|
528 |
-
|
529 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
530 |
}
|
531 |
}
|
532 |
if (itemToAdd && !gameState.character.inventory.includes(itemToAdd)) {
|
533 |
-
|
534 |
-
|
535 |
-
|
|
|
|
|
|
|
|
|
|
|
536 |
}
|
537 |
|
538 |
-
// --- Update Game State ---
|
539 |
gameState.currentPageId = nextPageId;
|
540 |
recalculateMaxHp();
|
541 |
gameState.character.stats.hp = Math.min(gameState.character.stats.hp, gameState.character.stats.maxHp);
|
542 |
|
543 |
console.log("Transitioning to page:", nextPageId, " New state:", JSON.stringify(gameState));
|
544 |
-
renderPageInternal(nextPageId, gameData[nextPageId],
|
545 |
}
|
546 |
|
|
|
547 |
function handleSellItem(itemName) {
|
548 |
console.log("Attempting to sell:", itemName);
|
549 |
const itemIndex = gameState.character.inventory.indexOf(itemName);
|
550 |
const itemInfo = itemsData[itemName];
|
551 |
-
|
552 |
-
|
553 |
-
|
554 |
-
|
555 |
-
|
556 |
-
|
557 |
-
|
558 |
-
|
559 |
-
|
560 |
-
|
561 |
-
|
562 |
-
|
563 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
564 |
}
|
565 |
|
566 |
|
567 |
-
function recalculateMaxHp() {
|
568 |
-
const baseHp = 10;
|
569 |
const conModifier = Math.floor((gameState.character.stats.constitution - 10) / 2);
|
570 |
-
gameState.character.stats.maxHp = Math.max(1, baseHp + conModifier * gameState.character.level);
|
571 |
}
|
572 |
|
573 |
-
function renderPageInternal(pageId, pageData, message = "") {
|
574 |
if (!pageData) {
|
575 |
console.error(`Render Error: No data for page ${pageId}`);
|
576 |
-
pageData = gameData[99] || { title: "Error", content: "<p>Render Error! Critical page data missing.</p>", illustration: "error", gameOver: true };
|
577 |
-
message += "
|
578 |
pageId = 99;
|
579 |
}
|
580 |
console.log(`Rendering page ${pageId}: "${pageData.title}"`);
|
581 |
|
582 |
storyTitleElement.textContent = pageData.title || "Untitled Page";
|
583 |
-
// Inject message first, then page content
|
584 |
storyContentElement.innerHTML = message + (pageData.content || "<p>...</p>");
|
585 |
|
586 |
updateStatsDisplay();
|
587 |
updateInventoryDisplay();
|
588 |
-
choicesElement.innerHTML = '';
|
589 |
|
590 |
const options = pageData.options || [];
|
591 |
-
const isGameOverPage = pageData.gameOver === true;
|
592 |
|
593 |
-
//
|
594 |
if (isGameOverPage && pageData.allowSell === true) {
|
595 |
const sellableItems = gameState.character.inventory.filter(itemName => {
|
596 |
const itemInfo = itemsData[itemName];
|
597 |
-
|
|
|
598 |
});
|
599 |
|
600 |
if (sellableItems.length > 0) {
|
601 |
-
choicesElement.innerHTML += `<h3>Sell Items:</h3>`;
|
602 |
sellableItems.forEach(itemName => {
|
|
|
603 |
const itemInfo = itemsData[itemName];
|
|
|
|
|
604 |
const sellButton = document.createElement('button');
|
605 |
-
sellButton.classList.add('choice-button', 'sell-button');
|
606 |
sellButton.textContent = `Sell ${itemName} (${itemInfo.goldValue} Gold)`;
|
607 |
sellButton.onclick = () => handleChoiceClick({ action: 'sell_item', item: itemName });
|
608 |
choicesElement.appendChild(sellButton);
|
609 |
});
|
610 |
-
choicesElement.innerHTML += `<hr style="border-color: #555; margin: 10px 0;">`; // Separator
|
611 |
}
|
612 |
}
|
613 |
|
614 |
-
|
615 |
-
|
616 |
-
if (!isGameOverPage && options.length > 0) { // Normal page with navigation choices
|
617 |
options.forEach(option => {
|
618 |
const button = document.createElement('button');
|
619 |
button.classList.add('choice-button');
|
620 |
button.textContent = option.text;
|
621 |
let requirementMet = true;
|
622 |
let requirementText = [];
|
623 |
-
|
624 |
-
if (option.
|
625 |
-
if (!gameState.character.inventory.includes(option.requireItem)) {
|
626 |
-
requirementMet = false; requirementText.push(`Requires: ${option.requireItem}`);
|
627 |
-
}
|
628 |
-
}
|
629 |
-
if (option.requireStat) {
|
630 |
-
const currentStat = gameState.character.stats[option.requireStat.stat] || 0;
|
631 |
-
if (currentStat < option.requireStat.value) {
|
632 |
-
requirementMet = false; requirementText.push(`Requires: ${option.requireStat.stat.charAt(0).toUpperCase() + option.requireStat.stat.slice(1)} ${option.requireStat.value}`);
|
633 |
-
}
|
634 |
-
}
|
635 |
-
|
636 |
button.disabled = !requirementMet;
|
637 |
if (!requirementMet) button.title = requirementText.join(', ');
|
638 |
-
else {
|
639 |
-
const choiceData = { next: option.next, addItem: option.addItem, check: option.check };
|
640 |
-
button.onclick = () => handleChoiceClick(choiceData);
|
641 |
-
}
|
642 |
choicesElement.appendChild(button);
|
643 |
});
|
644 |
-
} else if (isGameOverPage) {
|
645 |
const restartButton = document.createElement('button');
|
646 |
restartButton.classList.add('choice-button');
|
647 |
restartButton.textContent = "Restart Adventure (Keep Progress)";
|
648 |
-
// Use the specific restart_plus action
|
649 |
restartButton.onclick = () => handleChoiceClick({ action: 'restart_plus' });
|
650 |
choicesElement.appendChild(restartButton);
|
651 |
-
} else { // End of
|
652 |
choicesElement.insertAdjacentHTML('beforeend', '<p><i>There are no further paths from here.</i></p>');
|
653 |
const restartButton = document.createElement('button');
|
654 |
restartButton.classList.add('choice-button');
|
@@ -662,13 +657,12 @@
|
|
662 |
|
663 |
function renderPage(pageId) { renderPageInternal(pageId, gameData[pageId]); }
|
664 |
|
665 |
-
function updateStatsDisplay() {
|
666 |
const char=gameState.character;
|
667 |
-
// Added Gold display
|
668 |
statsElement.innerHTML = `<strong>Stats:</strong> <span class="stat-gold">Gold: ${char.gold}</span> <span>Lvl: ${char.level}</span> <span>XP: ${char.xp}/${char.xpToNextLevel}</span> <span>HP: ${char.stats.hp}/${char.stats.maxHp}</span> <span>Str: ${char.stats.strength}</span> <span>Int: ${char.stats.intelligence}</span> <span>Wis: ${char.stats.wisdom}</span> <span>Dex: ${char.stats.dexterity}</span> <span>Con: ${char.stats.constitution}</span> <span>Cha: ${char.stats.charisma}</span>`;
|
669 |
}
|
670 |
|
671 |
-
function updateInventoryDisplay() { //
|
672 |
let h='<strong>Inventory:</strong> ';
|
673 |
if(gameState.character.inventory.length === 0){
|
674 |
h+='<em>Empty</em>';
|
@@ -683,36 +677,33 @@
|
|
683 |
inventoryElement.innerHTML = h;
|
684 |
}
|
685 |
|
686 |
-
// --- Scene Update and Lighting --- (
|
|
|
687 |
function updateScene(illustrationKey) { if (!scene) { console.warn("Scene not initialized, cannot update visual."); return; } console.log("Updating scene for illustration key:", illustrationKey); if (currentAssemblyGroup) { scene.remove(currentAssemblyGroup); currentAssemblyGroup.traverse(child => { if (child.isMesh) { child.geometry.dispose(); } }); currentAssemblyGroup = null; } scene.fog = null; scene.background = new THREE.Color(0x222222); camera.position.set(0, 2.5, 7); camera.lookAt(0, 0.5, 0); let assemblyFunction; switch (illustrationKey) { case 'city-gates': assemblyFunction = createCityGatesAssembly; break; case 'weaponsmith': assemblyFunction = createWeaponsmithAssembly; break; case 'temple': assemblyFunction = createTempleAssembly; break; case 'resistance-meeting': assemblyFunction = createResistanceMeetingAssembly; break; case 'prisoner-cell': assemblyFunction = createPrisonerCellAssembly; break; case 'game-over': case 'game-over-generic': assemblyFunction = createGameOverAssembly; break; case 'error': assemblyFunction = createErrorAssembly; break; case 'crossroads-signpost-sunny': scene.fog = new THREE.Fog(0x87CEEB, 10, 35); camera.position.set(0, 3, 10); camera.lookAt(0, 1, 0); scene.background = new THREE.Color(0x87CEEB); assemblyFunction = createCrossroadsAssembly; break; case 'rolling-green-hills-shepherd-distance': case 'hilltop-view-overgrown-shrine-wildflowers': case 'overgrown-stone-shrine-wildflowers-close': scene.fog = new THREE.Fog(0xA8E4A0, 15, 50); camera.position.set(0, 5, 15); camera.lookAt(0, 2, -5); scene.background = new THREE.Color(0x90EE90); if (illustrationKey === 'overgrown-stone-shrine-wildflowers-close') camera.position.set(1, 2, 4); if (illustrationKey === 'hilltop-view-overgrown-shrine-wildflowers') camera.position.set(3, 4, 8); assemblyFunction = createRollingHillsAssembly; break; case 'windy-sea-cliffs-crashing-waves-path-down': case 'scanning-sea-cliffs-no-other-paths-visible': case 'close-up-handholds-carved-in-cliff-face': scene.fog = new THREE.Fog(0x6699CC, 10, 40); camera.position.set(5, 5, 10); camera.lookAt(-2, 0, -5); scene.background = new THREE.Color(0x6699CC); assemblyFunction = createCoastalCliffsAssembly; break; case 'hidden-cove-beach-dark-cave-entrance': case 'character-fallen-at-bottom-of-cliff-path-cove': scene.fog = new THREE.Fog(0x336699, 5, 30); camera.position.set(0, 2, 8); camera.lookAt(0, 1, -2); scene.background = new THREE.Color(0x336699); assemblyFunction = createHiddenCoveAssembly; break; case 'rocky-badlands-cracked-earth-harsh-sun': scene.fog = new THREE.Fog(0xD2B48C, 15, 40); camera.position.set(0, 3, 12); camera.lookAt(0, 1, 0); scene.background = new THREE.Color(0xCD853F); assemblyFunction = createDefaultAssembly; break; case 'shadowwood-forest': scene.fog = new THREE.Fog(0x2E2E2E, 5, 20); camera.position.set(0, 2, 8); camera.lookAt(0, 1, 0); scene.background = new THREE.Color(0x1A1A1A); assemblyFunction = createForestAssembly; break; case 'dark-forest-entrance-gnarled-roots-filtered-light': scene.fog = new THREE.Fog(0x2E2E2E, 5, 20); camera.position.set(0, 2, 8); camera.lookAt(0, 1, 0); scene.background = new THREE.Color(0x1A1A1A); assemblyFunction = createForestEntranceAssembly; break; case 'overgrown-forest-path-glowing-fungi-vines': case 'pushing-through-forest-undergrowth': scene.fog = new THREE.Fog(0x1A2F2A, 3, 15); camera.position.set(0, 1.5, 6); camera.lookAt(0, 0.5, 0); scene.background = new THREE.Color(0x112211); assemblyFunction = createOvergrownPathAssembly; break; case 'forest-clearing-mossy-statue-weathered-stone': case 'forest-clearing-mossy-statue-hidden-compartment': case 'forest-clearing-mossy-statue-offering': scene.fog = new THREE.Fog(0x2E4F3A, 5, 25); camera.position.set(0, 2, 5); camera.lookAt(0, 1, 0); scene.background = new THREE.Color(0x223322); assemblyFunction = createClearingStatueAssembly; break; case 'narrow-game-trail-forest-rope-bridge-ravine': case 'character-crossing-rope-bridge-safely': case 'rope-bridge-snapping-character-falling': case 'fallen-log-crossing-ravine': scene.fog = new THREE.Fog(0x2E2E2E, 5, 20); camera.position.set(2, 3, 6); camera.lookAt(0, -1, -2); scene.background = new THREE.Color(0x1A1A1A); assemblyFunction = createForestAssembly; break; case 'two-goblins-ambush-forest-path-spears': case 'forest-shadows-hiding-goblins-walking-past': case 'defeated-goblins-forest-path-loot': case 'blurred-motion-running-past-goblins-forest': scene.fog = new THREE.Fog(0x1A2F2A, 3, 15); camera.position.set(0, 2, 7); camera.lookAt(0, 1, 0); scene.background = new THREE.Color(0x112211); assemblyFunction = createGoblinAmbushAssembly; break; case 'forest-stream-crossing-dappled-sunlight-stones': case 'mossy-log-bridge-over-forest-stream': case 'character-splashing-into-stream-from-log': scene.fog = new THREE.Fog(0x668866, 8, 25); camera.position.set(0, 2, 6); camera.lookAt(0, 0.5, 0); scene.background = new THREE.Color(0x446644); if (illustrationKey === 'mossy-log-bridge-over-forest-stream') camera.position.set(1, 2, 5); assemblyFunction = createForestAssembly; break; case 'forest-edge-view-rocky-foothills-distant-mountain-fortress': case 'forest-edge': scene.fog = new THREE.Fog(0xAAAAAA, 10, 40); camera.position.set(0, 3, 10); camera.lookAt(0, 1, -5); scene.background = new THREE.Color(0x888888); assemblyFunction = createForestEdgeAssembly; break; case 'climbing-rocky-foothills-path-fortress-closer': case 'rockslide-blocking-mountain-path-boulders': case 'character-climbing-over-boulders': case 'character-slipping-on-rockslide-boulders': case 'rough-detour-path-around-rockslide': scene.fog = new THREE.Fog(0x778899, 8, 35); camera.position.set(0, 4, 9); camera.lookAt(0, 2, 0); scene.background = new THREE.Color(0x708090); assemblyFunction = createDefaultAssembly; break; case 'zoomed-view-mountain-fortress-western-ridge': scene.fog = new THREE.Fog(0x778899, 8, 35); camera.position.set(5, 6, 12); camera.lookAt(-2, 3, -5); scene.background = new THREE.Color(0x708090); assemblyFunction = createDefaultAssembly; break; case 'narrow-goat-trail-mountainside-fortress-view': scene.fog = new THREE.Fog(0x778899, 5, 30); camera.position.set(1, 3, 6); camera.lookAt(0, 2, -2); scene.background = new THREE.Color(0x708090); assemblyFunction = createDefaultAssembly; break; case 'narrow-windy-mountain-ridge-path': case 'character-falling-off-windy-ridge': scene.fog = new THREE.Fog(0x8899AA, 6, 25); camera.position.set(2, 5, 7); camera.lookAt(0, 3, -3); scene.background = new THREE.Color(0x778899); assemblyFunction = createDefaultAssembly; break; case 'approaching-dark-fortress-walls-guards': scene.fog = new THREE.Fog(0x444455, 5, 20); camera.position.set(0, 3, 8); camera.lookAt(0, 2, 0); scene.background = new THREE.Color(0x333344); assemblyFunction = createDefaultAssembly; break; case 'dark-cave-entrance-dripping-water': scene.fog = new THREE.Fog(0x1A1A1A, 2, 10); camera.position.set(0, 1.5, 4); camera.lookAt(0, 1, 0); scene.background = new THREE.Color(0x111111); assemblyFunction = createDarkCaveAssembly; break; default: console.warn(`Unknown illustration key: "${illustrationKey}". Using default scene.`); assemblyFunction = createDefaultAssembly; break; } try { currentAssemblyGroup = assemblyFunction(); if (currentAssemblyGroup && currentAssemblyGroup.isGroup) { scene.add(currentAssemblyGroup); adjustLighting(illustrationKey); } else { throw new Error("Assembly function did not return a valid THREE.Group."); } } catch (error) { console.error(`Error creating assembly for ${illustrationKey}:`, error); if (currentAssemblyGroup) { scene.remove(currentAssemblyGroup); } currentAssemblyGroup = createErrorAssembly(); scene.add(currentAssemblyGroup); adjustLighting('error'); } onWindowResize(); }
|
688 |
function adjustLighting(illustrationKey) { if (!scene) return; const lightsToRemove = scene.children.filter(child => child.isLight && !child.isAmbientLight); lightsToRemove.forEach(light => scene.remove(light)); const ambient = scene.children.find(c => c.isAmbientLight); if (!ambient) { console.warn("No ambient light found, adding default."); scene.add(new THREE.AmbientLight(0xffffff, 0.5)); } let directionalLight; let lightIntensity = 1.2; let ambientIntensity = 0.5; let lightColor = 0xffffff; let lightPosition = { x: 8, y: 15, z: 10 }; switch (illustrationKey) { case 'crossroads-signpost-sunny': case 'rolling-green-hills-shepherd-distance': case 'hilltop-view-overgrown-shrine-wildflowers': case 'overgrown-stone-shrine-wildflowers-close': ambientIntensity = 0.7; lightIntensity = 1.5; lightColor = 0xFFF8E1; lightPosition = { x: 10, y: 15, z: 10 }; break; case 'shadowwood-forest': case 'dark-forest-entrance-gnarled-roots-filtered-light': case 'overgrown-forest-path-glowing-fungi-vines': case 'forest-clearing-mossy-statue-weathered-stone': case 'narrow-game-trail-forest-rope-bridge-ravine': case 'two-goblins-ambush-forest-path-spears': case 'forest-stream-crossing-dappled-sunlight-stones': case 'forest-edge-view-rocky-foothills-distant-mountain-fortress': ambientIntensity = 0.4; lightIntensity = 0.8; lightColor = 0xB0C4DE; lightPosition = { x: 5, y: 12, z: 5 }; break; case 'dark-cave-entrance-dripping-water': ambientIntensity = 0.1; lightIntensity = 0.3; lightColor = 0x667799; lightPosition = { x: 0, y: 5, z: 3 }; break; case 'prisoner-cell': ambientIntensity = 0.2; lightIntensity = 0.5; lightColor = 0x7777AA; lightPosition = { x: 0, y: 10, z: 5 }; break; case 'windy-sea-cliffs-crashing-waves-path-down': case 'hidden-cove-beach-dark-cave-entrance': ambientIntensity = 0.6; lightIntensity = 1.0; lightColor = 0xCCDDFF; lightPosition = { x: -10, y: 12, z: 8 }; break; case 'rocky-badlands-cracked-earth-harsh-sun': ambientIntensity = 0.7; lightIntensity = 1.8; lightColor = 0xFFFFDD; lightPosition = { x: 5, y: 20, z: 5 }; break; case 'climbing-rocky-foothills-path-fortress-closer': case 'zoomed-view-mountain-fortress-western-ridge': case 'narrow-goat-trail-mountainside-fortress-view': case 'narrow-windy-mountain-ridge-path': case 'approaching-dark-fortress-walls-guards': ambientIntensity = 0.5; lightIntensity = 1.3; lightColor = 0xDDEEFF; lightPosition = { x: 10, y: 18, z: 15 }; break; case 'game-over': case 'game-over-generic': ambientIntensity = 0.2; lightIntensity = 0.8; lightColor = 0xFF6666; lightPosition = { x: 0, y: 5, z: 5 }; break; case 'error': ambientIntensity = 0.4; lightIntensity = 1.0; lightColor = 0xFFCC00; lightPosition = { x: 0, y: 5, z: 5 }; break; default: ambientIntensity = 0.5; lightIntensity = 1.2; lightColor = 0xffffff; lightPosition = { x: 8, y: 15, z: 10 }; break; } const currentAmbient = scene.children.find(c => c.isAmbientLight); if (currentAmbient) { currentAmbient.intensity = ambientIntensity; } directionalLight = new THREE.DirectionalLight(lightColor, lightIntensity); directionalLight.position.set(lightPosition.x, lightPosition.y, lightPosition.z); directionalLight.castShadow = true; directionalLight.shadow.mapSize.set(1024, 1024); directionalLight.shadow.camera.near = 0.5; directionalLight.shadow.camera.far = 50; directionalLight.shadow.camera.left = -20; directionalLight.shadow.camera.right = 20; directionalLight.shadow.camera.top = 20; directionalLight.shadow.camera.bottom = -20; directionalLight.shadow.bias = -0.001; scene.add(directionalLight); }
|
689 |
|
690 |
-
// --- Potential Future Improvements Comment --- (
|
691 |
/* [Keep comment block here] */
|
692 |
|
693 |
// ========================================
|
694 |
-
// Initialization
|
695 |
// ========================================
|
696 |
document.addEventListener('DOMContentLoaded', () => {
|
697 |
console.log("DOM Ready. Initializing game...");
|
698 |
-
|
699 |
-
|
700 |
-
|
701 |
-
|
702 |
-
|
703 |
-
|
704 |
-
|
705 |
-
|
706 |
-
|
707 |
-
|
708 |
-
|
709 |
-
|
710 |
-
|
711 |
-
|
712 |
-
if (sceneContainer) {
|
713 |
-
sceneContainer.innerHTML = '<p style="color: #f77; padding: 20px; font-size: 1.2em; text-align: center;">3D Scene Failed to Load</p>';
|
714 |
-
}
|
715 |
-
scene = null; camera = null; renderer = null;
|
716 |
}
|
717 |
});
|
718 |
</script>
|
|
|
38 |
padding: 20px;
|
39 |
overflow-y: auto;
|
40 |
background-color: #333;
|
41 |
+
min-width: 280px;
|
42 |
height: 100%;
|
43 |
box-sizing: border-box;
|
44 |
display: flex;
|
|
|
59 |
#story-content {
|
60 |
margin-bottom: 20px;
|
61 |
line-height: 1.6;
|
62 |
+
flex-grow: 1;
|
63 |
}
|
64 |
#story-content p { margin-bottom: 1em; }
|
65 |
#story-content p:last-child { margin-bottom: 0; }
|
66 |
+
#story-content .feedback-message { font-style: italic; margin-top: 1em; padding-left: 8px; border-left: 3px solid #888; } /* Generic feedback */
|
67 |
+
#story-content .feedback-success { color: #9f9; border-left-color: #4a4;} /* Success feedback */
|
68 |
+
#story-content .feedback-error { color: #f99; border-left-color: #a44;} /* Error feedback */
|
69 |
+
|
70 |
|
71 |
#stats-inventory-container {
|
72 |
margin-bottom: 20px;
|
|
|
100 |
#inventory-display .item-unknown { background-color: #555; border-color: #777;}
|
101 |
|
102 |
#choices-container {
|
103 |
+
margin-top: auto;
|
104 |
padding-top: 15px;
|
105 |
border-top: 1px solid #555;
|
106 |
}
|
|
|
118 |
.choice-button:hover:not(:disabled) { background-color: #d4a017; color: #222; border-color: #b8860b; }
|
119 |
.choice-button:disabled { background-color: #444; color: #888; cursor: not-allowed; border-color: #666; opacity: 0.7; }
|
120 |
|
121 |
+
.sell-button { background-color: #4a4a4a; border-color: #6a6a6a; }
|
122 |
+
.sell-button:hover:not(:disabled) { background-color: #a07017; border-color: #80500b; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
123 |
|
124 |
.roll-success { color: #7f7; border-left: 3px solid #4a4; padding-left: 8px; margin-bottom: 1em; font-size: 0.9em; }
|
125 |
.roll-failure { color: #f77; border-left: 3px solid #a44; padding-left: 8px; margin-bottom: 1em; font-size: 0.9em; }
|
|
|
156 |
<script type="module">
|
157 |
import * as THREE from 'three';
|
158 |
|
159 |
+
// DOM Element References (Checked - OK)
|
160 |
const sceneContainer = document.getElementById('scene-container');
|
161 |
const storyTitleElement = document.getElementById('story-title');
|
162 |
const storyContentElement = document.getElementById('story-content');
|
|
|
164 |
const statsElement = document.getElementById('stats-display');
|
165 |
const inventoryElement = document.getElementById('inventory-display');
|
166 |
|
167 |
+
// Global Three.js Variables (Checked - OK)
|
168 |
let scene, camera, renderer;
|
169 |
let currentAssemblyGroup = null;
|
170 |
|
171 |
+
// Materials (Checked - OK)
|
172 |
const stoneMaterial = new THREE.MeshStandardMaterial({ color: 0x888888, roughness: 0.8, metalness: 0.1 });
|
173 |
const woodMaterial = new THREE.MeshStandardMaterial({ color: 0x8B4513, roughness: 0.7, metalness: 0 });
|
174 |
const darkWoodMaterial = new THREE.MeshStandardMaterial({ color: 0x5C3D20, roughness: 0.7, metalness: 0 });
|
|
|
185 |
const wetStoneMaterial = new THREE.MeshStandardMaterial({ color: 0x2F4F4F, roughness: 0.7 });
|
186 |
const glowMaterial = new THREE.MeshStandardMaterial({ color: 0x00FFAA, emissive: 0x00FFAA, emissiveIntensity: 0.5 });
|
187 |
|
188 |
+
// --- Three.js Setup --- (Checked - OK)
|
189 |
function initThreeJS() {
|
190 |
+
if (!sceneContainer) { console.error("Scene container not found!"); return false; } // Return boolean on failure
|
191 |
+
try {
|
192 |
+
scene = new THREE.Scene();
|
193 |
+
scene.background = new THREE.Color(0x222222);
|
194 |
+
const width = sceneContainer.clientWidth;
|
195 |
+
const height = sceneContainer.clientHeight;
|
196 |
+
if (!width || !height) {
|
197 |
+
console.warn("Scene container has zero dimensions initially.");
|
198 |
+
// Use fallback or wait? For now, proceed but log.
|
199 |
+
}
|
200 |
+
camera = new THREE.PerspectiveCamera(75, (width / height) || 1, 0.1, 1000);
|
201 |
+
camera.position.set(0, 2.5, 7);
|
202 |
+
camera.lookAt(0, 0.5, 0);
|
203 |
+
renderer = new THREE.WebGLRenderer({ antialias: true });
|
204 |
+
renderer.setSize(width || 400, height || 300); // Use fallback dimensions
|
205 |
+
renderer.shadowMap.enabled = true;
|
206 |
+
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
207 |
+
sceneContainer.innerHTML = ''; // Clear previous content/errors
|
208 |
+
sceneContainer.appendChild(renderer.domElement);
|
209 |
+
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
|
210 |
+
scene.add(ambientLight);
|
211 |
+
window.addEventListener('resize', onWindowResize, false);
|
212 |
+
setTimeout(onWindowResize, 100);
|
213 |
+
animate();
|
214 |
+
return true; // Indicate success
|
215 |
+
} catch (error) {
|
216 |
+
console.error("Error during Three.js initialization:", error);
|
217 |
+
return false; // Indicate failure
|
218 |
+
}
|
219 |
}
|
220 |
|
221 |
function onWindowResize() {
|
|
|
226 |
camera.aspect = width / height;
|
227 |
camera.updateProjectionMatrix();
|
228 |
renderer.setSize(width, height);
|
229 |
+
} else {
|
230 |
+
console.warn("onWindowResize called with zero dimensions.");
|
231 |
}
|
232 |
}
|
233 |
|
234 |
function animate() {
|
235 |
+
// Guard against errors if renderer/scene is not valid
|
236 |
+
if (!renderer || !scene || !camera) {
|
237 |
+
// console.warn("Animation loop called before Three.js fully initialized or after error.");
|
238 |
+
return;
|
239 |
+
}
|
240 |
+
requestAnimationFrame(animate); // Request next frame first
|
241 |
+
try {
|
242 |
+
const time = performance.now() * 0.001;
|
243 |
+
scene.traverse(obj => {
|
244 |
+
if (obj.userData && obj.userData.update && typeof obj.userData.update === 'function') {
|
245 |
+
obj.userData.update(time);
|
246 |
+
}
|
247 |
+
});
|
248 |
+
renderer.render(scene, camera);
|
249 |
+
} catch (error) {
|
250 |
+
console.error("Error during animation/render loop:", error);
|
251 |
+
// Optionally stop the loop or display an error overlay
|
252 |
+
// For now, just log it to avoid crashing the browser if possible
|
253 |
+
}
|
254 |
}
|
255 |
|
256 |
function createMesh(geometry, material, position = { x: 0, y: 0, z: 0 }, rotation = { x: 0, y: 0, z: 0 }, scale = { x: 1, y: 1, z: 1 }) {
|
|
|
270 |
return ground;
|
271 |
}
|
272 |
|
273 |
+
// --- Procedural Generation Functions --- (Checked - OK)
|
274 |
+
// [Keep all create...Assembly functions here - unchanged from previous]
|
|
|
275 |
// ... (createDefaultAssembly, createCityGatesAssembly, ..., createDarkCaveAssembly) ...
|
276 |
function createDefaultAssembly() { const group = new THREE.Group(); const sphereGeo = new THREE.SphereGeometry(0.5, 16, 16); group.add(createMesh(sphereGeo, stoneMaterial, { x: 0, y: 0.5, z: 0 })); group.add(createGroundPlane()); return group; }
|
277 |
function createCityGatesAssembly() { const group = new THREE.Group(); const gh = 4, gw = 1.5, gd = 0.8, ah = 1, aw = 3; const tlGeo = new THREE.BoxGeometry(gw, gh, gd); group.add(createMesh(tlGeo, stoneMaterial, { x: -(aw / 2 + gw / 2), y: gh / 2, z: 0 })); const trGeo = new THREE.BoxGeometry(gw, gh, gd); group.add(createMesh(trGeo, stoneMaterial, { x: (aw / 2 + gw / 2), y: gh / 2, z: 0 })); const aGeo = new THREE.BoxGeometry(aw, ah, gd); group.add(createMesh(aGeo, stoneMaterial, { x: 0, y: gh - ah / 2, z: 0 })); const cs = 0.4; const cg = new THREE.BoxGeometry(cs, cs, gd * 1.1); for (let i = -1; i <= 1; i += 2) { group.add(createMesh(cg.clone(), stoneMaterial, { x: -(aw / 2 + gw / 2) + i * cs * 0.7, y: gh + cs / 2, z: 0 })); group.add(createMesh(cg.clone(), stoneMaterial, { x: (aw / 2 + gw / 2) + i * cs * 0.7, y: gh + cs / 2, z: 0 })); } group.add(createMesh(cg.clone(), stoneMaterial, { x: 0, y: gh + ah - cs / 2, z: 0 })); group.add(createGroundPlane(stoneMaterial)); return group; }
|
|
|
294 |
function createHiddenCoveAssembly() { const group = new THREE.Group(); group.add(createGroundPlane(sandMaterial, 15)); const caveGeo = new THREE.BoxGeometry(3, 2.5, 3); const caveMat = new THREE.MeshStandardMaterial({ color: 0x111111 }); group.add(createMesh(caveGeo, caveMat, { z: -6, y: 1.25 })); const rockGeo = new THREE.SphereGeometry(0.5, 6, 6); const rockMat = wetStoneMaterial.clone(); for (let i = 0; i < 15; i++) { group.add(createMesh(rockGeo, rockMat, { x: (Math.random() - 0.5) * 12, y: 0.25, z: (Math.random() - 0.5) * 12 }, { y: Math.random() * Math.PI })); } const seaweedGeo = new THREE.ConeGeometry(0.2, 1.2, 6); const seaweedMat = leafMaterial.clone().set({ color: 0x1E4D2B }); for (let i = 0; i < 10; i++) { group.add(createMesh(seaweedGeo, seaweedMat, { x: (Math.random() - 0.5) * 10, y: 0.6, z: (Math.random() - 0.5) * 10 + 2 }, { x: (Math.random() - 0.5) * 0.2, z: (Math.random() - 0.5) * 0.2 })); } return group; }
|
295 |
function createDarkCaveAssembly() { const group = new THREE.Group(); const caveRadius = 5; const caveHeight = 4; group.add(createGroundPlane(wetStoneMaterial, caveRadius * 2)); const wallGeo = new THREE.SphereGeometry(caveRadius, 32, 16, 0, Math.PI * 2, 0, Math.PI / 1.5); const wallMat = wetStoneMaterial.clone(); wallMat.side = THREE.BackSide; const wall = new THREE.Mesh(wallGeo, wallMat); wall.position.y = caveHeight * 0.6; group.add(wall); const stalactiteGeo = new THREE.ConeGeometry(0.1, 0.8, 8); const stalagmiteGeo = new THREE.ConeGeometry(0.15, 0.5, 8); for (let i = 0; i < 15; i++) { const x = (Math.random() - 0.5) * caveRadius * 1.5; const z = (Math.random() - 0.5) * caveRadius * 1.5; if (Math.random() > 0.5) { group.add(createMesh(stalactiteGeo, wetStoneMaterial, { x: x, y: caveHeight - 0.4, z: z })) } else { group.add(createMesh(stalagmiteGeo, wetStoneMaterial, { x: x, y: 0.25, z: z })) } } const dripGeo = new THREE.SphereGeometry(0.05, 8, 8); for (let i = 0; i < 5; i++) { const drip = createMesh(dripGeo, oceanMaterial, { x: (Math.random() - 0.5) * caveRadius, y: caveHeight - 0.2, z: (Math.random() - 0.5) * caveRadius }); drip.userData.startY = caveHeight - 0.2; drip.userData.update = (time) => { drip.position.y -= 0.1; if (drip.position.y < 0) { drip.position.y = drip.userData.startY; drip.position.x = (Math.random() - 0.5) * caveRadius; drip.position.z = (Math.random() - 0.5) * caveRadius; } }; group.add(drip); } return group; }
|
296 |
|
|
|
297 |
// ========================================
|
298 |
+
// Game Data (Checked - OK, added goldValue)
|
299 |
// ========================================
|
300 |
const itemsData = {
|
301 |
"Flaming Sword": {type:"weapon", description:"A legendary blade, wreathed in magical fire.", goldValue: 500},
|
|
|
304 |
"Healing Light Spell":{type:"spell", description:"A scroll containing the incantation to mend minor wounds.", goldValue: 50},
|
305 |
"Shield of Faith Spell":{type:"spell",description:"A scroll containing a prayer that grants temporary magical protection.", goldValue: 75},
|
306 |
"Binding Runes Scroll":{type:"spell", description:"Complex runes scribbled on parchment, said to temporarily immobilize a foe.", goldValue: 100},
|
307 |
+
"Secret Tunnel Map": {type:"quest", description:"A crudely drawn map showing a hidden path, perhaps into the fortress?", goldValue: 0}, // Quest items non-sellable
|
308 |
"Poison Daggers": {type:"weapon", description:"A pair of wicked-looking daggers coated in a fast-acting toxin.", goldValue: 150},
|
309 |
+
"Master Key": {type:"quest", description:"An ornate key rumored to unlock many doors, though perhaps not all.", goldValue: 0},
|
310 |
"Crude Dagger": {type:"weapon", description:"A roughly made dagger, chipped and stained.", goldValue: 10},
|
311 |
+
"Scout's Pouch": {type:"quest", description:"A small leather pouch containing flint & steel, jerky, and some odd coins.", goldValue: 20} // Pouch itself has value
|
|
|
312 |
};
|
313 |
|
314 |
+
const gameData = { // (Checked - OK, added allowSell flag to 99)
|
315 |
+
// [Keep all page data here - unchanged from previous]
|
316 |
+
// ... (pages 1 to 224) ...
|
317 |
+
"1": { title: "The Crossroads", content: `<p>Dust swirls around a weathered signpost under a bright, midday sun. Paths lead north into the gloomy Shadowwood, east towards rolling green hills, and west towards coastal cliffs battered by sea spray. Which path calls to you?</p>`, options: [ { text: "Enter the Shadowwood Forest (North)", next: 5 }, { text: "Head towards the Rolling Hills (East)", next: 2 }, { text: "Investigate the Coastal Cliffs (West)", next: 3 } ], illustration: "crossroads-signpost-sunny" },
|
318 |
+
"2": { title: "Rolling Hills", content: `<p>Verdant hills stretch before you, dotted with wildflowers. A gentle breeze whispers through the tall grass. In the distance, you see a lone figure tending to a flock of sheep. It feels peaceful, almost unnervingly so after the crossroads.</p>`, options: [ { text: "Follow the narrow path winding through the hills", next: 4 }, { text: "Try to hail the distant shepherd (Charisma Check?)", next: 99 } ], illustration: "rolling-green-hills-shepherd-distance" },
|
319 |
+
"3": { title: "Coastal Cliffs Edge", content: `<p>You stand atop windswept cliffs, the roar of crashing waves filling the air below. Seabirds circle overhead. A precarious-looking path, seemingly carved by desperate hands, descends the cliff face towards a hidden cove.</p>`, options: [ { text: "Attempt the precarious descent (Dexterity Check)", check: { stat: 'dexterity', dc: 12, onFailure: 31 }, next: 30 }, { text: "Scan the cliff face for easier routes (Wisdom Check)", check: { stat: 'wisdom', dc: 11, onFailure: 32 }, next: 33 } ], illustration: "windy-sea-cliffs-crashing-waves-path-down" },
|
320 |
+
"4": { title: "Hill Path Overlook", content: `<p>The path crests a hill, offering a panoramic view. To the east, the hills gradually give way to rugged, barren badlands. Nearby, nestled amongst wildflowers, you spot a small, ancient-looking shrine, heavily overgrown with vines.</p>`, options: [ { text: "Investigate the overgrown shrine", next: 40 }, { text: "Continue east towards the badlands", next: 41 } ], illustration: "hilltop-view-overgrown-shrine-wildflowers" },
|
321 |
+
"5": { title: "Shadowwood Entrance", content: `<p>The air grows cool and damp as you step beneath the dense canopy of the Shadowwood. Sunlight struggles to pierce the gloom, illuminating gnarled roots that writhe across the forest floor. A narrow, overgrown path leads deeper into the woods.</p>`, options: [ { text: "Follow the main, albeit overgrown, path", next: 6 }, { text: "Try to navigate through the lighter undergrowth beside the path", next: 7 }, { text: "Look for animal trails or signs of passage (Wisdom Check)", check: { stat: 'wisdom', dc: 10, onFailure: 6 }, next: 8 } ], illustration: "dark-forest-entrance-gnarled-roots-filtered-light" },
|
322 |
+
"6": { title: "Overgrown Forest Path", content: `<p>The path is barely visible beneath a thick layer of fallen leaves and creeping vines. Strange, faintly glowing fungi cling to rotting logs. You push deeper into the oppressive silence when suddenly, you hear a twig snap nearby!</p>`, options: [ { text: "Ready your weapon and investigate the sound", next: 10 }, { text: "Attempt to hide quietly amongst the ferns (Dexterity Check)", check: { stat: 'dexterity', dc: 11, onFailure: 10 }, next: 11 }, { text: "Call out cautiously, 'Who's there?'", next: 10 } ], illustration: "overgrown-forest-path-glowing-fungi-vines" },
|
323 |
+
"7": { title: "Tangled Undergrowth", content: `<p>Pushing through thick ferns and thorny bushes proves difficult. You stumble into a small, unexpected clearing. In the center stands a weathered stone statue, its features eroded by time and covered in thick moss. It depicts a forgotten deity or hero.</p>`, options: [ { text: "Examine the statue closely for clues or markings (Intelligence Check)", check: { stat: 'intelligence', dc: 13, onFailure: 71 }, next: 70 }, { text: "Ignore the statue and try to find the main path again", next: 72 }, { text: "Leave a small offering (if you have something suitable)", next: 73 } ], illustration: "forest-clearing-mossy-statue-weathered-stone" },
|
324 |
+
"8": { title: "Hidden Game Trail", content: `<p>Your sharp eyes spot a faint trail, almost invisible to the untrained observer, diverging from the main path. It looks like a route used by deer or other forest creatures. Following it, you soon arrive at the edge of a deep ravine spanned by a single, rickety rope bridge.</p><p>(+20 XP)</p>`, options: [ { text: "Risk crossing the rope bridge (Dexterity Check)", check: { stat: 'dexterity', dc: 10, onFailure: 81 }, next: 80 }, { text: "Search along the ravine edge for another way across", next: 82 } ], illustration: "narrow-game-trail-forest-rope-bridge-ravine", reward: { xp: 20 } },
|
325 |
+
"10": { title: "Goblin Ambush!", content: `<p>Suddenly, two scraggly goblins, clad in mismatched leather scraps and wielding crude, sharp spears, leap out from behind large toadstools! Their beady eyes fix on you with malicious intent.</p>`, options: [ { text: "Fight the goblins!", next: 12 }, { text: "Attempt to dodge past them and flee down the path (Dexterity Check)", check: { stat: 'dexterity', dc: 13, onFailure: 10 }, next: 13 } ], illustration: "two-goblins-ambush-forest-path-spears" },
|
326 |
+
"11": { title: "Hidden Evasion", content: `<p>Quickly and silently, you melt into the deep shadows beneath a large, ancient tree. The two goblins blunder past, bickering in their guttural tongue, completely oblivious to your presence.</p><p>(+30 XP)</p>`, options: [ { text: "Continue cautiously down the path once they are gone", next: 14 } ], illustration: "forest-shadows-hiding-goblins-walking-past", reward: { xp: 30 } },
|
327 |
+
"12": { title: "Ambush Victory!", content: `<p>Though caught by surprise, you react swiftly. After a brief, vicious skirmish, the goblins lie defeated at your feet. Searching their meagre belongings, you find a single, Crude Dagger.</p><p>(+50 XP)</p>`, options: [ { text: "Wipe your blade clean and press onward", next: 14 } ], illustration: "defeated-goblins-forest-path-loot", reward: { xp: 50, addItem: "Crude Dagger" } },
|
328 |
+
"13": { title: "Daring Escape", content: `<p>With surprising agility, you feint left, then dive right, tumbling past the goblins' clumsy spear thrusts! You scramble to your feet and sprint down the path, leaving the surprised goblins behind.</p><p>(+25 XP)</p>`, options: [ { text: "Keep running!", next: 14 } ], illustration: "blurred-motion-running-past-goblins-forest", reward: { xp: 25 } },
|
329 |
+
"14": { title: "Forest Stream Crossing", content: `<p>The overgrown path eventually leads to the bank of a clear, shallow stream. Smooth, mossy stones line the streambed, and dappled sunlight filters through the leaves overhead, sparkling on the water's surface.</p>`, options: [ { text: "Wade across the stream", next: 16 }, { text: "Look for a drier crossing point (fallen log?) upstream", next: 15 } ], illustration: "forest-stream-crossing-dappled-sunlight-stones" },
|
330 |
+
"15": { title: "Log Bridge", content: `<p>A short walk upstream reveals a large, fallen tree spanning the stream. It's covered in slick, green moss, making it look like a potentially treacherous crossing.</p>`, options: [ { text: "Cross carefully on the mossy log (Dexterity Check)", check: { stat: 'dexterity', dc: 9, onFailure: 151 }, next: 16 }, { text: "Decide it's too risky and go back to wade across", next: 14 } ], illustration: "mossy-log-bridge-over-forest-stream" },
|
331 |
+
"151": { title: "Splash!", content: `<p>You place a foot carefully on the log, but the moss is slicker than it looks! Your feet shoot out from under you, and you tumble into the cold stream with a loud splash! You're soaked and slightly embarrassed, but otherwise unharmed.</p>`, options: [ { text: "Shake yourself off and continue on the other side", next: 16 } ], illustration: "character-splashing-into-stream-from-log" },
|
332 |
+
"16": { title: "Edge of the Woods", content: `<p>Finally, the trees begin to thin, and you emerge from the oppressive gloom of the Shadowwood. Before you lie steep, rocky foothills leading up towards a formidable-looking mountain fortress perched high above.</p>`, options: [ { text: "Begin the ascent into the foothills towards the fortress", next: 17 }, { text: "Scan the fortress and surrounding terrain from afar (Wisdom Check)", check: { stat: 'wisdom', dc: 14, onFailure: 17 }, next: 18 } ], illustration: "forest-edge-view-rocky-foothills-distant-mountain-fortress" },
|
333 |
+
"17": { title: "Rocky Foothills Path", content: `<p>The climb is arduous, the path winding steeply upwards over loose scree and jagged rocks. The air thins slightly. The dark stone walls of the mountain fortress loom much larger now, seeming to watch your approach.</p>`, options: [ { text: "Continue the direct ascent", next: 19 }, { text: "Look for signs of a hidden trail or less obvious route (Wisdom Check)", check: { stat: 'wisdom', dc: 15, onFailure: 19 }, next: 20 } ], illustration: "climbing-rocky-foothills-path-fortress-closer" },
|
334 |
+
"18": { title: "Distant Observation", content: `<p>Taking a moment to study the fortress from this distance, your keen eyes notice something interesting. The main approach looks heavily guarded, but along the western ridge, the terrain seems slightly less sheer, potentially offering a less-guarded, albeit more treacherous, approach.</p><p>(+30 XP)</p>`, options: [ { text: "Decide against the risk and take the main path into the foothills", next: 17 }, { text: "Attempt the western ridge approach", next: 21 } ], illustration: "zoomed-view-mountain-fortress-western-ridge", reward: { xp: 30 } },
|
335 |
+
"19": { title: "Blocked Pass", content: `<p>As you round a sharp bend, your way is completely blocked by a recent rockslide! Huge boulders and debris choke the path, making further progress impossible along this route.</p>`, options: [ { text: "Try to climb over the unstable rockslide (Strength Check)", check: { stat: 'strength', dc: 14, onFailure: 191 }, next: 190 }, { text: "Search the surrounding cliffs for another way around", next: 192 } ], illustration: "rockslide-blocking-mountain-path-boulders" },
|
336 |
+
"20": { title: "Goat Trail", content: `<p>Your thorough search pays off! Partially hidden behind a cluster of hardy mountain shrubs, you discover a narrow trail, barely wide enough for a single person (or perhaps a mountain goat). It seems to bypass the main path, heading upwards towards the fortress.</p><p>(+40 XP)</p>`, options: [ { text: "Follow the precarious goat trail", next: 22 } ], illustration: "narrow-goat-trail-mountainside-fortress-view", reward: { xp: 40 } },
|
337 |
+
"21": { title: "Western Ridge", content:"<p>The path along the western ridge is dangerously narrow and exposed. Loose gravel shifts underfoot, and strong gusts of wind whip around you, threatening to push you off the edge into the dizzying drop below.</p>", options: [{text:"Proceed carefully along the ridge (Dexterity Check)", check:{stat:'dexterity', dc: 14, onFailure: 211}, next: 22 } ], illustration:"narrow-windy-mountain-ridge-path" },
|
338 |
+
"22": { title: "Fortress Approach", content:"<p>You've navigated the treacherous paths and finally stand near the imposing outer walls of the dark mountain fortress. Stern-faced guards patrol the battlements, their eyes scanning the approaches. The main gate looks heavily fortified.</p>", options: [ {text:"Search for a less obvious entrance (Wisdom Check)", check:{stat:'wisdom', dc: 16, onFailure: 221}, next: 220}, {text:"Attempt to bluff your way past the gate guards (Charisma Check)", check:{stat:'charisma', dc: 15, onFailure: 222}, next: 223}, {text:"Try to sneak past the gate guards (Dexterity Check)", check:{stat:'dexterity', dc: 17, onFailure: 222}, next: 224}, {text:"Retreat for now", next: 16} ], illustration:"approaching-dark-fortress-walls-guards"},
|
339 |
+
"30": { title: "Hidden Cove", content: `<p>Your careful descent, whether via the main path or hidden steps, brings you safely to a secluded, sandy cove sheltered by the towering cliffs. The air smells strongly of salt and seaweed. Half-hidden in the shadows at the back of the cove is the dark, foreboding entrance to a sea cave.</p><p>(+25 XP)</p>`, options: [ { text: "Explore the dark cave", next: 35 } ], illustration: "hidden-cove-beach-dark-cave-entrance", reward: { xp: 25 } },
|
340 |
+
"31": { title: "Tumbled Down", content: `<p>You lose your footing on the steep, treacherous path! You tumble and slide the last few feet, landing hard on the sandy cove floor. You take 5 points of damage from the fall. Shaking your head to clear it, you see the dark entrance to a sea cave nearby.</p>`, options: [ { text: "Gingerly get up and explore the dark cave", next: 35 } ], illustration: "character-fallen-at-bottom-of-cliff-path-cove", hpLoss: 5 },
|
341 |
+
"32": { title: "No Easier Path", content: `<p>You scan the towering cliffs intently, searching for any alternative routes down. Despite your efforts, you find no obviously easier or safer paths than the precarious one directly before you.</p>`, options: [ { text: "Attempt the precarious descent again (Dexterity Check)", check: { stat: 'dexterity', dc: 12, onFailure: 31 }, next: 30 } ], illustration: "scanning-sea-cliffs-no-other-paths-visible" },
|
342 |
+
"33": { title: "Smuggler's Steps?", content: `<p>Your keen eyes spot what others might miss: a series of barely visible handholds and footholds carved into the rock face, slightly hidden by an overhang. They look old but might offer a slightly less treacherous descent.</p><p>(+15 XP)</p>`, options: [ { text: "Use the hidden steps (Easier Dex Check)", check: { stat: 'dexterity', dc: 8, onFailure: 31 }, next: 30 } ], illustration: "close-up-handholds-carved-in-cliff-face", reward: { xp: 15 } },
|
343 |
+
"35": { title: "Dark Cave", content: `<p>You cautiously enter the sea cave. The air inside is heavy with the smell of salt, damp rock, and something else... decay. Water drips rhythmically from unseen stalactites somewhere deeper within the oppressive darkness.</p>`, options: [{ text: "Press deeper into the darkness (Requires Light Source?)", next: 99 } ], illustration: "dark-cave-entrance-dripping-water" },
|
344 |
+
"40": { title: "Overgrown Shrine", content: `<p>Pushing aside thick vines reveals a small stone shrine dedicated to a forgotten nature deity. Intricate carvings, though worn, are still visible beneath the moss and grime. A sense of ancient peace emanates from the stones. Wildflowers grow in profusion around its base.</p>`, options: [{ text: "Examine the carvings for meaning (Intelligence Check)", check:{stat:'intelligence', dc:11, onFailure: 401}, next: 400 }, {text: "Leave the shrine undisturbed", next: 4}, {text: "Say a quiet prayer for guidance", next: 402}], illustration: "overgrown-stone-shrine-wildflowers-close" },
|
345 |
+
"41": { title: "Rocky Badlands", content: `<p>The gentle green hills give way abruptly to cracked, sun-baked earth and jagged rock formations. The air is hot and still under a harsh, unforgiving sun. This land looks hostile and sparsely populated.</p>`, options: [{ text: "Scout ahead cautiously", next: 99 } ], illustration: "rocky-badlands-cracked-earth-harsh-sun" },
|
346 |
+
"70": { title: "Statue's Secret", content:"<p>Running your fingers over the mossy stone, you find a small, almost invisible seam near the base. Applying pressure, a hidden compartment clicks open! Inside is a Scout's Pouch.</p><p>(+40 XP)</p>", options: [{text:"Take the pouch and press on", next: 72}], illustration: "forest-clearing-mossy-statue-hidden-compartment", reward:{xp: 40, addItem: "Scout's Pouch"}},
|
347 |
+
"71": { title: "Just an Old Statue", content:"<p>Despite a careful examination, the statue appears to be just that – an old, weathered stone figure of no special significance that you can discern.</p>", options: [{text:"Ignore the statue and press on", next: 72}], illustration: "forest-clearing-mossy-statue-weathered-stone"},
|
348 |
+
"72": { title: "Back to the Thicket", content:"<p>Leaving the clearing and the statue behind, you push back into the dense undergrowth, eventually relocating the main forest path.</p>", options: [{text:"Continue along the main path", next: 6}], illustration:"pushing-through-forest-undergrowth"},
|
349 |
+
"73": { title: "A Small Offering", content:"<p>You place a small, simple offering at the statue's base (a ration, a coin, or perhaps just a moment of respect). You feel a subtle sense of approval or peace before turning to leave.</p>", options: [{text:"Try to find the main path again", next: 72}], illustration:"forest-clearing-mossy-statue-offering"},
|
350 |
+
"80": { title: "Across the Ravine", content:"<p>Taking a deep breath, you step onto the swaying rope bridge. With careful, deliberate steps, testing each plank before putting your weight on it, you make your way across the chasm to the other side.</p><p>(+25 XP)</p>", options: [{text:"Continue following the game trail", next: 14}], illustration:"character-crossing-rope-bridge-safely", reward:{xp:25}},
|
351 |
+
"81": { title: "Bridge Collapse!", content:"<p>Halfway across, a frayed rope snaps! The bridge lurches violently, sending you plunging into the ravine below! You lose 10 HP. Luckily, the bottom is covered in soft moss and mud, cushioning your fall.</p>", options: [{text:"Climb out and find another way", next: 82}], illustration:"rope-bridge-snapping-character-falling", hpLoss: 10},
|
352 |
+
"82": { title: "Ravine Detour", content:"<p>Searching along the ravine's edge, you eventually find a place where the chasm narrows, and a fallen log provides a much safer, if longer, way across.</p>", options: [{text:"Cross the log bridge and continue", next: 14}], illustration:"fallen-log-crossing-ravine"},
|
353 |
+
"151": { title: "Splash!", content: `<p>You place a foot carefully on the log, but the moss is slicker than it looks! Your feet shoot out from under you, and you tumble into the cold stream with a loud splash! You're soaked and slightly embarrassed, but otherwise unharmed.</p>`, options: [ { text: "Shake yourself off and continue on the other side", next: 16 } ], illustration: "character-splashing-into-stream-from-log" },
|
354 |
+
"190": { title: "Over the Rocks", content:"<p>Summoning your strength, you find handholds and footholds, scrambling and pulling yourself up and over the precarious rockslide. It's exhausting work, but you make it past the blockage.</p><p>(+35 XP)</p>", options: [{text:"Continue up the now clear path", next: 22}], illustration:"character-climbing-over-boulders", reward: {xp:35} },
|
355 |
+
"191": { title: "Climb Fails", content:"<p>The boulders are too large, too smooth, or too unstable. You try several approaches, but cannot safely climb over the rockslide. This way is blocked.</p>", options: [{text:"Search the surrounding cliffs for another way around", next: 192}], illustration:"character-slipping-on-rockslide-boulders"},
|
356 |
+
"192": { title: "Detour Found", content:"<p>After considerable searching along the cliff face, you find a rough, overgrown path leading steeply up and around the rockslide area. It eventually rejoins the main trail further up the mountain.</p>", options: [{text:"Follow the detour path", next: 22}], illustration:"rough-detour-path-around-rockslide"},
|
357 |
+
"211": {title:"Lost Balance", content:"<p>A particularly strong gust of wind catches you at a bad moment! You lose your balance and stumble, tumbling down a steep, rocky slope before managing to arrest your fall. You lose 10 HP.</p>", options:[{text:"Climb back up and reconsider the main path", next: 17}], illustration:"character-falling-off-windy-ridge", hpLoss: 10},
|
358 |
+
"220": { title: "Secret Passage?", content:"<p>Your careful search reveals loose stones near the base of the wall, potentially hiding a passage!</p>", options: [{text:"Investigate the loose stones", next: 99}], illustration:"approaching-dark-fortress-walls-guards"},
|
359 |
+
"221": { title: "No Obvious Weakness", content:"<p>The fortress walls look solid and well-maintained. You find no obvious weak points or hidden entrances from this vantage point.</p>", options: [{text:"Reconsider your approach", next: 22}], illustration:"approaching-dark-fortress-walls-guards"},
|
360 |
+
"222": { title: "Caught!", content:"<p>Your attempt fails miserably! The guards spot you immediately and raise the alarm! You are captured.</p>", options: [{text:"To the dungeons...", next: 99}], illustration:"approaching-dark-fortress-walls-guards"},
|
361 |
+
"223": { title: "Bluff Success?", content:"<p>Amazingly, your story seems plausible enough for the guards to let you pass through the gate!</p>", options: [{text:"Enter the fortress courtyard", next: 99}], illustration:"approaching-dark-fortress-walls-guards"},
|
362 |
+
"224": { title: "Sneak Success?", content:"<p>Moving like a shadow, you manage to slip past the gate guards unnoticed!</p>", options: [{text:"Enter the fortress courtyard", next: 99}], illustration:"approaching-dark-fortress-walls-guards"},
|
363 |
+
"400": { title: "Shrine Insights", content:"<p>The carvings depict cycles of growth and renewal. You feel a sense of calm wash over you, slightly restoring your vitality. (+1 HP)</p>", options: [{text:"Continue towards the badlands", next: 41}], illustration: "overgrown-stone-shrine-wildflowers-close", reward: {hpGain: 1}},
|
364 |
+
"401": { title: "Mysterious Carvings", content:"<p>The carvings are too worn and abstract to decipher their specific meaning, though you sense they are very old.</p>", options: [{text:"Continue towards the badlands", next: 41}], illustration: "overgrown-stone-shrine-wildflowers-close"},
|
365 |
+
"402": { title: "Moment of Peace", content:"<p>You spend a quiet moment in reflection. While no divine voice answers, the tranquility of the place settles your nerves.</p>", options: [{text:"Continue towards the badlands", next: 41}], illustration: "overgrown-stone-shrine-wildflowers-close"},
|
366 |
+
|
367 |
+
// Game Over Page (allowSell enabled)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
368 |
"99": {
|
369 |
title: "Game Over / To Be Continued...",
|
370 |
+
content: "<p>Your adventure ends here... for now. You can sell unwanted items for gold before starting again.</p>",
|
371 |
+
options: [ /* Restart button added dynamically */ ],
|
|
|
|
|
|
|
372 |
illustration: "game-over-generic",
|
373 |
+
gameOver: true,
|
374 |
+
allowSell: true // Enable selling feature on this page
|
375 |
}
|
376 |
};
|
377 |
|
|
|
378 |
// ========================================
|
379 |
+
// Game State (Checked - OK, uses default)
|
380 |
// ========================================
|
|
|
381 |
const defaultCharacterState = {
|
382 |
name: "Hero", race: "Human", alignment: "Neutral Good", class: "Adventurer",
|
383 |
+
level: 1, xp: 0, xpToNextLevel: 100, gold: 0,
|
384 |
stats: { strength: 8, intelligence: 10, wisdom: 10, dexterity: 10, constitution: 10, charisma: 8, hp: 12, maxHp: 12 },
|
385 |
inventory: []
|
386 |
};
|
|
|
|
|
387 |
let gameState = {
|
388 |
currentPageId: 1,
|
389 |
+
character: JSON.parse(JSON.stringify(defaultCharacterState)) // Start with default
|
|
|
|
|
|
|
390 |
};
|
391 |
|
|
|
392 |
// ========================================
|
393 |
+
// Game Logic Functions (Checked - OK)
|
394 |
// ========================================
|
395 |
|
|
|
396 |
function startNewGame() {
|
397 |
console.log("Starting brand new game...");
|
|
|
398 |
gameState = {
|
399 |
currentPageId: 1,
|
400 |
+
character: JSON.parse(JSON.stringify(defaultCharacterState)) // Full reset
|
|
|
401 |
};
|
402 |
+
renderPage(gameState.currentPageId); // Render page 1
|
403 |
}
|
404 |
|
|
|
405 |
function restartGamePlus() {
|
406 |
console.log("Restarting game (keeping progress)...");
|
407 |
+
// Only reset current location, keep character object
|
408 |
+
gameState.currentPageId = 1;
|
409 |
+
renderPage(gameState.currentPageId); // Render page 1
|
410 |
}
|
411 |
|
412 |
function handleChoiceClick(choiceData) {
|
413 |
console.log("Choice clicked:", choiceData);
|
414 |
+
let feedbackMessage = ""; // Message passed to render function
|
415 |
|
416 |
+
// --- Special Actions ---
|
417 |
if (choiceData.action === 'restart_plus') {
|
418 |
restartGamePlus();
|
419 |
return;
|
420 |
}
|
421 |
if (choiceData.action === 'sell_item') {
|
422 |
+
// handleSellItem now returns the feedback message
|
423 |
+
feedbackMessage = handleSellItem(choiceData.item);
|
424 |
+
// Re-render the current page (99) with the feedback
|
425 |
+
renderPageInternal(gameState.currentPageId, gameData[gameState.currentPageId], feedbackMessage);
|
426 |
return;
|
427 |
}
|
428 |
|
|
|
430 |
const optionNextPageId = parseInt(choiceData.next);
|
431 |
const itemToAdd = choiceData.addItem;
|
432 |
let nextPageId = optionNextPageId;
|
|
|
|
|
433 |
const check = choiceData.check;
|
434 |
|
|
|
435 |
if (isNaN(optionNextPageId) && !check) {
|
436 |
console.error("Invalid choice data: Missing 'next' page ID and no check defined.", choiceData);
|
437 |
+
feedbackMessage = `<p class="feedback-error">Error: Invalid choice data! Cannot proceed.</p>`;
|
438 |
+
renderPageInternal(gameState.currentPageId, gameData[gameState.currentPageId], feedbackMessage);
|
439 |
choicesElement.querySelectorAll('button').forEach(b => b.disabled = true);
|
440 |
return;
|
441 |
}
|
442 |
|
443 |
+
// --- Skill Check ---
|
444 |
if (check) {
|
445 |
const statValue = gameState.character.stats[check.stat] || 10;
|
446 |
const modifier = Math.floor((statValue - 10) / 2);
|
|
|
449 |
const dc = check.dc;
|
450 |
const statName = check.stat.charAt(0).toUpperCase() + check.stat.slice(1);
|
451 |
console.log(`Check: ${statName} (DC ${dc}) | Roll: ${roll} + Mod: ${modifier} = ${totalResult}`);
|
452 |
+
if (totalResult >= dc) {
|
|
|
453 |
nextPageId = optionNextPageId;
|
454 |
+
feedbackMessage += `<p class="roll-success"><em>${statName} Check Success! (${totalResult} vs DC ${dc})</em></p>`;
|
455 |
+
} else {
|
456 |
nextPageId = parseInt(check.onFailure);
|
457 |
+
feedbackMessage += `<p class="roll-failure"><em>${statName} Check Failed! (${totalResult} vs DC ${dc})</em></p>`;
|
458 |
if (isNaN(nextPageId)) {
|
459 |
console.error("Invalid onFailure ID:", check.onFailure);
|
460 |
nextPageId = 99;
|
461 |
+
feedbackMessage += `<p class="feedback-error">Error: Invalid failure path defined!</p>`;
|
462 |
}
|
463 |
}
|
464 |
}
|
|
|
467 |
const targetPageData = gameData[nextPageId];
|
468 |
if (!targetPageData) {
|
469 |
console.error(`Data for target page ${nextPageId} not found!`);
|
470 |
+
feedbackMessage = `<p class="feedback-error">Error: Next page data missing! Cannot continue.</p>`;
|
471 |
+
renderPageInternal(99, gameData[99], feedbackMessage); // Go to game over
|
472 |
return;
|
473 |
}
|
474 |
|
|
|
475 |
let hpLostThisTurn = 0;
|
476 |
if (targetPageData.hpLoss) {
|
477 |
hpLostThisTurn = targetPageData.hpLoss;
|
|
|
484 |
console.log(`Gained ${hpGained} HP.`);
|
485 |
}
|
486 |
|
|
|
487 |
if (gameState.character.stats.hp <= 0) {
|
488 |
gameState.character.stats.hp = 0;
|
489 |
console.log("Player died!");
|
490 |
+
nextPageId = 99;
|
491 |
+
feedbackMessage += `<p class="feedback-error"><em>You have succumbed to your injuries!${hpLostThisTurn > 0 ? ` (-${hpLostThisTurn} HP)` : ''}</em></p>`;
|
492 |
+
renderPageInternal(nextPageId, gameData[nextPageId], feedbackMessage); // Render game over immediately
|
493 |
+
return;
|
|
|
494 |
}
|
495 |
|
|
|
496 |
if (targetPageData.reward) {
|
497 |
if (targetPageData.reward.xp) {
|
498 |
gameState.character.xp += targetPageData.reward.xp;
|
499 |
console.log(`Gained ${targetPageData.reward.xp} XP! Total: ${gameState.character.xp}`);
|
500 |
+
// checkLevelUp();
|
501 |
}
|
502 |
if (targetPageData.reward.statIncrease) {
|
503 |
const stat = targetPageData.reward.statIncrease.stat;
|
|
|
509 |
}
|
510 |
}
|
511 |
if (targetPageData.reward.addItem && !gameState.character.inventory.includes(targetPageData.reward.addItem)) {
|
512 |
+
const itemName = targetPageData.reward.addItem;
|
513 |
+
// Check if item exists in itemsData before adding
|
514 |
+
if (itemsData[itemName]) {
|
515 |
+
gameState.character.inventory.push(itemName);
|
516 |
+
console.log(`Found item: ${itemName}`);
|
517 |
+
feedbackMessage += `<p class="feedback-message"><em>Item acquired: ${itemName}</em></p>`;
|
518 |
+
} else {
|
519 |
+
console.warn(`Attempted to add unknown item from reward: ${itemName}`);
|
520 |
+
feedbackMessage += `<p class="feedback-error"><em>Error: Tried to acquire unknown item '${itemName}'!</em></p>`;
|
521 |
+
}
|
522 |
}
|
523 |
}
|
524 |
if (itemToAdd && !gameState.character.inventory.includes(itemToAdd)) {
|
525 |
+
if (itemsData[itemToAdd]) { // Check item exists
|
526 |
+
gameState.character.inventory.push(itemToAdd);
|
527 |
+
console.log("Added item:", itemToAdd);
|
528 |
+
feedbackMessage += `<p class="feedback-message"><em>Item acquired: ${itemToAdd}</em></p>`;
|
529 |
+
} else {
|
530 |
+
console.warn(`Attempted to add unknown item from choice: ${itemToAdd}`);
|
531 |
+
feedbackMessage += `<p class="feedback-error"><em>Error: Tried to acquire unknown item '${itemToAdd}'!</em></p>`;
|
532 |
+
}
|
533 |
}
|
534 |
|
|
|
535 |
gameState.currentPageId = nextPageId;
|
536 |
recalculateMaxHp();
|
537 |
gameState.character.stats.hp = Math.min(gameState.character.stats.hp, gameState.character.stats.maxHp);
|
538 |
|
539 |
console.log("Transitioning to page:", nextPageId, " New state:", JSON.stringify(gameState));
|
540 |
+
renderPageInternal(nextPageId, gameData[nextPageId], feedbackMessage);
|
541 |
}
|
542 |
|
543 |
+
// Returns feedback message string
|
544 |
function handleSellItem(itemName) {
|
545 |
console.log("Attempting to sell:", itemName);
|
546 |
const itemIndex = gameState.character.inventory.indexOf(itemName);
|
547 |
const itemInfo = itemsData[itemName];
|
548 |
+
let message = "";
|
549 |
+
|
550 |
+
// Add extra checks
|
551 |
+
if (itemIndex === -1) {
|
552 |
+
console.warn(`Sell failed: Item "${itemName}" not in inventory.`);
|
553 |
+
message = `<p class="feedback-error">Cannot sell ${itemName} - you don't have it!</p>`;
|
554 |
+
} else if (!itemInfo) {
|
555 |
+
console.warn(`Sell failed: Item data for "${itemName}" not found.`);
|
556 |
+
message = `<p class="feedback-error">Cannot sell ${itemName} - item data missing!</p>`;
|
557 |
+
} else if (itemInfo.type === 'quest') {
|
558 |
+
console.log(`Sell blocked: Item "${itemName}" is a quest item.`);
|
559 |
+
message = `<p class="feedback-message">Cannot sell ${itemName} - it seems important.</p>`;
|
560 |
+
} else if (itemInfo.goldValue === undefined || itemInfo.goldValue <= 0) {
|
561 |
+
console.log(`Sell blocked: Item "${itemName}" has no gold value.`);
|
562 |
+
message = `<p class="feedback-message">${itemName} isn't worth any gold.</p>`;
|
563 |
+
} else {
|
564 |
+
// Proceed with selling
|
565 |
+
const value = itemInfo.goldValue;
|
566 |
+
gameState.character.gold += value;
|
567 |
+
gameState.character.inventory.splice(itemIndex, 1);
|
568 |
+
message = `<p class="feedback-success">Sold ${itemName} for ${value} Gold.</p>`;
|
569 |
+
console.log(`Sold ${itemName} for ${value} gold. Current gold: ${gameState.character.gold}`);
|
570 |
+
}
|
571 |
+
return message; // Return the message to be displayed
|
572 |
}
|
573 |
|
574 |
|
575 |
+
function recalculateMaxHp() { // Checked - OK
|
576 |
+
const baseHp = 10;
|
577 |
const conModifier = Math.floor((gameState.character.stats.constitution - 10) / 2);
|
578 |
+
gameState.character.stats.maxHp = Math.max(1, baseHp + conModifier * gameState.character.level);
|
579 |
}
|
580 |
|
581 |
+
function renderPageInternal(pageId, pageData, message = "") { // Checked - OK (added sell buttons)
|
582 |
if (!pageData) {
|
583 |
console.error(`Render Error: No data for page ${pageId}`);
|
584 |
+
pageData = gameData["99"] || { title: "Error", content: "<p>Render Error! Critical page data missing.</p>", illustration: "error", gameOver: true };
|
585 |
+
message += `<p class="feedback-error">Render Error: Page data for ID ${pageId} was missing!</p>`;
|
586 |
pageId = 99;
|
587 |
}
|
588 |
console.log(`Rendering page ${pageId}: "${pageData.title}"`);
|
589 |
|
590 |
storyTitleElement.textContent = pageData.title || "Untitled Page";
|
|
|
591 |
storyContentElement.innerHTML = message + (pageData.content || "<p>...</p>");
|
592 |
|
593 |
updateStatsDisplay();
|
594 |
updateInventoryDisplay();
|
595 |
+
choicesElement.innerHTML = '';
|
596 |
|
597 |
const options = pageData.options || [];
|
598 |
+
const isGameOverPage = pageData.gameOver === true;
|
599 |
|
600 |
+
// Generate Sell Buttons if applicable
|
601 |
if (isGameOverPage && pageData.allowSell === true) {
|
602 |
const sellableItems = gameState.character.inventory.filter(itemName => {
|
603 |
const itemInfo = itemsData[itemName];
|
604 |
+
// Check itemInfo exists before accessing properties
|
605 |
+
return itemInfo && itemInfo.type !== 'quest' && itemInfo.goldValue !== undefined && itemInfo.goldValue > 0;
|
606 |
});
|
607 |
|
608 |
if (sellableItems.length > 0) {
|
609 |
+
choicesElement.innerHTML += `<h3 style="margin-bottom: 5px;">Sell Items:</h3>`;
|
610 |
sellableItems.forEach(itemName => {
|
611 |
+
// Double check itemInfo exists here too before creating button
|
612 |
const itemInfo = itemsData[itemName];
|
613 |
+
if (!itemInfo) return; // Skip if item data somehow missing
|
614 |
+
|
615 |
const sellButton = document.createElement('button');
|
616 |
+
sellButton.classList.add('choice-button', 'sell-button');
|
617 |
sellButton.textContent = `Sell ${itemName} (${itemInfo.goldValue} Gold)`;
|
618 |
sellButton.onclick = () => handleChoiceClick({ action: 'sell_item', item: itemName });
|
619 |
choicesElement.appendChild(sellButton);
|
620 |
});
|
621 |
+
choicesElement.innerHTML += `<hr style="border-color: #555; margin: 15px 0 10px 0;">`; // Separator
|
622 |
}
|
623 |
}
|
624 |
|
625 |
+
// Generate Standard Choices / Restart Button
|
626 |
+
if (!isGameOverPage && options.length > 0) {
|
|
|
627 |
options.forEach(option => {
|
628 |
const button = document.createElement('button');
|
629 |
button.classList.add('choice-button');
|
630 |
button.textContent = option.text;
|
631 |
let requirementMet = true;
|
632 |
let requirementText = [];
|
633 |
+
if (option.requireItem) { if (!gameState.character.inventory.includes(option.requireItem)) { requirementMet = false; requirementText.push(`Requires: ${option.requireItem}`); } }
|
634 |
+
if (option.requireStat) { const currentStat = gameState.character.stats[option.requireStat.stat] || 0; if (currentStat < option.requireStat.value) { requirementMet = false; requirementText.push(`Requires: ${option.requireStat.stat.charAt(0).toUpperCase() + option.requireStat.stat.slice(1)} ${option.requireStat.value}`); } }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
635 |
button.disabled = !requirementMet;
|
636 |
if (!requirementMet) button.title = requirementText.join(', ');
|
637 |
+
else { const choiceData = { next: option.next, addItem: option.addItem, check: option.check }; button.onclick = () => handleChoiceClick(choiceData); }
|
|
|
|
|
|
|
638 |
choicesElement.appendChild(button);
|
639 |
});
|
640 |
+
} else if (isGameOverPage) {
|
641 |
const restartButton = document.createElement('button');
|
642 |
restartButton.classList.add('choice-button');
|
643 |
restartButton.textContent = "Restart Adventure (Keep Progress)";
|
|
|
644 |
restartButton.onclick = () => handleChoiceClick({ action: 'restart_plus' });
|
645 |
choicesElement.appendChild(restartButton);
|
646 |
+
} else if (pageId !== 99) { // End of branch, not explicit game over page
|
647 |
choicesElement.insertAdjacentHTML('beforeend', '<p><i>There are no further paths from here.</i></p>');
|
648 |
const restartButton = document.createElement('button');
|
649 |
restartButton.classList.add('choice-button');
|
|
|
657 |
|
658 |
function renderPage(pageId) { renderPageInternal(pageId, gameData[pageId]); }
|
659 |
|
660 |
+
function updateStatsDisplay() { // Checked - OK (added gold)
|
661 |
const char=gameState.character;
|
|
|
662 |
statsElement.innerHTML = `<strong>Stats:</strong> <span class="stat-gold">Gold: ${char.gold}</span> <span>Lvl: ${char.level}</span> <span>XP: ${char.xp}/${char.xpToNextLevel}</span> <span>HP: ${char.stats.hp}/${char.stats.maxHp}</span> <span>Str: ${char.stats.strength}</span> <span>Int: ${char.stats.intelligence}</span> <span>Wis: ${char.stats.wisdom}</span> <span>Dex: ${char.stats.dexterity}</span> <span>Con: ${char.stats.constitution}</span> <span>Cha: ${char.stats.charisma}</span>`;
|
663 |
}
|
664 |
|
665 |
+
function updateInventoryDisplay() { // Checked - OK
|
666 |
let h='<strong>Inventory:</strong> ';
|
667 |
if(gameState.character.inventory.length === 0){
|
668 |
h+='<em>Empty</em>';
|
|
|
677 |
inventoryElement.innerHTML = h;
|
678 |
}
|
679 |
|
680 |
+
// --- Scene Update and Lighting --- (Checked - OK)
|
681 |
+
// [Keep updateScene and adjustLighting functions here - unchanged from previous]
|
682 |
function updateScene(illustrationKey) { if (!scene) { console.warn("Scene not initialized, cannot update visual."); return; } console.log("Updating scene for illustration key:", illustrationKey); if (currentAssemblyGroup) { scene.remove(currentAssemblyGroup); currentAssemblyGroup.traverse(child => { if (child.isMesh) { child.geometry.dispose(); } }); currentAssemblyGroup = null; } scene.fog = null; scene.background = new THREE.Color(0x222222); camera.position.set(0, 2.5, 7); camera.lookAt(0, 0.5, 0); let assemblyFunction; switch (illustrationKey) { case 'city-gates': assemblyFunction = createCityGatesAssembly; break; case 'weaponsmith': assemblyFunction = createWeaponsmithAssembly; break; case 'temple': assemblyFunction = createTempleAssembly; break; case 'resistance-meeting': assemblyFunction = createResistanceMeetingAssembly; break; case 'prisoner-cell': assemblyFunction = createPrisonerCellAssembly; break; case 'game-over': case 'game-over-generic': assemblyFunction = createGameOverAssembly; break; case 'error': assemblyFunction = createErrorAssembly; break; case 'crossroads-signpost-sunny': scene.fog = new THREE.Fog(0x87CEEB, 10, 35); camera.position.set(0, 3, 10); camera.lookAt(0, 1, 0); scene.background = new THREE.Color(0x87CEEB); assemblyFunction = createCrossroadsAssembly; break; case 'rolling-green-hills-shepherd-distance': case 'hilltop-view-overgrown-shrine-wildflowers': case 'overgrown-stone-shrine-wildflowers-close': scene.fog = new THREE.Fog(0xA8E4A0, 15, 50); camera.position.set(0, 5, 15); camera.lookAt(0, 2, -5); scene.background = new THREE.Color(0x90EE90); if (illustrationKey === 'overgrown-stone-shrine-wildflowers-close') camera.position.set(1, 2, 4); if (illustrationKey === 'hilltop-view-overgrown-shrine-wildflowers') camera.position.set(3, 4, 8); assemblyFunction = createRollingHillsAssembly; break; case 'windy-sea-cliffs-crashing-waves-path-down': case 'scanning-sea-cliffs-no-other-paths-visible': case 'close-up-handholds-carved-in-cliff-face': scene.fog = new THREE.Fog(0x6699CC, 10, 40); camera.position.set(5, 5, 10); camera.lookAt(-2, 0, -5); scene.background = new THREE.Color(0x6699CC); assemblyFunction = createCoastalCliffsAssembly; break; case 'hidden-cove-beach-dark-cave-entrance': case 'character-fallen-at-bottom-of-cliff-path-cove': scene.fog = new THREE.Fog(0x336699, 5, 30); camera.position.set(0, 2, 8); camera.lookAt(0, 1, -2); scene.background = new THREE.Color(0x336699); assemblyFunction = createHiddenCoveAssembly; break; case 'rocky-badlands-cracked-earth-harsh-sun': scene.fog = new THREE.Fog(0xD2B48C, 15, 40); camera.position.set(0, 3, 12); camera.lookAt(0, 1, 0); scene.background = new THREE.Color(0xCD853F); assemblyFunction = createDefaultAssembly; break; case 'shadowwood-forest': scene.fog = new THREE.Fog(0x2E2E2E, 5, 20); camera.position.set(0, 2, 8); camera.lookAt(0, 1, 0); scene.background = new THREE.Color(0x1A1A1A); assemblyFunction = createForestAssembly; break; case 'dark-forest-entrance-gnarled-roots-filtered-light': scene.fog = new THREE.Fog(0x2E2E2E, 5, 20); camera.position.set(0, 2, 8); camera.lookAt(0, 1, 0); scene.background = new THREE.Color(0x1A1A1A); assemblyFunction = createForestEntranceAssembly; break; case 'overgrown-forest-path-glowing-fungi-vines': case 'pushing-through-forest-undergrowth': scene.fog = new THREE.Fog(0x1A2F2A, 3, 15); camera.position.set(0, 1.5, 6); camera.lookAt(0, 0.5, 0); scene.background = new THREE.Color(0x112211); assemblyFunction = createOvergrownPathAssembly; break; case 'forest-clearing-mossy-statue-weathered-stone': case 'forest-clearing-mossy-statue-hidden-compartment': case 'forest-clearing-mossy-statue-offering': scene.fog = new THREE.Fog(0x2E4F3A, 5, 25); camera.position.set(0, 2, 5); camera.lookAt(0, 1, 0); scene.background = new THREE.Color(0x223322); assemblyFunction = createClearingStatueAssembly; break; case 'narrow-game-trail-forest-rope-bridge-ravine': case 'character-crossing-rope-bridge-safely': case 'rope-bridge-snapping-character-falling': case 'fallen-log-crossing-ravine': scene.fog = new THREE.Fog(0x2E2E2E, 5, 20); camera.position.set(2, 3, 6); camera.lookAt(0, -1, -2); scene.background = new THREE.Color(0x1A1A1A); assemblyFunction = createForestAssembly; break; case 'two-goblins-ambush-forest-path-spears': case 'forest-shadows-hiding-goblins-walking-past': case 'defeated-goblins-forest-path-loot': case 'blurred-motion-running-past-goblins-forest': scene.fog = new THREE.Fog(0x1A2F2A, 3, 15); camera.position.set(0, 2, 7); camera.lookAt(0, 1, 0); scene.background = new THREE.Color(0x112211); assemblyFunction = createGoblinAmbushAssembly; break; case 'forest-stream-crossing-dappled-sunlight-stones': case 'mossy-log-bridge-over-forest-stream': case 'character-splashing-into-stream-from-log': scene.fog = new THREE.Fog(0x668866, 8, 25); camera.position.set(0, 2, 6); camera.lookAt(0, 0.5, 0); scene.background = new THREE.Color(0x446644); if (illustrationKey === 'mossy-log-bridge-over-forest-stream') camera.position.set(1, 2, 5); assemblyFunction = createForestAssembly; break; case 'forest-edge-view-rocky-foothills-distant-mountain-fortress': case 'forest-edge': scene.fog = new THREE.Fog(0xAAAAAA, 10, 40); camera.position.set(0, 3, 10); camera.lookAt(0, 1, -5); scene.background = new THREE.Color(0x888888); assemblyFunction = createForestEdgeAssembly; break; case 'climbing-rocky-foothills-path-fortress-closer': case 'rockslide-blocking-mountain-path-boulders': case 'character-climbing-over-boulders': case 'character-slipping-on-rockslide-boulders': case 'rough-detour-path-around-rockslide': scene.fog = new THREE.Fog(0x778899, 8, 35); camera.position.set(0, 4, 9); camera.lookAt(0, 2, 0); scene.background = new THREE.Color(0x708090); assemblyFunction = createDefaultAssembly; break; case 'zoomed-view-mountain-fortress-western-ridge': scene.fog = new THREE.Fog(0x778899, 8, 35); camera.position.set(5, 6, 12); camera.lookAt(-2, 3, -5); scene.background = new THREE.Color(0x708090); assemblyFunction = createDefaultAssembly; break; case 'narrow-goat-trail-mountainside-fortress-view': scene.fog = new THREE.Fog(0x778899, 5, 30); camera.position.set(1, 3, 6); camera.lookAt(0, 2, -2); scene.background = new THREE.Color(0x708090); assemblyFunction = createDefaultAssembly; break; case 'narrow-windy-mountain-ridge-path': case 'character-falling-off-windy-ridge': scene.fog = new THREE.Fog(0x8899AA, 6, 25); camera.position.set(2, 5, 7); camera.lookAt(0, 3, -3); scene.background = new THREE.Color(0x778899); assemblyFunction = createDefaultAssembly; break; case 'approaching-dark-fortress-walls-guards': scene.fog = new THREE.Fog(0x444455, 5, 20); camera.position.set(0, 3, 8); camera.lookAt(0, 2, 0); scene.background = new THREE.Color(0x333344); assemblyFunction = createDefaultAssembly; break; case 'dark-cave-entrance-dripping-water': scene.fog = new THREE.Fog(0x1A1A1A, 2, 10); camera.position.set(0, 1.5, 4); camera.lookAt(0, 1, 0); scene.background = new THREE.Color(0x111111); assemblyFunction = createDarkCaveAssembly; break; default: console.warn(`Unknown illustration key: "${illustrationKey}". Using default scene.`); assemblyFunction = createDefaultAssembly; break; } try { currentAssemblyGroup = assemblyFunction(); if (currentAssemblyGroup && currentAssemblyGroup.isGroup) { scene.add(currentAssemblyGroup); adjustLighting(illustrationKey); } else { throw new Error("Assembly function did not return a valid THREE.Group."); } } catch (error) { console.error(`Error creating assembly for ${illustrationKey}:`, error); if (currentAssemblyGroup) { scene.remove(currentAssemblyGroup); } currentAssemblyGroup = createErrorAssembly(); scene.add(currentAssemblyGroup); adjustLighting('error'); } onWindowResize(); }
|
683 |
function adjustLighting(illustrationKey) { if (!scene) return; const lightsToRemove = scene.children.filter(child => child.isLight && !child.isAmbientLight); lightsToRemove.forEach(light => scene.remove(light)); const ambient = scene.children.find(c => c.isAmbientLight); if (!ambient) { console.warn("No ambient light found, adding default."); scene.add(new THREE.AmbientLight(0xffffff, 0.5)); } let directionalLight; let lightIntensity = 1.2; let ambientIntensity = 0.5; let lightColor = 0xffffff; let lightPosition = { x: 8, y: 15, z: 10 }; switch (illustrationKey) { case 'crossroads-signpost-sunny': case 'rolling-green-hills-shepherd-distance': case 'hilltop-view-overgrown-shrine-wildflowers': case 'overgrown-stone-shrine-wildflowers-close': ambientIntensity = 0.7; lightIntensity = 1.5; lightColor = 0xFFF8E1; lightPosition = { x: 10, y: 15, z: 10 }; break; case 'shadowwood-forest': case 'dark-forest-entrance-gnarled-roots-filtered-light': case 'overgrown-forest-path-glowing-fungi-vines': case 'forest-clearing-mossy-statue-weathered-stone': case 'narrow-game-trail-forest-rope-bridge-ravine': case 'two-goblins-ambush-forest-path-spears': case 'forest-stream-crossing-dappled-sunlight-stones': case 'forest-edge-view-rocky-foothills-distant-mountain-fortress': ambientIntensity = 0.4; lightIntensity = 0.8; lightColor = 0xB0C4DE; lightPosition = { x: 5, y: 12, z: 5 }; break; case 'dark-cave-entrance-dripping-water': ambientIntensity = 0.1; lightIntensity = 0.3; lightColor = 0x667799; lightPosition = { x: 0, y: 5, z: 3 }; break; case 'prisoner-cell': ambientIntensity = 0.2; lightIntensity = 0.5; lightColor = 0x7777AA; lightPosition = { x: 0, y: 10, z: 5 }; break; case 'windy-sea-cliffs-crashing-waves-path-down': case 'hidden-cove-beach-dark-cave-entrance': ambientIntensity = 0.6; lightIntensity = 1.0; lightColor = 0xCCDDFF; lightPosition = { x: -10, y: 12, z: 8 }; break; case 'rocky-badlands-cracked-earth-harsh-sun': ambientIntensity = 0.7; lightIntensity = 1.8; lightColor = 0xFFFFDD; lightPosition = { x: 5, y: 20, z: 5 }; break; case 'climbing-rocky-foothills-path-fortress-closer': case 'zoomed-view-mountain-fortress-western-ridge': case 'narrow-goat-trail-mountainside-fortress-view': case 'narrow-windy-mountain-ridge-path': case 'approaching-dark-fortress-walls-guards': ambientIntensity = 0.5; lightIntensity = 1.3; lightColor = 0xDDEEFF; lightPosition = { x: 10, y: 18, z: 15 }; break; case 'game-over': case 'game-over-generic': ambientIntensity = 0.2; lightIntensity = 0.8; lightColor = 0xFF6666; lightPosition = { x: 0, y: 5, z: 5 }; break; case 'error': ambientIntensity = 0.4; lightIntensity = 1.0; lightColor = 0xFFCC00; lightPosition = { x: 0, y: 5, z: 5 }; break; default: ambientIntensity = 0.5; lightIntensity = 1.2; lightColor = 0xffffff; lightPosition = { x: 8, y: 15, z: 10 }; break; } const currentAmbient = scene.children.find(c => c.isAmbientLight); if (currentAmbient) { currentAmbient.intensity = ambientIntensity; } directionalLight = new THREE.DirectionalLight(lightColor, lightIntensity); directionalLight.position.set(lightPosition.x, lightPosition.y, lightPosition.z); directionalLight.castShadow = true; directionalLight.shadow.mapSize.set(1024, 1024); directionalLight.shadow.camera.near = 0.5; directionalLight.shadow.camera.far = 50; directionalLight.shadow.camera.left = -20; directionalLight.shadow.camera.right = 20; directionalLight.shadow.camera.top = 20; directionalLight.shadow.camera.bottom = -20; directionalLight.shadow.bias = -0.001; scene.add(directionalLight); }
|
684 |
|
685 |
+
// --- Potential Future Improvements Comment --- (Checked - OK)
|
686 |
/* [Keep comment block here] */
|
687 |
|
688 |
// ========================================
|
689 |
+
// Initialization (Checked - OK)
|
690 |
// ========================================
|
691 |
document.addEventListener('DOMContentLoaded', () => {
|
692 |
console.log("DOM Ready. Initializing game...");
|
693 |
+
// Attempt to initialize Three.js first
|
694 |
+
if (initThreeJS()) {
|
695 |
+
// If Three.js setup succeeds, start the game
|
696 |
+
startNewGame(); // Call startNewGame for the very first load
|
697 |
+
console.log("Game Started Successfully.");
|
698 |
+
} else {
|
699 |
+
// If Three.js setup failed, display error in UI
|
700 |
+
console.error("Initialization failed: Three.js setup error.");
|
701 |
+
storyTitleElement.textContent = "Initialization Error";
|
702 |
+
storyContentElement.innerHTML = `<p>A critical error occurred during 3D scene setup. The adventure cannot begin. Please check the console (F12) for technical details.</p>`;
|
703 |
+
choicesElement.innerHTML = '<p style="color: #f77;">Cannot proceed due to initialization error.</p>';
|
704 |
+
if (sceneContainer) {
|
705 |
+
sceneContainer.innerHTML = '<p style="color: #f77; padding: 20px; font-size: 1.2em; text-align: center;">3D Scene Failed to Load</p>';
|
706 |
+
}
|
|
|
|
|
|
|
|
|
707 |
}
|
708 |
});
|
709 |
</script>
|