Spaces:
Running
Running
Update index.html
Browse files- index.html +108 -377
index.html
CHANGED
@@ -5,6 +5,7 @@
|
|
5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
<title>Choose Your Own Procedural Adventure</title>
|
7 |
<style>
|
|
|
8 |
body {
|
9 |
font-family: 'Courier New', monospace;
|
10 |
background-color: #222;
|
@@ -38,7 +39,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,7 +60,7 @@
|
|
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; }
|
@@ -100,7 +101,7 @@
|
|
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,8 +119,15 @@
|
|
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 |
-
|
122 |
-
.sell-button
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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; }
|
@@ -131,7 +139,7 @@
|
|
131 |
<div id="ui-container">
|
132 |
<h2 id="story-title">Loading Adventure...</h2>
|
133 |
<div id="story-content">
|
134 |
-
<p>Please wait while the adventure loads.</p>
|
135 |
</div>
|
136 |
<div id="stats-inventory-container">
|
137 |
<div id="stats-display"></div>
|
@@ -156,19 +164,21 @@
|
|
156 |
<script type="module">
|
157 |
import * as THREE from 'three';
|
158 |
|
159 |
-
// DOM Element References
|
160 |
const sceneContainer = document.getElementById('scene-container');
|
161 |
const storyTitleElement = document.getElementById('story-title');
|
162 |
const storyContentElement = document.getElementById('story-content');
|
163 |
const choicesElement = document.getElementById('choices');
|
164 |
const statsElement = document.getElementById('stats-display');
|
165 |
const inventoryElement = document.getElementById('inventory-display');
|
|
|
166 |
|
167 |
-
// Global Three.js Variables
|
168 |
let scene, camera, renderer;
|
169 |
let currentAssemblyGroup = null;
|
|
|
170 |
|
171 |
-
// Materials
|
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,94 +195,16 @@
|
|
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() {
|
222 |
-
if (!renderer || !camera || !sceneContainer) return;
|
223 |
-
const width = sceneContainer.clientWidth;
|
224 |
-
const height = sceneContainer.clientHeight;
|
225 |
-
if (width > 0 && height > 0) {
|
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 }) {
|
257 |
-
const mesh = new THREE.Mesh(geometry, material);
|
258 |
-
mesh.position.set(position.x, position.y, position.z);
|
259 |
-
mesh.rotation.set(rotation.x, rotation.y, rotation.z);
|
260 |
-
mesh.scale.set(scale.x, scale.y, scale.z);
|
261 |
-
mesh.castShadow = true; mesh.receiveShadow = true;
|
262 |
-
return mesh;
|
263 |
-
}
|
264 |
|
265 |
-
|
266 |
-
|
267 |
-
|
268 |
-
|
269 |
-
|
270 |
-
|
271 |
-
}
|
272 |
|
273 |
-
// --- Procedural Generation Functions ---
|
274 |
-
// [Keep all create...Assembly functions here
|
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; }
|
278 |
function createWeaponsmithAssembly() { const group = new THREE.Group(); const bw = 3, bh = 2.5, bd = 3.5; const bGeo = new THREE.BoxGeometry(bw, bh, bd); group.add(createMesh(bGeo, darkWoodMaterial, { x: 0, y: bh / 2, z: 0 })); const ch = 3.5; const cGeo = new THREE.CylinderGeometry(0.3, 0.4, ch, 8); group.add(createMesh(cGeo, stoneMaterial, { x: bw * 0.3, y: ch / 2, z: -bd * 0.3 })); group.add(createGroundPlane(dirtMaterial)); return group; }
|
@@ -295,25 +227,23 @@
|
|
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
|
299 |
// ========================================
|
300 |
-
const itemsData = {
|
301 |
"Flaming Sword": {type:"weapon", description:"A legendary blade, wreathed in magical fire.", goldValue: 500},
|
302 |
"Whispering Bow": {type:"weapon", description:"Crafted by elves, its arrows fly almost silently.", goldValue: 350},
|
303 |
"Guardian Shield": {type:"armor", description:"A sturdy shield imbued with protective enchantments.", goldValue: 250},
|
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},
|
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}
|
312 |
};
|
313 |
|
314 |
-
const gameData = { //
|
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" },
|
@@ -328,7 +258,6 @@
|
|
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 } },
|
@@ -364,19 +293,19 @@
|
|
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
|
375 |
}
|
376 |
};
|
377 |
|
|
|
378 |
// ========================================
|
379 |
-
// Game State
|
380 |
// ========================================
|
381 |
const defaultCharacterState = {
|
382 |
name: "Hero", race: "Human", alignment: "Neutral Good", class: "Adventurer",
|
@@ -386,317 +315,119 @@
|
|
386 |
};
|
387 |
let gameState = {
|
388 |
currentPageId: 1,
|
389 |
-
character: JSON.parse(JSON.stringify(defaultCharacterState))
|
390 |
};
|
391 |
|
|
|
392 |
// ========================================
|
393 |
-
// Game Logic Functions
|
394 |
// ========================================
|
395 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
396 |
function startNewGame() {
|
397 |
console.log("Starting brand new game...");
|
|
|
398 |
gameState = {
|
399 |
currentPageId: 1,
|
400 |
-
character: JSON.parse(JSON.stringify(defaultCharacterState))
|
401 |
};
|
402 |
-
renderPage(gameState.currentPageId);
|
403 |
}
|
404 |
|
405 |
function restartGamePlus() {
|
406 |
console.log("Restarting game (keeping progress)...");
|
407 |
-
//
|
408 |
gameState.currentPageId = 1;
|
409 |
-
renderPage(gameState.currentPageId);
|
410 |
}
|
411 |
|
412 |
-
function handleChoiceClick(choiceData) {
|
413 |
console.log("Choice clicked:", choiceData);
|
414 |
-
let feedbackMessage = "";
|
415 |
-
|
416 |
-
|
417 |
-
|
418 |
-
|
419 |
-
|
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 |
-
|
429 |
-
// --- Standard Page Navigation ---
|
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);
|
447 |
-
const roll = Math.floor(Math.random() * 20) + 1;
|
448 |
-
const totalResult = roll + modifier;
|
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 |
-
}
|
465 |
-
|
466 |
-
// --- Page Transition & Consequences ---
|
467 |
const targetPageData = gameData[nextPageId];
|
468 |
-
if (!targetPageData) {
|
469 |
-
|
470 |
-
|
471 |
-
|
472 |
-
|
473 |
-
|
474 |
-
|
475 |
-
let hpLostThisTurn = 0;
|
476 |
-
if (targetPageData.hpLoss) {
|
477 |
-
hpLostThisTurn = targetPageData.hpLoss;
|
478 |
-
gameState.character.stats.hp -= hpLostThisTurn;
|
479 |
-
console.log(`Lost ${hpLostThisTurn} HP.`);
|
480 |
-
}
|
481 |
-
if (targetPageData.reward && targetPageData.reward.hpGain) {
|
482 |
-
const hpGained = targetPageData.reward.hpGain;
|
483 |
-
gameState.character.stats.hp += hpGained;
|
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;
|
504 |
-
const amount = targetPageData.reward.statIncrease.amount;
|
505 |
-
if (gameState.character.stats.hasOwnProperty(stat)) {
|
506 |
-
gameState.character.stats[stat] += amount;
|
507 |
-
console.log(`Stat ${stat} increased by ${amount}. New value: ${gameState.character.stats[stat]}`);
|
508 |
-
if (stat === 'constitution') recalculateMaxHp();
|
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 |
-
//
|
544 |
-
|
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 |
-
|
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 = "") { //
|
582 |
-
|
583 |
-
|
584 |
-
|
585 |
-
|
586 |
-
|
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');
|
650 |
-
restartButton.textContent = "Restart Adventure (Keep Progress)";
|
651 |
-
restartButton.onclick = () => handleChoiceClick({ action: 'restart_plus' });
|
652 |
-
choicesElement.appendChild(restartButton);
|
653 |
-
}
|
654 |
-
|
655 |
updateScene(pageData.illustration || 'default');
|
656 |
}
|
657 |
|
658 |
-
function renderPage(pageId) { renderPageInternal(pageId, gameData[pageId]); }
|
659 |
-
|
660 |
-
function
|
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 |
-
|
666 |
-
|
667 |
-
if(gameState.character.inventory.length === 0){
|
668 |
-
h+='<em>Empty</em>';
|
669 |
-
} else {
|
670 |
-
gameState.character.inventory.forEach(itemName=>{
|
671 |
-
const item = itemsData[itemName] || {type:'unknown',description:'An unknown item.'};
|
672 |
-
const itemClass = `item-${item.type || 'unknown'}`;
|
673 |
-
const descriptionText = typeof item.description === 'string' ? item.description : 'No description available.';
|
674 |
-
h += `<span class="${itemClass}" title="${descriptionText.replace(/"/g, '"')}">${itemName}</span>`;
|
675 |
-
});
|
676 |
-
}
|
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 ---
|
686 |
/* [Keep comment block here] */
|
687 |
|
688 |
// ========================================
|
689 |
-
// Initialization
|
690 |
// ========================================
|
691 |
document.addEventListener('DOMContentLoaded', () => {
|
692 |
-
console.log("DOM Ready. Initializing
|
693 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
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>`;
|
|
|
5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
<title>Choose Your Own Procedural Adventure</title>
|
7 |
<style>
|
8 |
+
/* [Keep all existing CSS rules here] */
|
9 |
body {
|
10 |
font-family: 'Courier New', monospace;
|
11 |
background-color: #222;
|
|
|
39 |
padding: 20px;
|
40 |
overflow-y: auto;
|
41 |
background-color: #333;
|
42 |
+
min-width: 280px; /* Increased min-width slightly */
|
43 |
height: 100%;
|
44 |
box-sizing: border-box;
|
45 |
display: flex;
|
|
|
60 |
#story-content {
|
61 |
margin-bottom: 20px;
|
62 |
line-height: 1.6;
|
63 |
+
flex-grow: 1; /* Let story content grow */
|
64 |
}
|
65 |
#story-content p { margin-bottom: 1em; }
|
66 |
#story-content p:last-child { margin-bottom: 0; }
|
|
|
101 |
#inventory-display .item-unknown { background-color: #555; border-color: #777;}
|
102 |
|
103 |
#choices-container {
|
104 |
+
margin-top: auto; /* Push choices to bottom */
|
105 |
padding-top: 15px;
|
106 |
border-top: 1px solid #555;
|
107 |
}
|
|
|
119 |
.choice-button:hover:not(:disabled) { background-color: #d4a017; color: #222; border-color: #b8860b; }
|
120 |
.choice-button:disabled { background-color: #444; color: #888; cursor: not-allowed; border-color: #666; opacity: 0.7; }
|
121 |
|
122 |
+
/* Specific style for Sell buttons */
|
123 |
+
.sell-button {
|
124 |
+
background-color: #4a4a4a;
|
125 |
+
border-color: #6a6a6a;
|
126 |
+
}
|
127 |
+
.sell-button:hover:not(:disabled) {
|
128 |
+
background-color: #a07017; /* Different hover for sell */
|
129 |
+
border-color: #80500b;
|
130 |
+
}
|
131 |
|
132 |
.roll-success { color: #7f7; border-left: 3px solid #4a4; padding-left: 8px; margin-bottom: 1em; font-size: 0.9em; }
|
133 |
.roll-failure { color: #f77; border-left: 3px solid #a44; padding-left: 8px; margin-bottom: 1em; font-size: 0.9em; }
|
|
|
139 |
<div id="ui-container">
|
140 |
<h2 id="story-title">Loading Adventure...</h2>
|
141 |
<div id="story-content">
|
142 |
+
<p id="loading-message">Please wait while the adventure loads.</p>
|
143 |
</div>
|
144 |
<div id="stats-inventory-container">
|
145 |
<div id="stats-display"></div>
|
|
|
164 |
<script type="module">
|
165 |
import * as THREE from 'three';
|
166 |
|
167 |
+
// DOM Element References
|
168 |
const sceneContainer = document.getElementById('scene-container');
|
169 |
const storyTitleElement = document.getElementById('story-title');
|
170 |
const storyContentElement = document.getElementById('story-content');
|
171 |
const choicesElement = document.getElementById('choices');
|
172 |
const statsElement = document.getElementById('stats-display');
|
173 |
const inventoryElement = document.getElementById('inventory-display');
|
174 |
+
const loadingMessageElement = document.getElementById('loading-message'); // Get reference to loading message
|
175 |
|
176 |
+
// Global Three.js Variables
|
177 |
let scene, camera, renderer;
|
178 |
let currentAssemblyGroup = null;
|
179 |
+
let loadingIntervalId = null; // Variable to hold the timer ID
|
180 |
|
181 |
+
// --- Materials --- [Unchanged]
|
182 |
const stoneMaterial = new THREE.MeshStandardMaterial({ color: 0x888888, roughness: 0.8, metalness: 0.1 });
|
183 |
const woodMaterial = new THREE.MeshStandardMaterial({ color: 0x8B4513, roughness: 0.7, metalness: 0 });
|
184 |
const darkWoodMaterial = new THREE.MeshStandardMaterial({ color: 0x5C3D20, roughness: 0.7, metalness: 0 });
|
|
|
195 |
const wetStoneMaterial = new THREE.MeshStandardMaterial({ color: 0x2F4F4F, roughness: 0.7 });
|
196 |
const glowMaterial = new THREE.MeshStandardMaterial({ color: 0x00FFAA, emissive: 0x00FFAA, emissiveIntensity: 0.5 });
|
197 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
198 |
|
199 |
+
// --- Three.js Setup --- [Unchanged]
|
200 |
+
function initThreeJS() { if (!sceneContainer) { console.error("Scene container not found!"); return false; } try { scene = new THREE.Scene(); scene.background = new THREE.Color(0x222222); const width = sceneContainer.clientWidth; const height = sceneContainer.clientHeight; if (!width || !height) { console.warn("Scene container has zero dimensions initially."); } camera = new THREE.PerspectiveCamera(75, (width / height) || 1, 0.1, 1000); camera.position.set(0, 2.5, 7); camera.lookAt(0, 0.5, 0); renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(width || 400, height || 300); renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFSoftShadowMap; sceneContainer.innerHTML = ''; sceneContainer.appendChild(renderer.domElement); const ambientLight = new THREE.AmbientLight(0xffffff, 0.5); scene.add(ambientLight); window.addEventListener('resize', onWindowResize, false); setTimeout(onWindowResize, 100); animate(); return true; } catch (error) { console.error("Error during Three.js initialization:", error); return false; } }
|
201 |
+
function onWindowResize() { if (!renderer || !camera || !sceneContainer) return; const width = sceneContainer.clientWidth; const height = sceneContainer.clientHeight; if (width > 0 && height > 0) { camera.aspect = width / height; camera.updateProjectionMatrix(); renderer.setSize(width, height); } else { console.warn("onWindowResize called with zero dimensions."); } }
|
202 |
+
function animate() { if (!renderer || !scene || !camera) { return; } requestAnimationFrame(animate); try { const time = performance.now() * 0.001; scene.traverse(obj => { if (obj.userData && obj.userData.update && typeof obj.userData.update === 'function') { obj.userData.update(time); } }); renderer.render(scene, camera); } catch (error) { console.error("Error during animation/render loop:", error); } }
|
203 |
+
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 }) { const mesh = new THREE.Mesh(geometry, material); mesh.position.set(position.x, position.y, position.z); mesh.rotation.set(rotation.x, rotation.y, rotation.z); mesh.scale.set(scale.x, scale.y, scale.z); mesh.castShadow = true; mesh.receiveShadow = true; return mesh; }
|
204 |
+
function createGroundPlane(material = groundMaterial, size = 20) { const groundGeo = new THREE.PlaneGeometry(size, size); const ground = new THREE.Mesh(groundGeo, material); ground.rotation.x = -Math.PI / 2; ground.position.y = -0.05; ground.receiveShadow = true; ground.castShadow = false; return ground; }
|
|
|
205 |
|
206 |
+
// --- Procedural Generation Functions --- [Unchanged]
|
207 |
+
// [Keep all create...Assembly functions here]
|
|
|
208 |
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; }
|
209 |
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; }
|
210 |
function createWeaponsmithAssembly() { const group = new THREE.Group(); const bw = 3, bh = 2.5, bd = 3.5; const bGeo = new THREE.BoxGeometry(bw, bh, bd); group.add(createMesh(bGeo, darkWoodMaterial, { x: 0, y: bh / 2, z: 0 })); const ch = 3.5; const cGeo = new THREE.CylinderGeometry(0.3, 0.4, ch, 8); group.add(createMesh(cGeo, stoneMaterial, { x: bw * 0.3, y: ch / 2, z: -bd * 0.3 })); group.add(createGroundPlane(dirtMaterial)); return group; }
|
|
|
227 |
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; }
|
228 |
|
229 |
// ========================================
|
230 |
+
// Game Data
|
231 |
// ========================================
|
232 |
+
const itemsData = { // [Unchanged]
|
233 |
"Flaming Sword": {type:"weapon", description:"A legendary blade, wreathed in magical fire.", goldValue: 500},
|
234 |
"Whispering Bow": {type:"weapon", description:"Crafted by elves, its arrows fly almost silently.", goldValue: 350},
|
235 |
"Guardian Shield": {type:"armor", description:"A sturdy shield imbued with protective enchantments.", goldValue: 250},
|
236 |
"Healing Light Spell":{type:"spell", description:"A scroll containing the incantation to mend minor wounds.", goldValue: 50},
|
237 |
"Shield of Faith Spell":{type:"spell",description:"A scroll containing a prayer that grants temporary magical protection.", goldValue: 75},
|
238 |
"Binding Runes Scroll":{type:"spell", description:"Complex runes scribbled on parchment, said to temporarily immobilize a foe.", goldValue: 100},
|
239 |
+
"Secret Tunnel Map": {type:"quest", description:"A crudely drawn map showing a hidden path, perhaps into the fortress?", goldValue: 0},
|
240 |
"Poison Daggers": {type:"weapon", description:"A pair of wicked-looking daggers coated in a fast-acting toxin.", goldValue: 150},
|
241 |
"Master Key": {type:"quest", description:"An ornate key rumored to unlock many doors, though perhaps not all.", goldValue: 0},
|
242 |
"Crude Dagger": {type:"weapon", description:"A roughly made dagger, chipped and stained.", goldValue: 10},
|
243 |
+
"Scout's Pouch": {type:"quest", description:"A small leather pouch containing flint & steel, jerky, and some odd coins.", goldValue: 20}
|
244 |
};
|
245 |
|
246 |
+
const gameData = { // [Unchanged except page 99]
|
|
|
|
|
247 |
"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" },
|
248 |
"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" },
|
249 |
"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" },
|
|
|
258 |
"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 } },
|
259 |
"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" },
|
260 |
"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" },
|
|
|
261 |
"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" },
|
262 |
"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" },
|
263 |
"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 } },
|
|
|
293 |
"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"},
|
294 |
"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"},
|
295 |
|
|
|
296 |
"99": {
|
297 |
title: "Game Over / To Be Continued...",
|
298 |
content: "<p>Your adventure ends here... for now. You can sell unwanted items for gold before starting again.</p>",
|
299 |
options: [ /* Restart button added dynamically */ ],
|
300 |
illustration: "game-over-generic",
|
301 |
gameOver: true,
|
302 |
+
allowSell: true // Enable selling feature
|
303 |
}
|
304 |
};
|
305 |
|
306 |
+
|
307 |
// ========================================
|
308 |
+
// Game State
|
309 |
// ========================================
|
310 |
const defaultCharacterState = {
|
311 |
name: "Hero", race: "Human", alignment: "Neutral Good", class: "Adventurer",
|
|
|
315 |
};
|
316 |
let gameState = {
|
317 |
currentPageId: 1,
|
318 |
+
character: JSON.parse(JSON.stringify(defaultCharacterState))
|
319 |
};
|
320 |
|
321 |
+
|
322 |
// ========================================
|
323 |
+
// Game Logic Functions
|
324 |
// ========================================
|
325 |
|
326 |
+
// Clears the loading animation interval
|
327 |
+
function stopLoadingAnimation() {
|
328 |
+
if (loadingIntervalId !== null) {
|
329 |
+
clearInterval(loadingIntervalId);
|
330 |
+
loadingIntervalId = null;
|
331 |
+
// Optional: Reset loading message text if needed, though renderPageInternal will overwrite it
|
332 |
+
if(loadingMessageElement) loadingMessageElement.textContent = "Please wait while the adventure loads.";
|
333 |
+
console.log("Loading animation stopped.");
|
334 |
+
}
|
335 |
+
}
|
336 |
+
|
337 |
function startNewGame() {
|
338 |
console.log("Starting brand new game...");
|
339 |
+
stopLoadingAnimation(); // Stop animation when game actually starts
|
340 |
gameState = {
|
341 |
currentPageId: 1,
|
342 |
+
character: JSON.parse(JSON.stringify(defaultCharacterState))
|
343 |
};
|
344 |
+
renderPage(gameState.currentPageId);
|
345 |
}
|
346 |
|
347 |
function restartGamePlus() {
|
348 |
console.log("Restarting game (keeping progress)...");
|
349 |
+
stopLoadingAnimation(); // Stop animation if restarting from an error state perhaps
|
350 |
gameState.currentPageId = 1;
|
351 |
+
renderPage(gameState.currentPageId);
|
352 |
}
|
353 |
|
354 |
+
function handleChoiceClick(choiceData) { // [Unchanged from previous working version]
|
355 |
console.log("Choice clicked:", choiceData);
|
356 |
+
let feedbackMessage = "";
|
357 |
+
if (choiceData.action === 'restart_plus') { restartGamePlus(); return; }
|
358 |
+
if (choiceData.action === 'sell_item') { feedbackMessage = handleSellItem(choiceData.item); renderPageInternal(gameState.currentPageId, gameData[gameState.currentPageId], feedbackMessage); return; }
|
359 |
+
const optionNextPageId = parseInt(choiceData.next); const itemToAdd = choiceData.addItem; let nextPageId = optionNextPageId; const check = choiceData.check;
|
360 |
+
if (isNaN(optionNextPageId) && !check) { console.error("Invalid choice data:", choiceData); feedbackMessage = `<p class="feedback-error">Error: Invalid choice data!</p>`; renderPageInternal(gameState.currentPageId, gameData[gameState.currentPageId], feedbackMessage); choicesElement.querySelectorAll('button').forEach(b => b.disabled = true); return; }
|
361 |
+
if (check) { const statValue = gameState.character.stats[check.stat] || 10; const modifier = Math.floor((statValue - 10) / 2); const roll = Math.floor(Math.random() * 20) + 1; const totalResult = roll + modifier; const dc = check.dc; const statName = check.stat.charAt(0).toUpperCase() + check.stat.slice(1); console.log(`Check: ${statName} (DC ${dc}) | Roll: ${roll} + Mod: ${modifier} = ${totalResult}`); if (totalResult >= dc) { nextPageId = optionNextPageId; feedbackMessage += `<p class="roll-success"><em>${statName} Check Success! (${totalResult} vs DC ${dc})</em></p>`; } else { nextPageId = parseInt(check.onFailure); feedbackMessage += `<p class="roll-failure"><em>${statName} Check Failed! (${totalResult} vs DC ${dc})</em></p>`; if (isNaN(nextPageId)) { console.error("Invalid onFailure ID:", check.onFailure); nextPageId = 99; feedbackMessage += `<p class="feedback-error">Error: Invalid failure path!</p>`; } } }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
362 |
const targetPageData = gameData[nextPageId];
|
363 |
+
if (!targetPageData) { console.error(`Data for target page ${nextPageId} not found!`); feedbackMessage = `<p class="feedback-error">Error: Next page data missing!</p>`; renderPageInternal(99, gameData[99], feedbackMessage); return; }
|
364 |
+
let hpLostThisTurn = 0; if (targetPageData.hpLoss) { hpLostThisTurn = targetPageData.hpLoss; gameState.character.stats.hp -= hpLostThisTurn; console.log(`Lost ${hpLostThisTurn} HP.`); } if (targetPageData.reward && targetPageData.reward.hpGain) { const hpGained = targetPageData.reward.hpGain; gameState.character.stats.hp += hpGained; console.log(`Gained ${hpGained} HP.`); }
|
365 |
+
if (gameState.character.stats.hp <= 0) { gameState.character.stats.hp = 0; console.log("Player died!"); nextPageId = 99; feedbackMessage += `<p class="feedback-error"><em>You have succumbed to your injuries!${hpLostThisTurn > 0 ? ` (-${hpLostThisTurn} HP)` : ''}</em></p>`; renderPageInternal(nextPageId, gameData[nextPageId], feedbackMessage); return; }
|
366 |
+
if (targetPageData.reward) { if (targetPageData.reward.xp) { gameState.character.xp += targetPageData.reward.xp; console.log(`Gained ${targetPageData.reward.xp} XP! Total: ${gameState.character.xp}`); } if (targetPageData.reward.statIncrease) { const stat = targetPageData.reward.statIncrease.stat; const amount = targetPageData.reward.statIncrease.amount; if (gameState.character.stats.hasOwnProperty(stat)) { gameState.character.stats[stat] += amount; console.log(`Stat ${stat} increased by ${amount}. New value: ${gameState.character.stats[stat]}`); if (stat === 'constitution') recalculateMaxHp(); } } if (targetPageData.reward.addItem && !gameState.character.inventory.includes(targetPageData.reward.addItem)) { const itemName = targetPageData.reward.addItem; if (itemsData[itemName]) { gameState.character.inventory.push(itemName); console.log(`Found item: ${itemName}`); feedbackMessage += `<p class="feedback-message"><em>Item acquired: ${itemName}</em></p>`; } else { console.warn(`Attempted to add unknown item reward: ${itemName}`); feedbackMessage += `<p class="feedback-error"><em>Error: Tried to acquire unknown item '${itemName}'!</em></p>`; } } }
|
367 |
+
if (itemToAdd && !gameState.character.inventory.includes(itemToAdd)) { if (itemsData[itemToAdd]) { gameState.character.inventory.push(itemToAdd); console.log("Added item:", itemToAdd); feedbackMessage += `<p class="feedback-message"><em>Item acquired: ${itemToAdd}</em></p>`; } else { console.warn(`Attempted to add unknown item choice: ${itemToAdd}`); feedbackMessage += `<p class="feedback-error"><em>Error: Tried to acquire unknown item '${itemToAdd}'!</em></p>`; } }
|
368 |
+
gameState.currentPageId = nextPageId; recalculateMaxHp(); gameState.character.stats.hp = Math.min(gameState.character.stats.hp, gameState.character.stats.maxHp);
|
369 |
+
console.log("Transitioning to page:", nextPageId, " New state:", JSON.stringify(gameState)); renderPageInternal(nextPageId, gameData[nextPageId], feedbackMessage);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
370 |
}
|
371 |
|
372 |
+
function handleSellItem(itemName) { // [Unchanged from previous working version]
|
373 |
+
console.log("Attempting to sell:", itemName); const itemIndex = gameState.character.inventory.indexOf(itemName); const itemInfo = itemsData[itemName]; let message = ""; if (itemIndex === -1) { console.warn(`Sell failed: Item "${itemName}" not in inventory.`); message = `<p class="feedback-error">Cannot sell ${itemName} - you don't have it!</p>`; } else if (!itemInfo) { console.warn(`Sell failed: Item data for "${itemName}" not found.`); message = `<p class="feedback-error">Cannot sell ${itemName} - item data missing!</p>`; } else if (itemInfo.type === 'quest') { console.log(`Sell blocked: Item "${itemName}" is a quest item.`); message = `<p class="feedback-message">Cannot sell ${itemName} - it seems important.</p>`; } else if (itemInfo.goldValue === undefined || itemInfo.goldValue <= 0) { console.log(`Sell blocked: Item "${itemName}" has no gold value.`); message = `<p class="feedback-message">${itemName} isn't worth any gold.</p>`; } else { const value = itemInfo.goldValue; gameState.character.gold += value; gameState.character.inventory.splice(itemIndex, 1); message = `<p class="feedback-success">Sold ${itemName} for ${value} Gold.</p>`; console.log(`Sold ${itemName} for ${value} gold. Current gold: ${gameState.character.gold}`); } return message;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
374 |
}
|
375 |
|
376 |
+
function recalculateMaxHp() { // [Unchanged from previous working version]
|
377 |
+
const baseHp = 10; const conModifier = Math.floor((gameState.character.stats.constitution - 10) / 2); gameState.character.stats.maxHp = Math.max(1, baseHp + conModifier * gameState.character.level);
|
|
|
|
|
|
|
378 |
}
|
379 |
|
380 |
+
function renderPageInternal(pageId, pageData, message = "") { // [Unchanged from previous working version]
|
381 |
+
stopLoadingAnimation(); // Stop loading animation whenever a page renders
|
382 |
+
if (!pageData) { console.error(`Render Error: No data for page ${pageId}`); pageData = gameData["99"] || { title: "Error", content: "<p>Render Error! Critical page data missing.</p>", illustration: "error", gameOver: true }; message += `<p class="feedback-error">Render Error: Page data for ID ${pageId} was missing!</p>`; pageId = 99; } console.log(`Rendering page ${pageId}: "${pageData.title}"`);
|
383 |
+
storyTitleElement.textContent = pageData.title || "Untitled Page"; storyContentElement.innerHTML = message + (pageData.content || "<p>...</p>"); updateStatsDisplay(); updateInventoryDisplay(); choicesElement.innerHTML = '';
|
384 |
+
const options = pageData.options || []; const isGameOverPage = pageData.gameOver === true;
|
385 |
+
if (isGameOverPage && pageData.allowSell === true) { const sellableItems = gameState.character.inventory.filter(itemName => { const itemInfo = itemsData[itemName]; return itemInfo && itemInfo.type !== 'quest' && itemInfo.goldValue !== undefined && itemInfo.goldValue > 0; }); if (sellableItems.length > 0) { choicesElement.innerHTML += `<h3 style="margin-bottom: 5px;">Sell Items:</h3>`; sellableItems.forEach(itemName => { const itemInfo = itemsData[itemName]; if (!itemInfo) return; const sellButton = document.createElement('button'); sellButton.classList.add('choice-button', 'sell-button'); sellButton.textContent = `Sell ${itemName} (${itemInfo.goldValue} Gold)`; sellButton.onclick = () => handleChoiceClick({ action: 'sell_item', item: itemName }); choicesElement.appendChild(sellButton); }); choicesElement.innerHTML += `<hr style="border-color: #555; margin: 15px 0 10px 0;">`; } }
|
386 |
+
if (!isGameOverPage && options.length > 0) { options.forEach(option => { const button = document.createElement('button'); button.classList.add('choice-button'); button.textContent = option.text; let requirementMet = true; let requirementText = []; if (option.requireItem) { if (!gameState.character.inventory.includes(option.requireItem)) { requirementMet = false; requirementText.push(`Requires: ${option.requireItem}`); } } 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}`); } } button.disabled = !requirementMet; if (!requirementMet) button.title = requirementText.join(', '); else { const choiceData = { next: option.next, addItem: option.addItem, check: option.check }; button.onclick = () => handleChoiceClick(choiceData); } choicesElement.appendChild(button); }); } else if (isGameOverPage) { const restartButton = document.createElement('button'); restartButton.classList.add('choice-button'); restartButton.textContent = "Restart Adventure (Keep Progress)"; restartButton.onclick = () => handleChoiceClick({ action: 'restart_plus' }); choicesElement.appendChild(restartButton); } else if (pageId !== 99) { choicesElement.insertAdjacentHTML('beforeend', '<p><i>There are no further paths from here.</i></p>'); const restartButton = document.createElement('button'); restartButton.classList.add('choice-button'); restartButton.textContent = "Restart Adventure (Keep Progress)"; restartButton.onclick = () => handleChoiceClick({ action: 'restart_plus' }); choicesElement.appendChild(restartButton); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
387 |
updateScene(pageData.illustration || 'default');
|
388 |
}
|
389 |
|
390 |
+
function renderPage(pageId) { renderPageInternal(pageId, gameData[pageId]); } // [Unchanged]
|
391 |
+
function updateStatsDisplay() { const char = gameState.character; 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>`; } // [Unchanged]
|
392 |
+
function updateInventoryDisplay() { let h = '<strong>Inventory:</strong> '; if (gameState.character.inventory.length === 0) { h += '<em>Empty</em>'; } else { gameState.character.inventory.forEach(itemName => { const item = itemsData[itemName] || { type: 'unknown', description: 'An unknown item.' }; const itemClass = `item-${item.type || 'unknown'}`; const descriptionText = typeof item.description === 'string' ? item.description : 'No description available.'; h += `<span class="${itemClass}" title="${descriptionText.replace(/"/g, '"')}">${itemName}</span>`; }); } inventoryElement.innerHTML = h; } // [Unchanged]
|
|
|
|
|
|
|
393 |
|
394 |
+
// --- Scene Update and Lighting --- [Unchanged]
|
395 |
+
// [Keep updateScene and adjustLighting functions here]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
396 |
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(); }
|
397 |
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); }
|
398 |
|
399 |
+
// --- Potential Future Improvements Comment --- [Unchanged]
|
400 |
/* [Keep comment block here] */
|
401 |
|
402 |
// ========================================
|
403 |
+
// Initialization
|
404 |
// ========================================
|
405 |
document.addEventListener('DOMContentLoaded', () => {
|
406 |
+
console.log("DOM Ready. Initializing...");
|
407 |
+
|
408 |
+
// Start loading animation
|
409 |
+
let dotCount = 0;
|
410 |
+
const baseLoadingText = "Please wait while the adventure loads";
|
411 |
+
if (loadingMessageElement) { // Check if element exists
|
412 |
+
loadingIntervalId = setInterval(() => {
|
413 |
+
dotCount = (dotCount + 1) % 4; // Cycle 0, 1, 2, 3
|
414 |
+
const dots = '.'.repeat(dotCount);
|
415 |
+
loadingMessageElement.textContent = baseLoadingText + dots;
|
416 |
+
}, 500); // Update every 500ms
|
417 |
+
console.log("Loading animation started.");
|
418 |
+
} else {
|
419 |
+
console.warn("Loading message element not found.");
|
420 |
+
}
|
421 |
+
|
422 |
+
// Attempt to initialize Three.js
|
423 |
if (initThreeJS()) {
|
424 |
+
// If Three.js setup succeeds, start the game logic
|
425 |
+
// The loading animation will be stopped by the first call to renderPageInternal
|
426 |
startNewGame(); // Call startNewGame for the very first load
|
427 |
+
console.log("Game setup initiated.");
|
428 |
} else {
|
429 |
+
// If Three.js setup failed, display error in UI and stop animation
|
430 |
+
stopLoadingAnimation(); // Stop animation on error
|
431 |
console.error("Initialization failed: Three.js setup error.");
|
432 |
storyTitleElement.textContent = "Initialization Error";
|
433 |
storyContentElement.innerHTML = `<p>A critical error occurred during 3D scene setup. The adventure cannot begin. Please check the console (F12) for technical details.</p>`;
|