import * as THREE from 'three'; // Optional: Add OrbitControls for debugging/viewing scene // import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; // --- DOM Elements --- const sceneContainer = document.getElementById('scene-container'); const storyTitleElement = document.getElementById('story-title'); const storyContentElement = document.getElementById('story-content'); const choicesElement = document.getElementById('choices'); const statsElement = document.getElementById('stats-display'); const inventoryElement = document.getElementById('inventory-display'); // --- Three.js Setup --- let scene, camera, renderer, cube; // Basic scene object // let controls; // Optional OrbitControls function initThreeJS() { // Scene scene = new THREE.Scene(); scene.background = new THREE.Color(0x222222); // Match body background // Camera camera = new THREE.PerspectiveCamera(75, sceneContainer.clientWidth / sceneContainer.clientHeight, 0.1, 1000); camera.position.z = 5; // Renderer renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(sceneContainer.clientWidth, sceneContainer.clientHeight); sceneContainer.appendChild(renderer.domElement); // Basic Lighting const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); // Soft white light scene.add(ambientLight); const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0); directionalLight.position.set(5, 10, 7.5); scene.add(directionalLight); // Basic Object (Placeholder for scene illustration) const geometry = new THREE.BoxGeometry(1, 1, 1); const material = new THREE.MeshStandardMaterial({ color: 0xcccccc }); // Default color cube = new THREE.Mesh(geometry, material); scene.add(cube); // Optional Controls // controls = new OrbitControls(camera, renderer.domElement); // controls.enableDamping = true; // Handle Resize window.addEventListener('resize', onWindowResize, false); // Start Animation Loop animate(); } function onWindowResize() { if (!renderer || !camera) return; camera.aspect = sceneContainer.clientWidth / sceneContainer.clientHeight; camera.updateProjectionMatrix(); renderer.setSize(sceneContainer.clientWidth, sceneContainer.clientHeight); } function animate() { requestAnimationFrame(animate); // Simple animation if (cube) { cube.rotation.x += 0.005; cube.rotation.y += 0.005; } // if (controls) controls.update(); // If using OrbitControls if (renderer && scene && camera) { renderer.render(scene, camera); } } // --- Game Data (Ported from Python, simplified for now) --- const gameData = { "1": { title: "The Beginning", content: `
The Evil Power Master has been terrorizing the land... You stand at the entrance to Silverhold, ready to begin your quest.
How will you prepare?
`, options: [ { text: "Visit the local weaponsmith", next: 2, /* addItem: "..." */ }, { text: "Seek wisdom at the temple", next: 3, /* addItem: "..." */ }, { text: "Meet the resistance leader", next: 4, /* addItem: "..." */ } ], illustration: "city-gates" // Key for Three.js scene }, "2": { title: "The Weaponsmith", content: `Gorn the weaponsmith welcomes you. "You'll need more than common steel," he says, offering weapons.
`, options: [ { text: "Take the Flaming Sword", next: 5, addItem: "Flaming Sword" }, { text: "Choose the Whispering Bow", next: 5, addItem: "Whispering Bow" }, { text: "Select the Guardian Shield", next: 5, addItem: "Guardian Shield" } ], illustration: "weaponsmith" }, "3": { title: "The Ancient Temple", content: `High Priestess Alara greets you. "Prepare your mind and spirit." She offers to teach you a secret art.
`, options: [ { text: "Learn Healing Light", next: 5, addItem: "Healing Light Spell" }, { text: "Master Shield of Faith", next: 5, addItem: "Shield of Faith Spell" }, { text: "Study Binding Runes", next: 5, addItem: "Binding Runes Scroll" } ], illustration: "temple" }, "4": { title: "The Resistance Leader", content: `Lyra, the resistance leader, shows you a map. "His fortress has three possible entry points." She offers an item.
`, options: [ { text: "Take the Secret Tunnel Map", next: 5, addItem: "Secret Tunnel Map" }, { text: "Accept Poison Daggers", next: 5, addItem: "Poison Daggers" }, { text: "Choose the Master Key", next: 5, addItem: "Master Key" } ], illustration: "resistance-meeting" }, "5": { title: "The Journey Begins", content: `You leave Silverhold and enter the corrupted Shadowwood Forest. Strange sounds echo. Which path will you take?
`, options: [ { text: "Take the main road", next: 6 }, // Leads to page 6 (Ambush) { text: "Follow the river path", next: 7 }, // Leads to page 7 (River Spirit) { text: "Brave the ruins shortcut", next: 8 } // Leads to page 8 (Ruins) ], illustration: "shadowwood-forest" // Key for Three.js scene // Add more pages here... }, // Add placeholder pages 6, 7, 8 etc. to continue the story "6": { title: "Ambush!", content: "Scouts jump out! 'Surrender!'
", options: [{ text: "Fight!", next: 9 }, { text: "Try to flee!", next: 10 }], // Example links illustration: "road-ambush" }, // ... Add many more pages based on your Python data ... "9": { // Example continuation title: "Victory!", content: "You defeat the scouts and continue.
", options: [{ text: "Proceed to the fortress plains", next: 15 }], illustration: "forest-edge" }, "10": { // Example continuation title: "Captured!", content: "You failed to escape and are captured!
", options: [{ text: "Accept fate (for now)", next: 20 }], // Go to prison wagon page illustration: "prisoner-cell" }, // Game Over placeholder "99": { title: "Game Over", content: "Your adventure ends here.
", options: [{ text: "Restart", next: 1 }], // Link back to start illustration: "game-over", gameOver: true } }; const itemsData = { // Simplified item data "Flaming Sword": { type: "weapon", description: "A fiery blade" }, "Whispering Bow": { type: "weapon", description: "A silent bow" }, "Guardian Shield": { type: "armor", description: "A protective shield" }, "Healing Light Spell": { type: "spell", description: "Mends minor wounds" }, "Shield of Faith Spell": { type: "spell", description: "Temporary shield" }, "Binding Runes Scroll": { type: "spell", description: "Binds an enemy" }, "Secret Tunnel Map": { type: "quest", description: "Shows a hidden path" }, "Poison Daggers": { type: "weapon", description: "Daggers with poison" }, "Master Key": { type: "quest", description: "Unlocks many doors" }, // Add other items... }; // --- Game State --- let gameState = { currentPageId: 1, inventory: [], stats: { courage: 7, wisdom: 5, strength: 6, hp: 30, maxHp: 30 } }; // --- Game Logic Functions --- function startGame() { gameState = { // Reset state currentPageId: 1, inventory: [], stats: { courage: 7, wisdom: 5, strength: 6, hp: 30, maxHp: 30 } }; renderPage(gameState.currentPageId); } function renderPage(pageId) { const page = gameData[pageId]; if (!page) { console.error(`Error: Page data not found for ID: ${pageId}`); storyTitleElement.textContent = "Error"; storyContentElement.innerHTML = "Could not load page data. Adventure halted.
"; choicesElement.innerHTML = ''; // Provide restart option updateScene('error'); // Show error scene return; } // Update UI storyTitleElement.textContent = page.title || "Untitled Page"; storyContentElement.innerHTML = page.content || "...
"; updateStatsDisplay(); updateInventoryDisplay(); // Update Choices choicesElement.innerHTML = ''; // Clear old choices if (page.options && page.options.length > 0) { page.options.forEach(option => { const button = document.createElement('button'); button.classList.add('choice-button'); button.textContent = option.text; // Check requirements (basic check for now) let requirementMet = true; if (option.requireItem && !gameState.inventory.includes(option.requireItem)) { requirementMet = false; button.title = `Requires: ${option.requireItem}`; // Tooltip button.disabled = true; } // Add requireAnyItem check here later if needed if (requirementMet) { // Store data needed for handling the choice button.dataset.nextPage = option.next; if (option.addItem) { button.dataset.addItem = option.addItem; } // Add other potential effects as data attributes if needed button.onclick = () => handleChoiceClick(button.dataset); } choicesElement.appendChild(button); }); } else if (page.gameOver) { const button = document.createElement('button'); button.classList.add('choice-button'); button.textContent = "Restart Adventure"; button.dataset.nextPage = 1; // Restart goes to page 1 button.onclick = () => handleChoiceClick(button.dataset); choicesElement.appendChild(button); } else { choicesElement.innerHTML = 'No further options available from here.
'; const button = document.createElement('button'); button.classList.add('choice-button'); button.textContent = "Restart Adventure"; button.dataset.nextPage = 1; // Restart goes to page 1 button.onclick = () => handleChoiceClick(button.dataset); choicesElement.appendChild(button); } // Update 3D Scene updateScene(page.illustration || 'default'); } function handleChoiceClick(dataset) { const nextPageId = parseInt(dataset.nextPage); // Ensure it's a number const itemToAdd = dataset.addItem; if (isNaN(nextPageId)) { console.error("Invalid nextPageId:", dataset.nextPage); return; } // --- Process Effects of Making the Choice --- // Add item if specified and not already present if (itemToAdd && !gameState.inventory.includes(itemToAdd)) { gameState.inventory.push(itemToAdd); console.log("Added item:", itemToAdd); } // Add stat changes/hp loss *linked to the choice itself* here if needed // --- Move to Next Page and Process Landing Effects --- gameState.currentPageId = nextPageId; const nextPageData = gameData[nextPageId]; if (nextPageData) { // Apply HP loss defined on the *landing* page if (nextPageData.hpLoss) { gameState.stats.hp -= nextPageData.hpLoss; console.log(`Lost ${nextPageData.hpLoss} HP.`); if (gameState.stats.hp <= 0) { console.log("Player died from HP loss!"); gameState.stats.hp = 0; renderPage(99); // Go to a specific game over page ID return; // Stop further processing } } // Apply stat increase defined on the *landing* page if (nextPageData.statIncrease) { const stat = nextPageData.statIncrease.stat; const amount = nextPageData.statIncrease.amount; if (gameState.stats.hasOwnProperty(stat)) { gameState.stats[stat] += amount; console.log(`Stat ${stat} increased by ${amount}.`); } } // Check if landing page is game over if (nextPageData.gameOver) { console.log("Reached Game Over page."); renderPage(nextPageId); return; } } else { console.error(`Data for page ${nextPageId} not found!`); // Optionally go to an error page or restart renderPage(99); // Go to game over page as fallback return; } // Render the new page renderPage(nextPageId); } function updateStatsDisplay() { let statsHTML = 'Stats: '; statsHTML += `HP: ${gameState.stats.hp}/${gameState.stats.maxHp}`; statsHTML += `Str: ${gameState.stats.strength}`; statsHTML += `Wis: ${gameState.stats.wisdom}`; statsHTML += `Cor: ${gameState.stats.courage}`; statsElement.innerHTML = statsHTML; } function updateInventoryDisplay() { let inventoryHTML = 'Inventory: '; if (gameState.inventory.length === 0) { inventoryHTML += 'Empty'; } else { gameState.inventory.forEach(item => { const itemInfo = itemsData[item] || { type: 'unknown', description: '???' }; // Add class based on item type for styling const itemClass = `item-${itemInfo.type || 'unknown'}`; inventoryHTML += `${item}`; }); } inventoryElement.innerHTML = inventoryHTML; } function updateScene(illustrationKey) { console.log("Updating scene for:", illustrationKey); if (!cube) return; // Don't do anything if cube isn't initialized // Simple scene update: Change cube color based on key let color = 0xcccccc; // Default grey switch (illustrationKey) { case 'city-gates': color = 0xaaaaaa; break; case 'weaponsmith': color = 0x8B4513; break; // Brown case 'temple': color = 0xFFFFE0; break; // Light yellow case 'resistance-meeting': color = 0x696969; break; // Dim grey case 'shadowwood-forest': color = 0x228B22; break; // Forest green case 'road-ambush': color = 0xD2691E; break; // Chocolate (dirt road) case 'river-spirit': color = 0xADD8E6; break; // Light blue case 'ancient-ruins': color = 0x778899; break; // Light slate grey case 'forest-edge': color = 0x90EE90; break; // Light green case 'prisoner-cell': color = 0x444444; break; // Dark grey case 'game-over': color = 0xff0000; break; // Red case 'error': color = 0xffa500; break; // Orange default: color = 0xcccccc; break; // Default grey for unknown } cube.material.color.setHex(color); // In a more complex setup, you would: // 1. Remove old objects from the scene (scene.remove(object)) // 2. Load/create new objects based on illustrationKey // 3. Add new objects to the scene (scene.add(newObject)) } // --- Initialization --- initThreeJS(); startGame(); // Start the game after setting up Three.js // Make handleChoiceClick globally accessible IF using inline onclick // If using addEventListener, this is not needed. // window.handleChoiceClick = handleChoiceClick;