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; |