Moving like a shadow, you manage to slip past the gate guards unnoticed!
", options: [{text:"Enter the fortress courtyard", next: 99}], illustration:"approaching-dark-fortress-walls-guards"}, // TODO: Expand fortress interior // --- Game Over / Error State --- - "99": { title: "Game Over / To Be Continued...", content: "Your adventure ends here (for now). Thanks for playing!
", options: [{ text: "Restart Adventure", next: 1 }], illustration: "game-over-generic", gameOver: true } + "99": { + title: "Game Over / To Be Continued...", + content: "Your adventure ends here... for now. You can sell unwanted items for gold before starting again.
", // Modified content + // Options will be dynamically generated in renderPageInternal for selling + options: [ + // The restart button will be added last by renderPageInternal logic + ], + illustration: "game-over-generic", + gameOver: true, // Mark this page specifically for game over logic + allowSell: true // Add a flag to enable selling on this page + } }; // ======================================== // Game State // ======================================== + // Define default character state - used for first launch + const defaultCharacterState = { + name: "Hero", race: "Human", alignment: "Neutral Good", class: "Adventurer", + level: 1, xp: 0, xpToNextLevel: 100, gold: 0, // Added gold + stats: { strength: 8, intelligence: 10, wisdom: 10, dexterity: 10, constitution: 10, charisma: 8, hp: 12, maxHp: 12 }, + inventory: [] + }; + + // Initialize gameState let gameState = { currentPageId: 1, - character: { - name: "Hero", race: "Human", alignment: "Neutral Good", class: "Adventurer", - level: 1, xp: 0, xpToNextLevel: 100, - stats: { strength: 8, intelligence: 10, wisdom: 10, dexterity: 10, constitution: 10, charisma: 8, hp: 12, maxHp: 12 }, - inventory: [] - // TODO: Add equipment slots (weapon, armor, etc.) - // TODO: Add status effects array - } + // Deep copy default state initially to avoid modification issues + character: JSON.parse(JSON.stringify(defaultCharacterState)), + // Store sell feedback message temporarily + lastSellMessage: "" }; + // ======================================== // Game Logic Functions // ======================================== - function startGame() { - // Reset state if restarting - const defaultChar = { - name: "Hero", race: "Human", alignment: "Neutral Good", class: "Adventurer", - level: 1, xp: 0, xpToNextLevel: 100, - stats: { strength: 8, intelligence: 10, wisdom: 10, dexterity: 10, constitution: 10, charisma: 8, hp: 12, maxHp: 12 }, - inventory: [] - }; - // Deep copy necessary for nested objects like stats - gameState = { currentPageId: 1, character: JSON.parse(JSON.stringify(defaultChar)) }; - console.log("Starting new game with state:", JSON.stringify(gameState)); + + // Function to start a brand new game (resets everything) + function startNewGame() { + console.log("Starting brand new game..."); + // Reset state completely using the default + gameState = { + currentPageId: 1, + character: JSON.parse(JSON.stringify(defaultCharacterState)), + lastSellMessage: "" + }; + renderPage(gameState.currentPageId); + } + + // Function to restart, keeping character progress ("New Game Plus") + function restartGamePlus() { + console.log("Restarting game (keeping progress)..."); + gameState.currentPageId = 1; // Only reset the page ID + gameState.lastSellMessage = ""; // Clear any sell messages renderPage(gameState.currentPageId); } function handleChoiceClick(choiceData) { console.log("Choice clicked:", choiceData); + // --- Special Actions (Sell, Restart) --- + if (choiceData.action === 'restart_plus') { + restartGamePlus(); + return; + } + if (choiceData.action === 'sell_item') { + handleSellItem(choiceData.item); + return; + } + + // --- Standard Page Navigation --- const optionNextPageId = parseInt(choiceData.next); - const itemToAdd = choiceData.addItem; // Item from direct choice property (less common now) + const itemToAdd = choiceData.addItem; let nextPageId = optionNextPageId; - let rollResultMessage = ""; - const check = choiceData.check; // Skill check object { stat, dc, onFailure } - - // --- Input Validation --- - // Handle explicit restart command (typically from page 99) - if (choiceData.next === 1 && (pageData = gameData[gameState.currentPageId]) && pageData.gameOver) { - console.log("Restarting game..."); - startGame(); - return; - } - // Basic validation for normal choices + let rollResultMessage = gameState.lastSellMessage || ""; // Carry over sell message if any + gameState.lastSellMessage = ""; // Clear sell message after use + const check = choiceData.check; + + // --- Basic Input Validation --- if (isNaN(optionNextPageId) && !check) { console.error("Invalid choice data: Missing 'next' page ID and no check defined.", choiceData); - // Attempt to render current page again with an error, or go to game over const currentPageData = gameData[gameState.currentPageId] || gameData[99]; renderPageInternal(gameState.currentPageId, currentPageData , "Error: Invalid choice data encountered! Cannot proceed.
"); - // Make choices inactive? choicesElement.querySelectorAll('button').forEach(b => b.disabled = true); return; } // --- Skill Check Logic --- if (check) { - const statValue = gameState.character.stats[check.stat] || 10; // Default to 10 if stat missing + 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); // Capitalize stat name + 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) { // Success - nextPageId = optionNextPageId; // Proceed to the 'next' page defined in the option - rollResultMessage = `${statName} Check Success! (Rolled ${roll} + ${modifier} = ${totalResult} vs DC ${dc})
`; + nextPageId = optionNextPageId; + rollResultMessage += `${statName} Check Success! (Rolled ${roll} + ${modifier} = ${totalResult} vs DC ${dc})
`; } else { // Failure - nextPageId = parseInt(check.onFailure); // Go to the 'onFailure' page ID - rollResultMessage = `${statName} Check Failed! (Rolled ${roll} + ${modifier} = ${totalResult} vs DC ${dc})
`; + nextPageId = parseInt(check.onFailure); + rollResultMessage += `${statName} Check Failed! (Rolled ${roll} + ${modifier} = ${totalResult} vs DC ${dc})
`; if (isNaN(nextPageId)) { console.error("Invalid onFailure ID:", check.onFailure); - nextPageId = 99; // Default to game over on invalid failure ID + nextPageId = 99; rollResultMessage += "Error: Invalid failure path defined!
"; } } } // --- Page Transition & Consequences --- - const targetPageData = gameData[nextPageId]; // Get data for the *next* page + const targetPageData = gameData[nextPageId]; if (!targetPageData) { console.error(`Data for target page ${nextPageId} not found!`); renderPageInternal(99, gameData[99] || { title: "Error", content: "Page Data Missing!
", illustration: "error", gameOver: true }, "Error: Next page data missing! Cannot continue.
"); @@ -931,33 +490,29 @@ gameState.character.stats.hp -= hpLostThisTurn; console.log(`Lost ${hpLostThisTurn} HP.`); } - // TODO: Implement hpGain (similar to hpLoss but adding HP) if (targetPageData.reward && targetPageData.reward.hpGain) { const hpGained = targetPageData.reward.hpGain; gameState.character.stats.hp += hpGained; console.log(`Gained ${hpGained} HP.`); - // Clamp HP to maxHP later } - // Check for death *after* applying HP changes for this turn + // Check for death *after* applying HP changes if (gameState.character.stats.hp <= 0) { - gameState.character.stats.hp = 0; // Don't go below 0 + gameState.character.stats.hp = 0; console.log("Player died!"); - nextPageId = 99; // Override navigation to game over page - rollResultMessage += `You have succumbed to your injuries! (-${hpLostThisTurn} HP)
`; - // Ensure we use game over page data for rendering - const gameOverPageData = gameData[nextPageId]; - renderPageInternal(nextPageId, gameOverPageData, rollResultMessage); - return; // Stop further processing for this choice + nextPageId = 99; // Force navigation to game over page + rollResultMessage += `You have succumbed to your injuries!${hpLostThisTurn > 0 ? ` (-${hpLostThisTurn} HP)` : ''}
`; + const gameOverPageData = gameData[nextPageId]; + renderPageInternal(nextPageId, gameOverPageData, rollResultMessage); + return; // Stop processing } - // Apply other rewards if the player is still alive + // Apply other rewards if alive if (targetPageData.reward) { if (targetPageData.reward.xp) { gameState.character.xp += targetPageData.reward.xp; console.log(`Gained ${targetPageData.reward.xp} XP! Total: ${gameState.character.xp}`); - // TODO: Check for Level Up - // checkLevelUp(); + // checkLevelUp(); // TODO } if (targetPageData.reward.statIncrease) { const stat = targetPageData.reward.statIncrease.stat; @@ -965,20 +520,15 @@ if (gameState.character.stats.hasOwnProperty(stat)) { gameState.character.stats[stat] += amount; console.log(`Stat ${stat} increased by ${amount}. New value: ${gameState.character.stats[stat]}`); - // Recalculate Max HP immediately if Constitution changes - if (stat === 'constitution') { - recalculateMaxHp(); - } + if (stat === 'constitution') recalculateMaxHp(); } } - // Add item from reward property if (targetPageData.reward.addItem && !gameState.character.inventory.includes(targetPageData.reward.addItem)) { gameState.character.inventory.push(targetPageData.reward.addItem); console.log(`Found item: ${targetPageData.reward.addItem}`); - rollResultMessage += `Item acquired: ${targetPageData.reward.addItem}
`; // Add feedback + rollResultMessage += `Item acquired: ${targetPageData.reward.addItem}
`; } } - // Add item from direct choice property (less common, but handle for compatibility) if (itemToAdd && !gameState.character.inventory.includes(itemToAdd)) { gameState.character.inventory.push(itemToAdd); console.log("Added item:", itemToAdd); @@ -986,122 +536,139 @@ } // --- Update Game State --- - gameState.currentPageId = nextPageId; // Update current page *after* processing consequences - - // Recalculate derived stats (like Max HP) and clamp current values + gameState.currentPageId = nextPageId; recalculateMaxHp(); - gameState.character.stats.hp = Math.min(gameState.character.stats.hp, gameState.character.stats.maxHp); // Clamp current HP + gameState.character.stats.hp = Math.min(gameState.character.stats.hp, gameState.character.stats.maxHp); console.log("Transitioning to page:", nextPageId, " New state:", JSON.stringify(gameState)); - // Render the determined next page - renderPageInternal(nextPageId, gameData[nextPageId], rollResultMessage); // Use potentially overridden nextPageId + renderPageInternal(nextPageId, gameData[nextPageId], rollResultMessage); + } + + function handleSellItem(itemName) { + console.log("Attempting to sell:", itemName); + const itemIndex = gameState.character.inventory.indexOf(itemName); + const itemInfo = itemsData[itemName]; + + if (itemIndex !== -1 && itemInfo && itemInfo.goldValue > 0) { + const value = itemInfo.goldValue; + gameState.character.gold += value; + gameState.character.inventory.splice(itemIndex, 1); // Remove item + gameState.lastSellMessage = `Sold ${itemName} for ${value} Gold.
`; + console.log(`Sold ${itemName} for ${value} gold. Current gold: ${gameState.character.gold}`); + } else { + console.warn("Could not sell item:", itemName, " - Item not found, no value, or invalid."); + gameState.lastSellMessage = `Cannot sell ${itemName}.
`; // Use failure style for error feedback + } + // Re-render the current page (which should be the Game Over page '99') + renderPageInternal(gameState.currentPageId, gameData[gameState.currentPageId], gameState.lastSellMessage); } + function recalculateMaxHp() { const baseHp = 10; // Base HP for level 1 adventurer const conModifier = Math.floor((gameState.character.stats.constitution - 10) / 2); - // TODO: Incorporate level into HP calculation, e.g., baseHp + (level * conModifier) + other bonuses - gameState.character.stats.maxHp = baseHp + conModifier * gameState.character.level; - // Ensure HP is at least 1? - gameState.character.stats.maxHp = Math.max(1, gameState.character.stats.maxHp); + gameState.character.stats.maxHp = Math.max(1, baseHp + conModifier * gameState.character.level); // Ensure HP is at least 1 } - // TODO: function checkLevelUp() { ... } - function renderPageInternal(pageId, pageData, message = "") { - // --- Page Data Validation --- if (!pageData) { console.error(`Render Error: No data for page ${pageId}`); - pageData = gameData[99] || { title: "Error", content: "Render Error! Critical page data missing.
", illustration: "error", gameOver: true }; // Fallback to error/game over + pageData = gameData[99] || { title: "Error", content: "Render Error! Critical page data missing.
", illustration: "error", gameOver: true }; message += "Render Error: Page data was missing! Cannot proceed.
"; - pageId = 99; // Ensure we treat this as the game over page + pageId = 99; } - console.log(`Rendering page ${pageId}: "${pageData.title}"`); - // --- Update UI Elements --- storyTitleElement.textContent = pageData.title || "Untitled Page"; - storyContentElement.innerHTML = message + (pageData.content || "...
"); // Prepend messages (like roll results) + // Inject message first, then page content + storyContentElement.innerHTML = message + (pageData.content || "...
"); updateStatsDisplay(); updateInventoryDisplay(); - choicesElement.innerHTML = ''; // Clear previous choices - // --- Generate Choices --- const options = pageData.options || []; - const isGameOverOrEnd = pageData.gameOver || (options.length === 0 && pageId !== 99); // Check if it's an end state + const isGameOverPage = pageData.gameOver === true; // Specifically check the gameOver flag + + // --- Generate Sell Buttons (Only on Game Over page if allowSell is true) --- + if (isGameOverPage && pageData.allowSell === true) { + const sellableItems = gameState.character.inventory.filter(itemName => { + const itemInfo = itemsData[itemName]; + return itemInfo && itemInfo.goldValue > 0 && itemInfo.type !== 'quest'; // Only sell non-quest items with value + }); + + if (sellableItems.length > 0) { + choicesElement.innerHTML += `There are no further paths from here.
'); - } + } else if (isGameOverPage) { // Game Over page needs restart button + const restartButton = document.createElement('button'); + restartButton.classList.add('choice-button'); + restartButton.textContent = "Restart Adventure (Keep Progress)"; + // Use the specific restart_plus action + restartButton.onclick = () => handleChoiceClick({ action: 'restart_plus' }); + choicesElement.appendChild(restartButton); + } else { // End of a branch (not game over page 99), offer restart + choicesElement.insertAdjacentHTML('beforeend', 'There are no further paths from here.
'); + 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); } - // --- Update 3D Scene --- updateScene(pageData.illustration || 'default'); } - function renderPage(pageId) { - // Simple wrapper, could add pre/post render logic here if needed - renderPageInternal(pageId, gameData[pageId]); - } + function renderPage(pageId) { renderPageInternal(pageId, gameData[pageId]); } function updateStatsDisplay() { const char=gameState.character; - statsElement.innerHTML = `Stats: Lvl: ${char.level} XP: ${char.xp}/${char.xpToNextLevel} HP: ${char.stats.hp}/${char.stats.maxHp} Str: ${char.stats.strength} Int: ${char.stats.intelligence} Wis: ${char.stats.wisdom} Dex: ${char.stats.dexterity} Con: ${char.stats.constitution} Cha: ${char.stats.charisma}`; + // Added Gold display + statsElement.innerHTML = `Stats: Gold: ${char.gold} Lvl: ${char.level} XP: ${char.xp}/${char.xpToNextLevel} HP: ${char.stats.hp}/${char.stats.maxHp} Str: ${char.stats.strength} Int: ${char.stats.intelligence} Wis: ${char.stats.wisdom} Dex: ${char.stats.dexterity} Con: ${char.stats.constitution} Cha: ${char.stats.charisma}`; } - function updateInventoryDisplay() { + function updateInventoryDisplay() { // Unchanged let h='Inventory: '; if(gameState.character.inventory.length === 0){ h+='Empty'; @@ -1109,285 +676,19 @@ gameState.character.inventory.forEach(itemName=>{ const item = itemsData[itemName] || {type:'unknown',description:'An unknown item.'}; const itemClass = `item-${item.type || 'unknown'}`; - // Ensure description is a string const descriptionText = typeof item.description === 'string' ? item.description : 'No description available.'; - h += `${itemName}`; // Escape quotes in title + h += `${itemName}`; }); } inventoryElement.innerHTML = h; } - function updateScene(illustrationKey) { - if (!scene) { - console.warn("Scene not initialized, cannot update visual."); - return; - } - console.log("Updating scene for illustration key:", illustrationKey); - - // --- Cleanup --- - if (currentAssemblyGroup) { - scene.remove(currentAssemblyGroup); - // Basic disposal - more complex scenes might need deeper disposal - currentAssemblyGroup.traverse(child => { - if (child.isMesh) { - child.geometry.dispose(); - // Dispose materials if they are unique to this assembly and not reused - // if (Array.isArray(child.material)) { - // child.material.forEach(m => m.dispose()); - // } else { - // child.material.dispose(); - // } - } - }); - currentAssemblyGroup = null; - } - scene.fog = null; // Reset fog - scene.background = new THREE.Color(0x222222); // Reset background - // Reset camera to default unless overridden by case - camera.position.set(0, 2.5, 7); - camera.lookAt(0, 0.5, 0); - - // --- Select Assembly --- - let assemblyFunction; - switch (illustrationKey) { - // Basic Structures - 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; - - // Landscape / Outdoor Scenes - 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': // Base visual is hills - case 'overgrown-stone-shrine-wildflowers-close': // Base visual is hills - 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); - // Adjust camera for close-up shrine views specifically - 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': // Reuse visual - case 'close-up-handholds-carved-in-cliff-face': // Reuse visual - 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': // Reuse visual - 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; // Placeholder - Needs badlands assembly - - // Forest Scenes - case 'shadowwood-forest': // Generic forest, use if needed - 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': // Reuse visual - 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': // Reuse visual - case 'forest-clearing-mossy-statue-offering': // Reuse visual - 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': // Reuse visual - case 'rope-bridge-snapping-character-falling': // Reuse visual - case 'fallen-log-crossing-ravine': // Reuse visual (needs log added?) - 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; // TODO: Needs bridge/ravine elements - case 'two-goblins-ambush-forest-path-spears': - case 'forest-shadows-hiding-goblins-walking-past': // Reuse visual, maybe different camera? - case 'defeated-goblins-forest-path-loot': // Reuse visual, remove goblins? - case 'blurred-motion-running-past-goblins-forest': // Reuse visual - 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; // TODO: Modify based on state - case 'forest-stream-crossing-dappled-sunlight-stones': - case 'mossy-log-bridge-over-forest-stream': // Needs log added? - case 'character-splashing-into-stream-from-log': // Reuse visual - 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; // TODO: Needs stream/log elements - case 'forest-edge-view-rocky-foothills-distant-mountain-fortress': - case 'forest-edge': // Explicit key for this scene - 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; - - // Mountain / Fortress Scenes - 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; // Placeholder - Needs rocky foothills assembly - 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; // Placeholder - 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; // Placeholder - 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; // Placeholder - 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; // Placeholder - Needs fortress walls assembly - - // Indoor Scenes - 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 / Fallback - default: - console.warn(`Unknown illustration key: "${illustrationKey}". Using default scene.`); - assemblyFunction = createDefaultAssembly; break; - } + // --- Scene Update and Lighting --- (Unchanged updateScene, adjustLighting) + 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(); } + 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); } - // --- Create and Add Assembly --- - try { - currentAssemblyGroup = assemblyFunction(); - if (currentAssemblyGroup && currentAssemblyGroup.isGroup) { // Check if it's a valid group - scene.add(currentAssemblyGroup); - adjustLighting(illustrationKey); // Adjust lighting based on the final scene key - } 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); } // Clean up potential partial assembly - currentAssemblyGroup = createErrorAssembly(); // Display error cone - scene.add(currentAssemblyGroup); - adjustLighting('error'); // Use default/error lighting - } - onWindowResize(); // Ensure camera aspect is correct after potential changes - } - - function adjustLighting(illustrationKey) { - if (!scene) return; - // Remove existing non-ambient lights first - 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 in scene, adding default."); - scene.add(new THREE.AmbientLight(0xffffff, 0.5)); // Add default if missing - } - - let directionalLight; - let lightIntensity = 1.2; - let ambientIntensity = 0.5; - let lightColor = 0xffffff; - let lightPosition = {x: 8, y: 15, z: 10}; - - // Adjust lighting based on scene type (match cases in updateScene) - switch (illustrationKey) { - // Sunny / Open Areas - 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; // Bright, warm light - // Forest / Dim Light - 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': // Edge might be brighter - ambientIntensity = 0.4; lightIntensity = 0.8; lightColor = 0xB0C4DE; lightPosition = {x: 5, y: 12, z: 5}; break; // Dimmer, slightly blue/green filtered light - // Caves / Dark Indoors - case 'dark-cave-entrance-dripping-water': - ambientIntensity = 0.1; lightIntensity = 0.3; lightColor = 0x667799; lightPosition = {x: 0, y: 5, z: 3}; break; // Very dim, cool light from entrance - case 'prisoner-cell': - ambientIntensity = 0.2; lightIntensity = 0.5; lightColor = 0x7777AA; lightPosition = {x: 0, y: 10, z: 5}; break; // Gloomy, cold light, maybe top-down - // Coastal / Overcast - 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; // Bright but cool/diffused coastal light - // Badlands / Harsh Light - case 'rocky-badlands-cracked-earth-harsh-sun': - ambientIntensity = 0.7; lightIntensity = 1.8; lightColor = 0xFFFFDD; lightPosition = {x: 5, y: 20, z: 5}; break; // Very bright, slightly yellow harsh sunlight - // Mountains / Fortress Approach - 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; // Clear mountain air light, slightly cool - // Special States - case 'game-over': case 'game-over-generic': - ambientIntensity = 0.2; lightIntensity = 0.8; lightColor = 0xFF6666; lightPosition = {x: 0, y: 5, z: 5}; break; // Reddish tint for game over - case 'error': - ambientIntensity = 0.4; lightIntensity = 1.0; lightColor = 0xFFCC00; lightPosition = {x: 0, y: 5, z: 5}; break; // Orange/Yellow tint for error - default: // Default lighting fallback - ambientIntensity = 0.5; lightIntensity = 1.2; lightColor = 0xffffff; lightPosition = {x: 8, y: 15, z: 10}; break; - } - - // Update ambient light - const currentAmbient = scene.children.find(c => c.isAmbientLight); - if (currentAmbient) { - currentAmbient.intensity = ambientIntensity; - } - - // Add new directional light - directionalLight = new THREE.DirectionalLight(lightColor, lightIntensity); - directionalLight.position.set(lightPosition.x, lightPosition.y, lightPosition.z); - directionalLight.castShadow = true; - // Configure shadow properties (adjust map size and camera frustum as needed) - directionalLight.shadow.mapSize.set(1024, 1024); // Or 2048 for better quality - directionalLight.shadow.camera.near = 0.5; - directionalLight.shadow.camera.far = 50; // Adjust based on typical scene scale - directionalLight.shadow.camera.left = -20; // Adjust frustum size based on scene scale - directionalLight.shadow.camera.right = 20; - directionalLight.shadow.camera.top = 20; - directionalLight.shadow.camera.bottom = -20; - directionalLight.shadow.bias = -0.001; // Adjust shadow bias to prevent artifacts - scene.add(directionalLight); - - // Optional: Add a light helper for debugging - // const helper = new THREE.DirectionalLightHelper( directionalLight, 5 ); - // scene.add( helper ); - // const shadowHelper = new THREE.CameraHelper( directionalLight.shadow.camera ); - // scene.add( shadowHelper ); - } - - // ======================================== - // Potential Future Improvements (Comment Block) - // ======================================== - /* - Potential Areas for Expansion/Improvement: - - * More Scene Variety: Add more create...Assembly functions and corresponding illustration keys for greater visual diversity (e.g., villages, ruins, different cave types, interiors). - * More Complex Scenes: The current scenes are quite abstract. More detail could be added, potentially involving more complex geometry generation (e.g., procedural buildings, terrain algorithms) or even simple pre-made models loaded for key elements (like the statue, specific monsters). - * Interaction: Add interaction with the 3D scene (e.g., clicking on objects using raycasting to examine them, pick up items visually, or activate levers). - * Combat System: Implement a more detailed combat mechanic instead of abstract resolution (e.g., turn-based, display enemy models, track enemy HP, use stats for attack/damage rolls, visual feedback for hits/misses). - * Character Progression: Implement a level-up system based on XP (increase stats, HP, unlock abilities/spells). Check for level up after gaining XP. - * Save/Load: Add functionality to save and load game progress (perhaps using `localStorage` to store the `gameState` object as a JSON string). Add save/load buttons to the UI. - * Data Management: For a larger game, storing `gameData` and `itemsData` in external JSON files and fetching them (`Workspace` API) would be more manageable than keeping them inline. - * Error Handling: Add more robust checks for missing data (items, pages) or potential errors during scene generation and game logic. Improve feedback on errors (e.g., specific messages instead of just going to page 99). - * Code Organization: Split the JavaScript into modules (e.g., `three-setup.js`, `game-logic.js`, `ui-manager.js`, `scene-generator.js`, `data.js`) for better maintainability using ES6 modules and imports. - * Refine Scene Logic: Improve how scene elements correspond to game state (e.g., actually remove defeated goblin models from the scene, visually represent items found on the ground before pickup, change scene lighting based on time of day if implemented). - * Add More Mechanics: Implement status effects (poison, bless), equipment slots (weapon, armor, shield) with stat modifiers, spells with costs (mana/charges), currency/shops, more complex skill checks (e.g., opposed checks, checks with advantage/disadvantage based on items/status), quest tracking system. - * Accessibility: Review and improve accessibility (ARIA attributes for dynamic content updates, keyboard navigation for choices). - * Performance: For very complex scenes or many objects, consider optimizing geometry (instancing similar objects like trees/rocks), using lower-poly models, texture atlases, and optimizing the render loop. Dispose of unused Three.js objects properly. - * Sound/Music: Add background music loops and sound effects for actions, ambiance, and feedback. - */ + // --- Potential Future Improvements Comment --- (Unchanged) + /* [Keep comment block here] */ // ======================================== // Initialization @@ -1397,23 +698,20 @@ try { initThreeJS(); if (scene && camera && renderer) { - startGame(); // Start the game logic only after successful Three.js init + // Use startNewGame for the very first load + startNewGame(); console.log("Game Started Successfully."); } else { - // If initThreeJS failed but didn't throw, or refs are null throw new Error("Three.js initialization failed or did not complete."); } } catch (error) { console.error("Initialization failed:", error); - // Display user-friendly error message in the UI storyTitleElement.textContent = "Initialization Error"; storyContentElement.innerHTML = `A critical error occurred during setup. The adventure cannot begin. Please check the console (F12) for technical details.
${error.stack || error}`; choicesElement.innerHTML = '
Cannot proceed due to initialization error.
'; - // Attempt to clear or show error in the 3D view area if (sceneContainer) { sceneContainer.innerHTML = '3D Scene Failed to Load
'; } - // Prevent further Three.js operations if it failed scene = null; camera = null; renderer = null; } });