ChooseYourOwnAdventure2 / backup5.index.html
awacke1's picture
Rename index.html to backup5.index.html
1538158 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Wacky Shapes Adventure! (Fixed)</title>
<style>
/* --- Base Styles (Similar to previous, slightly tweaked) --- */
body { font-family: 'Verdana', sans-serif; background-color: #2a3a4a; color: #f0f0f0; 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: 3px solid #506070; min-width: 250px; background-color: #101820; height: 100%; box-sizing: border-box; overflow: hidden; }
#ui-container { flex-grow: 2; padding: 25px; overflow-y: auto; background-color: #3a4a5a; min-width: 320px; height: 100%; box-sizing: border-box; display: flex; flex-direction: column; }
#scene-container canvas { display: block; }
/* --- UI Elements --- */
#story-title { color: #ffd700; /* Gold */ margin: 0 0 15px 0; padding-bottom: 10px; border-bottom: 2px solid #506070; font-size: 1.8em; font-weight: bold; text-shadow: 1px 1px 2px #111; }
#story-content { margin-bottom: 25px; line-height: 1.7; flex-grow: 1; font-size: 1.1em; color: #e8e8e8;}
#stats-inventory-container { margin-bottom: 25px; padding: 15px; border: 1px solid #506070; border-radius: 5px; background-color: #4a5a6a; font-size: 0.95em; }
#stats-display, #inventory-display { margin-bottom: 10px; line-height: 1.8; }
#stats-display span, #inventory-display .item-tag { display: inline-block; background-color: #5a6a7a; padding: 4px 10px; border-radius: 15px; margin: 0 8px 5px 0; border: 1px solid #7a8a9a; white-space: nowrap; box-shadow: inset 0 1px 2px rgba(0,0,0,0.2); }
#stats-display strong, #inventory-display strong { color: #e0e0e0; margin-right: 6px; }
#inventory-display em { color: #aabbcc; font-style: normal; }
.item-tool { background-color: #a0522d; border-color: #cd853f; color: #fff;} /* Sienna/Peru */
.item-key { background-color: #ffd700; border-color: #f0e68c; color: #333;} /* Gold/Khaki */
.item-treasure { background-color: #20b2aa; border-color: #48d1cc; color: #fff;} /* LightSeaGreen/MediumTurquoise */
.item-food { background-color: #ff6347; border-color: #fa8072; color: #fff;} /* Tomato/Salmon */
.item-unknown { background-color: #778899; border-color: #b0c4de;} /* LightSlateGray/LightSteelBlue */
/* --- Choices & Messages --- */
#choices-container { margin-top: auto; padding-top: 20px; border-top: 2px solid #506070; }
#choices-container h3 { margin-top: 0; margin-bottom: 12px; color: #e0e0e0; font-size: 1.2em; font-weight: bold;}
#choices { display: flex; flex-direction: column; gap: 12px; }
.choice-button { display: block; width: 100%; padding: 13px 16px; margin-bottom: 0; background-color: #607080; color: #fff; border: 1px solid #8090a0; border-radius: 5px; cursor: pointer; text-align: left; font-family: 'Verdana', sans-serif; font-size: 1.05em; font-weight: bold; transition: background-color 0.2s, transform 0.1s; box-sizing: border-box; letter-spacing: 0.5px; }
.choice-button:hover:not(:disabled) { background-color: #ffc107; color: #222; border-color: #ffca2c; transform: translateY(-1px); }
.choice-button:disabled { background-color: #4e5a66; color: #8a9aab; cursor: not-allowed; border-color: #607080; opacity: 0.7; }
.message { padding: 8px 12px; margin: 10px 0; border-left-width: 4px; border-left-style: solid; font-size: 1em; background-color: rgba(0, 0, 0, 0.1); border-radius: 3px; }
.message-info { color: #ccc; border-left-color: #666; }
.message-item { color: #9cf; border-left-color: #48a; }
.message-xp { color: #af8; border-left-color: #6a4; }
.message-warning { color: #f99; border-left-color: #c66; }
/* --- Action Info --- */
#action-info { position: absolute; bottom: 10px; left: 10px; background-color: rgba(0,0,0,0.7); color: #ffcc66; padding: 5px 10px; border-radius: 3px; font-size: 0.9em; display: block; z-index: 10;} /* Keep visible */
</style>
</head>
<body>
<div id="game-container">
<div id="scene-container">
<div id="action-info">Initializing...</div>
</div>
<div id="ui-container">
<h2 id="story-title">Initializing...</h2>
<div id="story-content"><p>Loading assets...</p></div>
<div id="stats-inventory-container">
<div id="stats-display">Loading Stats...</div>
<div id="inventory-display">Inventory: ...</div>
</div>
<div id="choices-container">
<h3>What will you do?</h3>
<div id="choices">Loading...</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';
console.log("Script module execution started.");
// --- 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');
const actionInfoElement = document.getElementById('action-info'); // <<< Added back the declaration
console.log("DOM elements obtained.");
// --- Core Three.js Variables ---
let scene, camera, renderer, clock;
let currentSceneGroup = null;
let currentLights = [];
let currentMessage = "";
// --- Materials ---
const MAT = {
stone_grey: new THREE.MeshStandardMaterial({ color: 0x8a8a8a, roughness: 0.8 }),
stone_brown: new THREE.MeshStandardMaterial({ color: 0x9d8468, roughness: 0.85 }),
wood_light: new THREE.MeshStandardMaterial({ color: 0xcdaa7d, roughness: 0.7 }),
wood_dark: new THREE.MeshStandardMaterial({ color: 0x6f4e2d, roughness: 0.75 }),
leaf_green: new THREE.MeshStandardMaterial({ color: 0x4caf50, roughness: 0.6, side: THREE.DoubleSide }),
leaf_autumn: new THREE.MeshStandardMaterial({ color: 0xffa040, roughness: 0.65, side: THREE.DoubleSide }),
ground_dirt: new THREE.MeshStandardMaterial({ color: 0x8b5e3c, roughness: 0.9 }),
ground_grass: new THREE.MeshStandardMaterial({ color: 0x558b2f, roughness: 0.85 }),
metal_shiny: new THREE.MeshStandardMaterial({ color: 0xc0c0c0, roughness: 0.2, metalness: 0.8 }),
metal_rusty: new THREE.MeshStandardMaterial({ color: 0xb7410e, roughness: 0.9, metalness: 0.2 }),
crystal_blue: new THREE.MeshStandardMaterial({ color: 0x87cefa, roughness: 0.1, metalness: 0.1, transparent: true, opacity: 0.8, emissive:0x205080, emissiveIntensity: 0.4 }),
crystal_red: new THREE.MeshStandardMaterial({ color: 0xff7f7f, roughness: 0.1, metalness: 0.1, transparent: true, opacity: 0.8, emissive:0x802020, emissiveIntensity: 0.4 }),
bright_yellow: new THREE.MeshStandardMaterial({ color: 0xffeb3b, roughness: 0.6 }),
deep_purple: new THREE.MeshStandardMaterial({ color: 0x9c27b0, roughness: 0.7 }),
sky_blue: new THREE.MeshStandardMaterial({ color: 0x03a9f4, roughness: 0.5 }),
};
// --- Game State ---
let gameState = {};
// --- Item Data ---
const itemsData = {
"Wobbly Key": {type:"key", description:"Looks like it fits a jiggly lock."},
"Shiny Sprocket": {type:"treasure", description:"A gear that gleams."},
"Bouncy Mushroom": {type:"food", description:"Seems edible... maybe?"},
"Sturdy Stick": {type:"tool", description:"Good for poking things."},
"Cave Crystal": {type:"unknown", description:"A faintly glowing crystal shard."} // Added from previous example
};
// --- Procedural Assembly Shapes ---
const SHAPE_GENERATORS = {
'pointy_cone': (size) => new THREE.ConeGeometry(size * 0.5, size * 1.2, 6 + Math.floor(Math.random() * 5)),
'round_blob': (size) => new THREE.SphereGeometry(size * 0.6, 12, 8),
'spinny_torus': (size) => new THREE.TorusGeometry(size * 0.5, size * 0.15, 8, 16),
'boxy_chunk': (size) => new THREE.BoxGeometry(size, size * (0.8 + Math.random() * 0.4), size * (0.8 + Math.random() * 0.4)),
'spiky_ball': (size) => new THREE.IcosahedronGeometry(size * 0.7, 0),
'flat_plate': (size) => new THREE.BoxGeometry(size * 1.5, size * 0.1, size * 1.5),
'tall_cylinder': (size) => new THREE.CylinderGeometry(size * 0.3, size * 0.3, size * 2.0, 8),
'knotty_thing': (size) => new THREE.TorusKnotGeometry(size * 0.4, size * 0.1, 50, 8),
'gem_shape': (size) => new THREE.OctahedronGeometry(size * 0.6, 0),
'basic_tetra': (size) => new THREE.TetrahedronGeometry(size * 0.7, 0),
'squashed_ball': (size) => new THREE.SphereGeometry(size * 0.6, 12, 8).scale(1, 0.6, 1),
'holed_box': (size) => {
const shape = new THREE.Shape();
shape.moveTo(-size/2, -size/2); shape.lineTo(-size/2, size/2); shape.lineTo(size/2, size/2); shape.lineTo(size/2, -size/2); shape.closePath();
const hole = new THREE.Path();
hole.absellipse(0, 0, size*0.2, size*0.2, 0, Math.PI*2, false);
shape.holes.push(hole);
return new THREE.ExtrudeGeometry(shape, {depth: size*0.3, bevelEnabled: false});
}
};
const shapeKeys = Object.keys(SHAPE_GENERATORS);
// --- Game Data (Page-Based) ---
const gameData = {
1: {
title: "The Giggling Grove",
content: "<p>You stand at the edge of a forest filled with wobbly, colorful trees. A path leads deeper in. What strange shapes will you find?</p>",
options: [ { text: "Follow the Wobbly Path", next: 2 }, { text: "Look for Shiny Things", next: 3 } ],
assemblyParams: {
baseShape: 'ground_grass', baseSize: 30,
mainShapes: ['tall_cylinder', 'squashed_ball'],
accents: ['pointy_cone'],
count: 25, scaleRange: [0.8, 2.5],
colorTheme: [MAT.wood_light, MAT.leaf_green, MAT.bright_yellow],
arrangement: 'scatter'
}
},
2: {
title: "Deeper in the Grove",
content: "<p>The trees here have funny faces carved into them! One seems to be hiding something behind it.</p>",
options: [ { text: "Peek behind the tree", next: 4 }, { text: "Keep going wobble-ward", next: 5 }, { text: "Head back", next: 1 } ],
assemblyParams: {
baseShape: 'ground_dirt', baseSize: 25,
mainShapes: ['tall_cylinder', 'basic_tetra', 'round_blob'],
accents: ['spiky_ball'],
count: 20, scaleRange: [1.0, 3.0],
colorTheme: [MAT.wood_dark, MAT.leaf_autumn, MAT.deep_purple],
arrangement: 'cluster'
}
},
3: {
title: "Shiny Spot",
content: "<p>Ooh, sparkly! You found a patch where little gem-like shapes are growing out of the ground.</p>",
options: [ { text: "Try to pick one", next: 6 }, { text: "Go back to the path", next: 1 } ],
assemblyParams: {
baseShape: 'ground_grass', baseSize: 15,
mainShapes: ['gem_shape', 'basic_tetra'],
accents: ['pointy_cone'],
count: 30, scaleRange: [0.2, 0.7],
colorTheme: [MAT.crystal_blue, MAT.crystal_red, MAT.stone_grey],
arrangement: 'patch', patchPos: {x:0, y:0.1, z:0}, patchRadius: 5
}
},
4: {
title: "Tree's Secret",
content: "<p>Aha! Tucked behind the tree trunk, you found a Sturdy Stick!</p>",
options: [ { text: "Awesome! Go back.", next: 2 } ],
reward: { addItem: "Sturdy Stick", xp: 5 }, // Add reward
assemblyParams: { // Same as page 2
baseShape: 'ground_dirt', baseSize: 25,
mainShapes: ['tall_cylinder', 'basic_tetra', 'round_blob'],
accents: ['spiky_ball'],
count: 20, scaleRange: [1.0, 3.0],
colorTheme: [MAT.wood_dark, MAT.leaf_autumn, MAT.deep_purple],
arrangement: 'cluster'
}
},
5: {
title: "The Wobble End",
content: "<p>The path ends at a tall tower made of wobbly, stacked blocks! It looks climbable, but tricky.</p>",
options: [ { text: "Try to climb (TBC)", next: 99 }, { text: "Go back", next: 2 } ],
assemblyParams: {
baseShape: 'ground_dirt', baseSize: 20,
mainShapes: ['boxy_chunk', 'holed_box'],
accents: ['spinny_torus'],
count: 15, scaleRange: [1.5, 2.5],
colorTheme: [MAT.stone_brown, MAT.stone_grey, MAT.metal_rusty],
arrangement: 'stack', stackHeight: 8
}
},
6: {
title: "Picked a Gem!",
content: "<p>Success! You plucked a pretty crystal from the ground. It feels warm.</p>",
options: [ { text: "Cool! Go back.", next: 1 } ],
reward: { addItem: "Cave Crystal", xp: 10 }, // Add reward
assemblyParams: { // Same as page 3
baseShape: 'ground_grass', baseSize: 15,
mainShapes: ['gem_shape', 'basic_tetra'],
accents: ['pointy_cone'],
count: 30, scaleRange: [0.2, 0.7],
colorTheme: [MAT.crystal_blue, MAT.crystal_red, MAT.stone_grey],
arrangement: 'patch', patchPos: {x:0, y:0.1, z:0}, patchRadius: 5
}
},
99: {
title: "Adventure Paused!",
content: "<p>Wow, what an adventure! That's all for now, but maybe more fun awaits another day?</p>",
options: [ { text: "Start Over?", next: 1 } ],
assemblyParams: {
baseShape: 'flat_plate', baseSize: 10,
mainShapes: ['basic_tetra'],
accents: [],
count: 5, scaleRange: [1, 2],
colorTheme: [MAT.sky_blue, MAT.bright_yellow],
arrangement: 'center_stack', stackHeight: 3
}
}
};
// --- Core Functions ---
function initThreeJS() {
console.log("initThreeJS started.");
scene = new THREE.Scene();
scene.background = new THREE.Color(0x2a3a4a);
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, 5, 10);
camera.lookAt(0, 1, 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, 100);
animate();
console.log("initThreeJS finished.");
}
function onWindowResize() {
if (!renderer || !camera || !sceneContainer) return;
const width = sceneContainer.clientWidth || 1;
const height = sceneContainer.clientHeight || 1;
if (width > 0 && height > 0) {
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.setSize(width, height);
}
}
function animate() {
requestAnimationFrame(animate);
const delta = clock.getDelta();
const time = clock.getElapsedTime();
if (currentSceneGroup) {
currentSceneGroup.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 setupLighting(type = 'default') {
currentLights.forEach(light => { if (light.parent) light.parent.remove(light); if(scene.children.includes(light)) scene.remove(light); });
currentLights = [];
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambientLight);
currentLights.push(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0);
directionalLight.position.set(8, 15, 10);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.set(1024, 1024);
directionalLight.shadow.camera.near = 0.5; directionalLight.shadow.camera.far = 50;
const sb = 20;
directionalLight.shadow.camera.left = -sb; directionalLight.shadow.camera.right = sb;
directionalLight.shadow.camera.top = sb; directionalLight.shadow.camera.bottom = -sb;
directionalLight.shadow.bias = -0.0005;
scene.add(directionalLight);
currentLights.push(directionalLight);
}
function createProceduralAssembly(params) {
console.log("Creating procedural assembly with params:", params);
const group = new THREE.Group();
const {
baseShape = 'ground_grass', baseSize = 20,
mainShapes = ['boxy_chunk'], accents = ['pointy_cone'],
count = 10, scaleRange = [0.5, 1.5],
colorTheme = [MAT.stone_grey, MAT.wood_light, MAT.leaf_green],
arrangement = 'scatter',
stackHeight = 5,
clusterRadius = 5,
patchPos = {x:0, y:0, z:0}, patchRadius = 3
} = params;
let baseMesh;
if (baseShape.startsWith('ground_')) {
const groundMat = MAT[baseShape] || MAT.ground_grass;
const groundGeo = new THREE.PlaneGeometry(baseSize, baseSize);
baseMesh = new THREE.Mesh(groundGeo, groundMat);
baseMesh.rotation.x = -Math.PI / 2; baseMesh.position.y = 0;
baseMesh.receiveShadow = true; baseMesh.castShadow = false;
group.add(baseMesh);
} else {
const baseGeoFunc = SHAPE_GENERATORS[baseShape] || SHAPE_GENERATORS['flat_plate'];
const baseGeo = baseGeoFunc(baseSize);
baseMesh = createMesh(baseGeo, colorTheme[0] || MAT.stone_grey, {y:0.1});
baseMesh.receiveShadow = true; baseMesh.castShadow = false;
group.add(baseMesh);
}
const allShapes = [...mainShapes, ...accents];
let lastY = 0; // For stacking
let stackCount = 0; // Track items in current stack
for (let i = 0; i < count; i++) {
if(allShapes.length === 0) break; // Avoid errors if no shapes defined
const shapeKey = allShapes[Math.floor(Math.random() * allShapes.length)];
const geoFunc = SHAPE_GENERATORS[shapeKey];
if (!geoFunc) { console.warn(`Shape generator not found for key: ${shapeKey}`); continue; }
const scaleFactor = scaleRange[0] + Math.random() * (scaleRange[1] - scaleRange[0]);
const geometry = geoFunc(scaleFactor);
const material = colorTheme[Math.floor(Math.random() * colorTheme.length)];
const mesh = createMesh(geometry, material);
geometry.computeBoundingBox();
const height = (geometry.boundingBox.max.y - geometry.boundingBox.min.y) * mesh.scale.y;
let position = { x:0, y:0, z:0 };
switch(arrangement) {
case 'stack':
if (stackCount < stackHeight) {
position.y = lastY + height / 2;
position.x = (Math.random() - 0.5) * 0.5 * scaleFactor;
position.z = (Math.random() - 0.5) * 0.5 * scaleFactor;
lastY += height * 0.9;
stackCount++;
} else continue; // Skip if stack is full
break;
case 'center_stack':
if (stackCount < stackHeight) {
position.y = lastY + height / 2;
lastY += height * 0.95;
stackCount++;
} else continue;
break;
case 'cluster':
const angle = Math.random() * Math.PI * 2;
const radius = Math.random() * clusterRadius;
position.x = Math.cos(angle) * radius;
position.z = Math.sin(angle) * radius;
position.y = height / 2;
break;
case 'patch':
const pAngle = Math.random() * Math.PI * 2;
const pRadius = Math.random() * patchRadius;
position.x = patchPos.x + Math.cos(pAngle) * pRadius;
position.z = patchPos.z + Math.sin(pAngle) * pRadius;
position.y = (patchPos.y || 0) + height / 2;
break;
case 'scatter':
default:
position.x = (Math.random() - 0.5) * baseSize * 0.8;
position.z = (Math.random() - 0.5) * baseSize * 0.8;
position.y = height / 2;
break;
}
mesh.position.set(position.x, position.y, position.z);
mesh.rotation.set(
Math.random() * (arrangement.includes('stack') ? 0.2 : Math.PI), // Less x/z rotation if stacked
Math.random() * Math.PI * 2,
Math.random() * (arrangement.includes('stack') ? 0.2 : Math.PI)
);
if (Math.random() < 0.3) {
mesh.userData.rotSpeed = (Math.random() - 0.5) * 0.4;
mesh.userData.bobSpeed = 1 + Math.random();
mesh.userData.bobAmount = 0.05 + Math.random() * 0.05;
mesh.userData.startY = mesh.position.y;
mesh.userData.update = (time, delta) => {
mesh.rotation.y += mesh.userData.rotSpeed * delta;
mesh.position.y = mesh.userData.startY + Math.sin(time * mesh.userData.bobSpeed) * mesh.userData.bobAmount;
};
}
group.add(mesh);
}
console.log(`Assembly created with ${group.children.length} objects.`);
return group;
}
function updateScene(assemblyParams) {
console.log("updateScene called with params:", assemblyParams);
if (currentSceneGroup) {
currentSceneGroup.traverse(child => {
if (child.isMesh) {
if(child.geometry) child.geometry.dispose();
}
});
scene.remove(currentSceneGroup);
currentSceneGroup = null;
}
setupLighting('default'); // Reset lighting
if (!assemblyParams) {
console.warn("No assemblyParams provided, creating default scene.");
assemblyParams = { baseShape: 'ground_dirt', count: 5, mainShapes: ['boxy_chunk'] };
}
try {
currentSceneGroup = createProceduralAssembly(assemblyParams);
scene.add(currentSceneGroup);
console.log("New scene group added.");
} catch (error) {
console.error("Error creating procedural assembly:", error);
}
}
function startGame() {
console.log("startGame called.");
const defaultChar = {
name: "Player",
stats: { hp: 20, maxHp: 20, xp: 0, strength: 10, dexterity: 10, constitution: 10, intelligence: 10, wisdom: 10, charisma: 10 },
inventory: ["Sturdy Stick"]
};
gameState = {
currentPageId: 1,
character: JSON.parse(JSON.stringify(defaultChar))
};
renderPage(gameState.currentPageId);
console.log("startGame finished.");
}
function handleChoiceClick(choiceData) {
console.log("Choice clicked:", choiceData);
currentMessage = "";
let nextPageId = parseInt(choiceData.next);
const currentPageData = gameData[gameState.currentPageId];
const targetPageData = gameData[nextPageId];
if (!targetPageData) {
console.error(`Invalid next page ID: ${nextPageId}`);
currentMessage = `<p class="message message-warning">Oops! That path seems to lead nowhere yet.</p>`;
nextPageId = gameState.currentPageId;
}
if (currentPageData && currentPageData.options) {
const chosenOption = currentPageData.options.find(opt => opt.next === choiceData.next);
if (chosenOption && chosenOption.reward) {
console.log("Applying reward:", chosenOption.reward);
if(chosenOption.reward.addItem && itemsData[chosenOption.reward.addItem]) {
if (!gameState.character.inventory.includes(chosenOption.reward.addItem)) {
gameState.character.inventory.push(chosenOption.reward.addItem);
currentMessage += `<p class="message message-item">You found a ${chosenOption.reward.addItem}!</p>`;
} else {
currentMessage += `<p class="message message-info">You found another ${chosenOption.reward.addItem}, but your pockets are full!</p>`;
}
}
if(chosenOption.reward.xp) {
gameState.character.stats.xp += chosenOption.reward.xp;
currentMessage += `<p class="message message-xp">You gained ${chosenOption.reward.xp} XP!</p>`;
}
}
}
gameState.currentPageId = nextPageId;
renderPage(nextPageId);
}
function renderPage(pageId) {
console.log(`Rendering page ${pageId}`);
const pageData = gameData[pageId];
if (!pageData) {
console.error(`No page data for ID: ${pageId}`);
storyTitleElement.textContent = "Uh Oh!";
storyContentElement.innerHTML = currentMessage + `<p>Where did the world go? Maybe try going back?</p>`;
choicesElement.innerHTML = `<button class="choice-button" onclick="window.handleChoiceClick({ next: 1 })">Go Back to Start</button>`;
updateStatsDisplay(); updateInventoryDisplay(); updateActionInfo();
updateScene({ baseShape: 'ground_dirt', count: 1, mainShapes: ['basic_tetra'], colorTheme: [MAT.metal_rusty] });
return;
}
storyTitleElement.textContent = pageData.title || "An Unnamed Place";
storyContentElement.innerHTML = currentMessage + (pageData.content || "<p>It's... a place. Things exist here.</p>");
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;
button.onclick = () => handleChoiceClick(option);
choicesElement.appendChild(button);
});
} else {
const button = document.createElement('button');
button.classList.add('choice-button');
button.textContent = "The End (Start Over?)";
button.onclick = () => handleChoiceClick({ next: 1 });
choicesElement.appendChild(button);
}
updateStatsDisplay();
updateInventoryDisplay();
updateActionInfo();
updateScene(pageData.assemblyParams);
}
window.handleChoiceClick = handleChoiceClick;
function updateStatsDisplay() {
if (!gameState.character || !statsElement) return;
const stats = gameState.character.stats;
const hpColor = stats.hp / stats.maxHp < 0.3 ? '#f88' : (stats.hp / stats.maxHp < 0.6 ? '#fd5' : '#8f8');
statsElement.innerHTML = `<strong>Stats:</strong>
<span style="color:${hpColor}">HP: ${stats.hp}/${stats.maxHp}</span> <span>XP: ${stats.xp}</span><br>
<span>Str: ${stats.strength}</span> <span>Dex: ${stats.dexterity}</span> <span>Con: ${stats.constitution}</span>
<span>Int: ${stats.intelligence}</span> <span>Wis: ${stats.wisdom}</span> <span>Cha: ${stats.charisma}</span>`;
}
function updateInventoryDisplay() {
if (!gameState.character || !inventoryElement) return;
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="item-tag ${itemClass}" title="${itemDef.description}">${item}</span>`;
});
}
inventoryElement.innerHTML = invHtml;
}
function updateActionInfo() {
if (!actionInfoElement || !gameState ) return;
actionInfoElement.textContent = `Location: ${gameData[gameState.currentPageId]?.title || 'Unknown'}`;
}
document.addEventListener('DOMContentLoaded', () => {
console.log("DOM Ready - Initializing Wacky Shapes Adventure!");
try {
initThreeJS();
if (!scene || !camera || !renderer) throw new Error("Three.js failed to initialize.");
startGame();
console.log("Game world initialized and started.");
} 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><p style="color:yellow;">Check console (F12) for details.</p>`;
if(sceneContainer) sceneContainer.innerHTML = '<p style="color:red; padding: 20px;">3D Scene Failed</p>';
const statsInvContainer = document.getElementById('stats-inventory-container');
const choicesCont = document.getElementById('choices-container');
if (statsInvContainer) statsInvContainer.style.display = 'none';
if (choicesCont) choicesCont.style.display = 'none';
}
});
</script>
</body>
</html>