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