Spaces:
Running
Running
<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> |