Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Procedural Adventure Reboot</title> | |
<style> | |
body { | |
font-family: 'Courier New', monospace; | |
background-color: #1a1a1a; | |
color: #e0e0e0; | |
margin: 0; | |
padding: 0; | |
overflow: hidden; | |
display: flex; | |
flex-direction: column; | |
height: 100vh; | |
} | |
#game-container { | |
display: flex; | |
flex-grow: 1; | |
overflow: hidden; | |
} | |
#scene-container { | |
flex-grow: 3; | |
position: relative; | |
border-right: 2px solid #444; | |
min-width: 250px; | |
background-color: #000; | |
height: 100%; | |
box-sizing: border-box; | |
overflow: hidden; | |
} | |
#ui-container { | |
flex-grow: 2; | |
padding: 25px; | |
overflow-y: auto; | |
background-color: #2b2b2b; | |
min-width: 320px; | |
height: 100%; | |
box-sizing: border-box; | |
display: flex; | |
flex-direction: column; | |
} | |
#scene-container canvas { display: block; } | |
#story-title { | |
color: #f0c060; | |
margin: 0 0 15px 0; | |
padding-bottom: 10px; | |
border-bottom: 1px solid #555; | |
font-size: 1.6em; | |
text-shadow: 1px 1px 1px #000; | |
} | |
#story-content { | |
margin-bottom: 25px; | |
line-height: 1.7; | |
flex-grow: 1; | |
font-size: 1.1em; | |
} | |
#story-content p { margin-bottom: 1.1em; } | |
#story-content p:last-child { margin-bottom: 0; } | |
#stats-inventory-container { | |
margin-bottom: 25px; | |
padding: 15px; | |
border: 1px solid #444; | |
border-radius: 4px; | |
background-color: #333; | |
font-size: 0.95em; | |
} | |
#stats-display, #inventory-display { | |
margin-bottom: 10px; | |
line-height: 1.8; | |
} | |
#stats-display span, #inventory-display span { | |
display: inline-block; | |
background-color: #484848; | |
padding: 3px 9px; | |
border-radius: 15px; | |
margin: 0 8px 5px 0; | |
border: 1px solid #6a6a6a; | |
white-space: nowrap; | |
box-shadow: inset 0 1px 2px rgba(0,0,0,0.3); | |
} | |
#stats-display strong, #inventory-display strong { color: #ccc; margin-right: 6px; } | |
#inventory-display em { color: #888; font-style: normal; } | |
.item-quest { background-color: #666030; border-color: #999048;} | |
.item-weapon { background-color: #663030; border-color: #994848;} | |
.item-armor { background-color: #306630; border-color: #489948;} | |
.item-consumable { background-color: #664430; border-color: #996648;} | |
.item-unknown { background-color: #555; border-color: #777;} | |
#choices-container { | |
margin-top: auto; | |
padding-top: 20px; | |
border-top: 1px solid #555; | |
} | |
#choices-container h3 { margin-top: 0; margin-bottom: 12px; color: #ccc; font-size: 1.1em; } | |
#choices { display: flex; flex-direction: column; gap: 12px; } | |
.choice-button { | |
display: block; width: 100%; padding: 12px 15px; | |
margin-bottom: 0; | |
background-color: #555; color: #eee; border: 1px solid #777; | |
border-radius: 4px; cursor: pointer; text-align: left; | |
font-family: 'Courier New', monospace; font-size: 1.05em; | |
transition: background-color 0.2s, border-color 0.2s, box-shadow 0.1s; | |
box-sizing: border-box; | |
} | |
.choice-button:hover:not(:disabled) { | |
background-color: #e0b050; color: #111; border-color: #c89040; | |
box-shadow: 0 0 5px rgba(255, 200, 100, 0.5); | |
} | |
.choice-button:disabled { | |
background-color: #404040; color: #777; cursor: not-allowed; border-color: #555; | |
opacity: 0.7; | |
} | |
.choice-button[title]:disabled::after { | |
content: ' (' attr(title) ')'; font-style: italic; color: #999; font-size: 0.9em; margin-left: 5px; | |
} | |
.message { | |
padding: 8px 12px; margin-bottom: 1em; border-left-width: 3px; border-left-style: solid; | |
font-size: 0.95em; background-color: rgba(255, 255, 255, 0.05); | |
} | |
.message-success { color: #8f8; border-left-color: #4a4; } | |
.message-failure { color: #f88; border-left-color: #a44; } | |
.message-info { color: #aaa; border-left-color: #666; } | |
.message-item { color: #8bf; border-left-color: #46a; } | |
</style> | |
</head> | |
<body> | |
<div id="game-container"> | |
<div id="scene-container"></div> | |
<div id="ui-container"> | |
<h2 id="story-title">Initiating...</h2> | |
<div id="story-content"><p>Stand by...</p></div> | |
<div id="stats-inventory-container"> | |
<div id="stats-display"></div> | |
<div id="inventory-display"></div> | |
</div> | |
<div id="choices-container"> | |
<h3>What will you do?</h3> | |
<div id="choices"></div> | |
</div> | |
</div> | |
</div> | |
<script type="importmap"> | |
{ "imports": { | |
"three": "https://unpkg.com/[email protected]/build/three.module.js", | |
"three/addons/": "https://unpkg.com/[email protected]/examples/jsm/" | |
}} | |
</script> | |
<script type="module"> | |
import * as THREE from 'three'; | |
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'); | |
let scene, camera, renderer, clock; | |
let currentSceneGroup = null; | |
let currentLights = []; | |
const MAT = { | |
stone: new THREE.MeshStandardMaterial({ color: 0x777788, roughness: 0.85, metalness: 0.1 }), | |
wood: new THREE.MeshStandardMaterial({ color: 0x9F6633, roughness: 0.75, metalness: 0 }), | |
leaf: new THREE.MeshStandardMaterial({ color: 0x3E9B4E, roughness: 0.6, metalness: 0, side: THREE.DoubleSide }), | |
ground: new THREE.MeshStandardMaterial({ color: 0x556B2F, roughness: 0.95, metalness: 0 }), | |
metal: new THREE.MeshStandardMaterial({ color: 0xaaaaaa, metalness: 0.85, roughness: 0.35 }), | |
dirt: new THREE.MeshStandardMaterial({ color: 0x8B5E3C, roughness: 0.9 }), | |
grass: new THREE.MeshStandardMaterial({ color: 0x4CB781, roughness: 0.85 }), | |
water: new THREE.MeshStandardMaterial({ color: 0x4682B4, roughness: 0.3, metalness: 0.2, transparent: true, opacity: 0.85 }), | |
error: new THREE.MeshStandardMaterial({ color: 0xff3300, roughness: 0.5, emissive: 0x551100 }), | |
gameOver: new THREE.MeshStandardMaterial({ color: 0xaa0000, roughness: 0.6, metalness: 0.2, emissive: 0x220000 }), | |
simple: new THREE.MeshStandardMaterial({ color: 0xaaaaaa, roughness: 0.8 }), | |
}; | |
function initThreeJS() { | |
scene = new THREE.Scene(); | |
scene.background = new THREE.Color(0x1a1a1a); | |
clock = new THREE.Clock(); | |
const width = sceneContainer.clientWidth || 1; | |
const height = sceneContainer.clientHeight || 1; | |
camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 1000); | |
camera.position.set(0, 3, 9); | |
camera.lookAt(0, 0.5, 0); | |
renderer = new THREE.WebGLRenderer({ antialias: true }); | |
renderer.setSize(width, height); | |
renderer.shadowMap.enabled = true; | |
renderer.shadowMap.type = THREE.PCFSoftShadowMap; | |
renderer.toneMapping = THREE.ACESFilmicToneMapping; | |
renderer.outputColorSpace = THREE.SRGBColorSpace; | |
sceneContainer.appendChild(renderer.domElement); | |
window.addEventListener('resize', onWindowResize, false); | |
setTimeout(onWindowResize, 50); | |
animate(); | |
} | |
function onWindowResize() { | |
if (!renderer || !camera || !sceneContainer) return; | |
const width = sceneContainer.clientWidth || 1; | |
const height = sceneContainer.clientHeight || 1; | |
camera.aspect = width / height; | |
camera.updateProjectionMatrix(); | |
renderer.setSize(width, height); | |
} | |
function animate() { | |
requestAnimationFrame(animate); | |
const delta = clock.getDelta(); | |
const time = clock.getElapsedTime(); | |
scene.traverse(obj => { if (obj.userData.update) obj.userData.update(time, delta); }); | |
if (renderer && scene && camera) renderer.render(scene, camera); | |
} | |
function createMesh(geometry, material, pos = {x:0,y:0,z:0}, rot = {x:0,y:0,z:0}, scale = {x:1,y:1,z:1}) { | |
const mesh = new THREE.Mesh(geometry, material); | |
mesh.position.set(pos.x, pos.y, pos.z); | |
mesh.rotation.set(rot.x, rot.y, rot.z); | |
mesh.scale.set(scale.x, scale.y, scale.z); | |
mesh.castShadow = true; mesh.receiveShadow = true; | |
return mesh; | |
} | |
function createGround(material = MAT.ground, size = 20) { | |
const geo = new THREE.PlaneGeometry(size, size); | |
const ground = new THREE.Mesh(geo, material); | |
ground.rotation.x = -Math.PI / 2; ground.position.y = 0; | |
ground.receiveShadow = true; ground.castShadow = false; | |
return ground; | |
} | |
function setupLighting(type = 'default') { | |
currentLights.forEach(light => scene.remove(light)); | |
currentLights = []; | |
let ambientIntensity = 0.3; | |
let dirIntensity = 0.8; | |
let dirColor = 0xffffff; | |
let dirPosition = new THREE.Vector3(10, 15, 8); | |
if (type === 'forest') { | |
ambientIntensity = 0.2; dirIntensity = 0.6; dirColor = 0xccffcc; dirPosition = new THREE.Vector3(5, 10, 5); | |
} else if (type === 'cave') { | |
ambientIntensity = 0.1; dirIntensity = 0; | |
const ptLight = new THREE.PointLight(0xffaa55, 1.5, 12, 1); | |
ptLight.position.set(0, 1.5, 1); | |
ptLight.castShadow = true; | |
ptLight.shadow.mapSize.set(512, 512); | |
scene.add(ptLight); currentLights.push(ptLight); | |
} else if (type === 'gameover') { | |
ambientIntensity = 0.1; dirIntensity = 0.4; dirColor = 0xff6666; | |
} | |
const ambientLight = new THREE.AmbientLight(0xffffff, ambientIntensity); | |
scene.add(ambientLight); | |
currentLights.push(ambientLight); | |
if (dirIntensity > 0) { | |
const directionalLight = new THREE.DirectionalLight(dirColor, dirIntensity); | |
directionalLight.position.copy(dirPosition); | |
directionalLight.castShadow = true; | |
directionalLight.shadow.mapSize.set(1024, 1024); | |
directionalLight.shadow.camera.near = 0.5; | |
directionalLight.shadow.camera.far = 50; | |
const shadowBounds = 15; | |
directionalLight.shadow.camera.left = -shadowBounds; | |
directionalLight.shadow.camera.right = shadowBounds; | |
directionalLight.shadow.camera.top = shadowBounds; | |
directionalLight.shadow.camera.bottom = -shadowBounds; | |
directionalLight.shadow.bias = -0.0005; | |
scene.add(directionalLight); | |
currentLights.push(directionalLight); | |
} | |
} | |
function createDefaultScene() { | |
const group = new THREE.Group(); | |
group.add(createGround(MAT.dirt, 15)); | |
const boxGeo = new THREE.BoxGeometry(1, 1, 1); | |
group.add(createMesh(boxGeo, MAT.stone, {y: 0.5})); | |
return { group, lighting: 'default' }; | |
} | |
function createForestScene() { | |
const group = new THREE.Group(); | |
group.add(createGround(MAT.ground, 25)); | |
const trunkGeo = new THREE.CylinderGeometry(0.2, 0.3, 4, 8); | |
const leafGeo = new THREE.SphereGeometry(1.5, 8, 6); | |
for(let i=0; i<15; i++) { | |
const x = (Math.random() - 0.5) * 20; | |
const z = (Math.random() - 0.5) * 20; | |
if(Math.sqrt(x*x+z*z) < 2) continue; | |
const tree = new THREE.Group(); | |
const trunk = createMesh(trunkGeo, MAT.wood, {y: 2}); | |
const leaves = createMesh(leafGeo, MAT.leaf, {y: 4.5}); | |
tree.add(trunk); tree.add(leaves); | |
tree.position.set(x, 0, z); | |
tree.rotation.y = Math.random() * Math.PI * 2; | |
group.add(tree); | |
} | |
return { group, lighting: 'forest' }; | |
} | |
function createGameOverScene() { | |
const group = new THREE.Group(); | |
group.add(createGround(MAT.stone.clone().set({color: 0x333333}), 10)); | |
const boxGeo = new THREE.BoxGeometry(2, 2, 2); | |
group.add(createMesh(boxGeo, MAT.gameOver, {y: 1})); | |
return { group, lighting: 'gameover' }; | |
} | |
function createCaveScene() { | |
const group = new THREE.Group(); | |
group.add(createGround(MAT.stone, 15)); | |
const wallGeo = new THREE.SphereGeometry(10, 32, 16, 0, Math.PI*2, 0, Math.PI*0.7); | |
const walls = createMesh(wallGeo, MAT.stone, {y: 3}); | |
walls.material.side = THREE.BackSide; | |
group.add(walls); | |
const coneGeo = new THREE.ConeGeometry(0.2, 1.0, 8); | |
for(let i=0; i<10; i++){ | |
const st = createMesh(coneGeo, MAT.stone, {x: (Math.random()-0.5)*12, y: 5 + Math.random(), z: (Math.random()-0.5)*12}, {x:Math.PI}); | |
group.add(st); | |
} | |
return { group, lighting: 'cave' }; | |
} | |
function updateScene(illustrationKey = 'default') { | |
if (currentSceneGroup) { | |
currentSceneGroup.traverse(child => { | |
if (child.isMesh) child.geometry.dispose(); | |
}); | |
scene.remove(currentSceneGroup); | |
currentSceneGroup = null; | |
} | |
let sceneData; | |
switch (illustrationKey) { | |
case 'forest': case 'overgrown-path': case 'goblin-ambush': | |
sceneData = createForestScene(); break; | |
case 'game-over': | |
sceneData = createGameOverScene(); break; | |
case 'cave': | |
sceneData = createCaveScene(); break; | |
default: | |
sceneData = createDefaultScene(); break; | |
} | |
currentSceneGroup = sceneData.group; | |
scene.add(currentSceneGroup); | |
setupLighting(sceneData.lighting); | |
} | |
const itemsData = { | |
"Rusty Sword": {type:"weapon", description:"Old but sharp."}, | |
"Torch": {type:"consumable", description:"Provides light.", use: "light"}, | |
"Key": {type:"quest", description:"A simple iron key."} | |
}; | |
const gameData = { | |
1: { | |
title: "A Fork in the Path", | |
content: "<p>The dirt path splits. To the north, it enters a dark forest. To the east, it winds towards distant hills.</p>", | |
options: [ | |
{ text: "Enter the Forest", next: 2 }, | |
{ text: "Head towards the Hills", next: 3 } | |
], | |
illustration: "forest" | |
}, | |
2: { | |
title: "Deep Forest", | |
content: "<p>The trees loom overhead, blocking most light. It's eerily quiet. You spot a faint glimmer ahead.</p>", | |
options: [ | |
{ text: "Investigate the glimmer", next: 4 }, | |
{ text: "Turn back to the path", next: 1 } | |
], | |
illustration: "forest" | |
}, | |
3: { | |
title: "Rolling Hills", | |
content: "<p>The hills are gentle under an open sky. You see a small cave entrance in the side of one hill.</p>", | |
options: [ | |
{ text: "Explore the cave", next: 5 }, | |
{ text: "Continue over the hills", next: 99 }, | |
{ text: "Go back to the path", next: 1 } | |
], | |
illustration: "default" | |
}, | |
4: { | |
title: "Forest Glimmer", | |
content: "<p>The glimmer comes from a Rusty Sword half-buried in the leaves.</p>", | |
options: [ { text: "Take the sword", next: 1, reward: {addItem: "Rusty Sword"} } ], | |
illustration: "forest" | |
}, | |
5: { | |
title: "Small Cave", | |
content: "<p>The cave is dark and damp. You can barely see. Something skitters in the darkness.</p>", | |
options: [ | |
{ text: "Light Torch (if you have one)", requireItem: "Torch", consumeItem: true, next: 6}, | |
{ text: "Try to fight in the dark (Dexterity Check DC 13)", check: {stat: "dexterity", dc: 13, onFailure: 7}, next: 8}, | |
{ text: "Flee the cave", next: 3 } | |
], | |
illustration: "cave" | |
}, | |
6: { | |
title: "Cave - Lit", | |
content: "<p>Your torch illuminates the cave. You see giant spiders! One lunges.</p>", | |
options: [ { text: "Fight!", next: 8 } ], | |
illustration: "cave" | |
}, | |
7: { | |
title: "Cave - Failure", | |
content: "<p>Thrashing blindly, you feel a sharp pain as something bites you! (-5 HP)</p>", | |
options: [ { text: "Try to flee!", next: 3, hpLoss: 5 } ], | |
illustration: "cave" | |
}, | |
8: { | |
title: "Cave - Victory", | |
content: "<p>You manage to defeat the creatures! You find an old Key.</p>", | |
options: [ { text: "Leave the cave", next: 3, reward: {addItem: "Key", xp: 50} } ], | |
illustration: "cave" | |
}, | |
99: { | |
title: "The End?", | |
content: "<p>Your journey ends here... for now.</p>", | |
options: [ { text: "Restart", next: 1 } ], | |
illustration: "game-over", | |
gameOver: true | |
} | |
}; | |
let gameState = { | |
currentPageId: 1, | |
character: { | |
name: "Hero", | |
stats: { strength: 10, dexterity: 10, constitution: 10, hp: 15, maxHp: 15, xp: 0 }, | |
inventory: [] | |
} | |
}; | |
function startGame() { | |
const defaultChar = { | |
name: "Hero", | |
stats: { strength: 10, dexterity: 10, constitution: 10, hp: 15, maxHp: 15, xp: 0 }, | |
inventory: [] | |
}; | |
gameState = { | |
currentPageId: 1, | |
character: JSON.parse(JSON.stringify(defaultChar)) | |
}; | |
console.log("Starting new game:", gameState); | |
renderPage(gameState.currentPageId); | |
} | |
function handleChoiceClick(choiceData) { | |
console.log("Choice:", choiceData); | |
let messageLog = ""; | |
let consumedItemName = null; | |
if (choiceData.consumeItem && choiceData.requireItem) { | |
if (gameState.character.inventory.includes(choiceData.requireItem)) { | |
gameState.character.inventory = gameState.character.inventory.filter(i => i !== choiceData.requireItem); | |
consumedItemName = choiceData.requireItem; | |
messageLog += `<p class="message message-item"><em>Used: ${consumedItemName}</em></p>`; | |
} else { | |
console.error("Tried to consume unavailable item:", choiceData.requireItem); | |
return; | |
} | |
} | |
let nextPageId = parseInt(choiceData.nextPage); | |
const check = choiceData.check; | |
if (check) { | |
const baseStatValue = gameState.character.stats[check.stat] || 10; | |
const roll = Math.floor(Math.random() * 20) + 1; | |
const modifier = Math.floor((baseStatValue - 10) / 2); | |
const totalResult = roll + modifier; | |
const dc = check.dc; | |
console.log(`Check: ${check.stat} (Base: ${baseStatValue}, Mod: ${modifier}) | Roll: ${roll} -> Total: ${totalResult} vs DC ${dc}`); | |
if (totalResult >= dc) { | |
messageLog += `<p class="message message-success"><em>${check.stat.charAt(0).toUpperCase() + check.stat.slice(1)} Check Success! (${totalResult} vs DC ${dc})</em></p>`; | |
nextPageId = parseInt(choiceData.nextPage); | |
} else { | |
messageLog += `<p class="message message-failure"><em>${check.stat.charAt(0).toUpperCase() + check.stat.slice(1)} Check Failed! (${totalResult} vs DC ${dc})</em></p>`; | |
nextPageId = parseInt(check.onFailure); | |
if (isNaN(nextPageId)) { | |
console.error("Invalid onFailure ID:", check.onFailure); | |
nextPageId = 99; | |
messageLog += `<p class="message message-failure">Error: Invalid failure path!</p>`; | |
} | |
} | |
} | |
const targetPageData = gameData[nextPageId]; | |
if (!targetPageData) { | |
console.error(`Page data missing for ID: ${nextPageId}`); | |
renderPageInternal(99, gameData[99], messageLog + `<p class="message message-failure">Error: Page data missing!</p>`); | |
return; | |
} | |
let hpChange = 0; | |
if (targetPageData.hpLoss) { | |
hpChange = -targetPageData.hpLoss; | |
messageLog += `<p class="message message-failure"><em>Lost ${targetPageData.hpLoss} HP.</em></p>`; | |
} | |
if (targetPageData.reward?.hpGain) { | |
hpChange = targetPageData.reward.hpGain; | |
messageLog += `<p class="message message-success"><em>Recovered ${targetPageData.reward.hpGain} HP.</em></p>`; | |
} | |
if(hpChange !== 0) { | |
gameState.character.stats.hp += hpChange; | |
gameState.character.stats.hp = Math.max(0, Math.min(gameState.character.stats.hp, gameState.character.stats.maxHp)); | |
} | |
if (targetPageData.reward?.xp) { | |
gameState.character.stats.xp += targetPageData.reward.xp; | |
messageLog += `<p class="message message-info"><em>Gained ${targetPageData.reward.xp} XP.</em></p>`; | |
} | |
if (targetPageData.reward?.addItem) { | |
const item = targetPageData.reward.addItem; | |
if (itemsData[item] && !gameState.character.inventory.includes(item)) { | |
gameState.character.inventory.push(item); | |
messageLog += `<p class="message message-item"><em>Acquired: ${item}</em></p>`; | |
} | |
} | |
if (gameState.character.stats.hp <= 0) { | |
console.log("Player died!"); | |
nextPageId = 99; | |
messageLog += `<p class="message message-failure"><b>You have succumbed!</b></p>`; | |
renderPageInternal(nextPageId, gameData[nextPageId] || gameData[99], messageLog); | |
return; | |
} | |
gameState.currentPageId = nextPageId; | |
renderPageInternal(nextPageId, gameData[nextPageId], messageLog); | |
} | |
function renderPageInternal(pageId, pageData, message = "") { | |
if (!pageData) { | |
console.error(`Render Error: No data for page ${pageId}!`); | |
pageData = gameData[99]; | |
message += `<p class="message message-failure">Render Error: Page ${pageId} not found!</p>`; | |
pageId = 99; | |
} | |
storyTitleElement.textContent = pageData.title || "Untitled"; | |
storyContentElement.innerHTML = message + (pageData.content || "<p>...</p>"); | |
updateStatsDisplay(); | |
updateInventoryDisplay(); | |
choicesElement.innerHTML = ''; | |
if (pageData.options && pageData.options.length > 0) { | |
pageData.options.forEach(option => { | |
const button = document.createElement('button'); | |
button.classList.add('choice-button'); | |
button.textContent = option.text; | |
let requirementMet = true; | |
let reqText = []; | |
if (option.requireItem && !gameState.character.inventory.includes(option.requireItem)) { | |
requirementMet = false; | |
reqText.push(`Requires: ${option.requireItem}`); | |
} | |
button.disabled = !requirementMet; | |
if (!requirementMet) { | |
button.title = reqText.join(', '); | |
} else { | |
const choiceData = { | |
nextPage: option.next, | |
check: option.check, | |
onFailure: option.onFailure, | |
reward: option.reward, | |
hpLoss: option.hpLoss, | |
requireItem: option.requireItem, | |
consumeItem: option.consumeItem, | |
}; | |
button.onclick = () => handleChoiceClick(choiceData); | |
} | |
choicesElement.appendChild(button); | |
}); | |
} else { | |
const button = document.createElement('button'); | |
button.classList.add('choice-button'); | |
button.textContent = "Restart"; | |
button.onclick = () => handleChoiceClick({ nextPage: 1 }); | |
choicesElement.appendChild(button); | |
} | |
updateScene(pageData.illustration); | |
} | |
function renderPage(pageId) { | |
renderPageInternal(pageId, gameData[pageId]); | |
} | |
function updateStatsDisplay() { | |
const { hp, maxHp, xp } = gameState.character.stats; | |
const hpColor = hp / maxHp < 0.3 ? '#f88' : (hp / maxHp < 0.6 ? '#fd5' : '#8f8'); | |
statsElement.innerHTML = `<strong>Stats:</strong> <span style="color:${hpColor}">HP: ${hp}/${maxHp}</span> <span>XP: ${xp}</span>`; | |
} | |
function updateInventoryDisplay() { | |
let invHtml = '<strong>Inventory:</strong> '; | |
if (gameState.character.inventory.length === 0) { | |
invHtml += '<em>Empty</em>'; | |
} else { | |
gameState.character.inventory.forEach(item => { | |
const itemDef = itemsData[item] || { type: 'unknown', description: '???' }; | |
const itemClass = `item-${itemDef.type || 'unknown'}`; | |
invHtml += `<span class="${itemClass}" title="${itemDef.description}">${item}</span>`; | |
}); | |
} | |
inventoryElement.innerHTML = invHtml; | |
} | |
document.addEventListener('DOMContentLoaded', () => { | |
console.log("DOM Ready - Initializing Adventure Reboot."); | |
try { | |
initThreeJS(); | |
if (!scene || !camera || !renderer) throw new Error("Three.js failed to initialize."); | |
startGame(); | |
console.log("Game started successfully."); | |
} catch (error) { | |
console.error("Initialization failed:", error); | |
storyTitleElement.textContent = "Initialization Error"; | |
storyContentElement.innerHTML = `<p style="color:red;">Failed to start game:</p><pre style="color:red; white-space: pre-wrap;">${error.stack || error}</pre>`; | |
choicesElement.innerHTML = ''; | |
if(sceneContainer) sceneContainer.innerHTML = '<p style="color:red; padding: 20px;">3D Scene Failed</p>'; | |
statsInventoryContainer.style.display = 'none'; | |
choicesContainer.style.display = 'none'; | |
} | |
}); | |
</script> | |
</body> | |
</html> |