Spaces:
Running
Running
| 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: `<p>The Evil Power Master has been terrorizing the land... You stand at the entrance to Silverhold, ready to begin your quest.</p><p>How will you prepare?</p>`, | |
| 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: `<p>Gorn the weaponsmith welcomes you. "You'll need more than common steel," he says, offering weapons.</p>`, | |
| 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: `<p>High Priestess Alara greets you. "Prepare your mind and spirit." She offers to teach you a secret art.</p>`, | |
| 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: `<p>Lyra, the resistance leader, shows you a map. "His fortress has three possible entry points." She offers an item.</p>`, | |
| 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: `<p>You leave Silverhold and enter the corrupted Shadowwood Forest. Strange sounds echo. Which path will you take?</p>`, | |
| 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: "<p>Scouts jump out! 'Surrender!'</p>", | |
| 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: "<p>You defeat the scouts and continue.</p>", | |
| options: [{ text: "Proceed to the fortress plains", next: 15 }], | |
| illustration: "forest-edge" | |
| }, | |
| "10": { // Example continuation | |
| title: "Captured!", | |
| content: "<p>You failed to escape and are captured!</p>", | |
| options: [{ text: "Accept fate (for now)", next: 20 }], // Go to prison wagon page | |
| illustration: "prisoner-cell" | |
| }, | |
| // Game Over placeholder | |
| "99": { | |
| title: "Game Over", | |
| content: "<p>Your adventure ends here.</p>", | |
| 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 = "<p>Could not load page data. Adventure halted.</p>"; | |
| choicesElement.innerHTML = '<button class="choice-button" onclick="handleChoice(1)">Restart</button>'; // Provide restart option | |
| updateScene('error'); // Show error scene | |
| return; | |
| } | |
| // Update UI | |
| storyTitleElement.textContent = page.title || "Untitled Page"; | |
| storyContentElement.innerHTML = page.content || "<p>...</p>"; | |
| 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 = '<p><i>No further options available from here.</i></p>'; | |
| 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 = '<strong>Stats:</strong> '; | |
| statsHTML += `<span>HP: ${gameState.stats.hp}/${gameState.stats.maxHp}</span>`; | |
| statsHTML += `<span>Str: ${gameState.stats.strength}</span>`; | |
| statsHTML += `<span>Wis: ${gameState.stats.wisdom}</span>`; | |
| statsHTML += `<span>Cor: ${gameState.stats.courage}</span>`; | |
| statsElement.innerHTML = statsHTML; | |
| } | |
| function updateInventoryDisplay() { | |
| let inventoryHTML = '<strong>Inventory:</strong> '; | |
| if (gameState.inventory.length === 0) { | |
| inventoryHTML += '<em>Empty</em>'; | |
| } 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 += `<span class="${itemClass}" title="${itemInfo.description}">${item}</span>`; | |
| }); | |
| } | |
| 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; |