awacke1 commited on
Commit
e172c2b
·
verified ·
1 Parent(s): 210ada3

Create index.html

Browse files
Files changed (1) hide show
  1. index.html +608 -0
index.html ADDED
@@ -0,0 +1,608 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Wacky Shapes Adventure!</title>
7
+ <style>
8
+ /* --- Base Styles (Similar to previous, slightly tweaked) --- */
9
+ body { font-family: 'Verdana', sans-serif; background-color: #2a3a4a; color: #f0f0f0; margin: 0; padding: 0; overflow: hidden; display: flex; flex-direction: column; height: 100vh; }
10
+ #game-container { display: flex; flex-grow: 1; overflow: hidden; }
11
+ #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; }
12
+ #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; }
13
+ #scene-container canvas { display: block; }
14
+
15
+ /* --- UI Elements --- */
16
+ #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; }
17
+ #story-content { margin-bottom: 25px; line-height: 1.7; flex-grow: 1; font-size: 1.1em; color: #e8e8e8;}
18
+ #stats-inventory-container { margin-bottom: 25px; padding: 15px; border: 1px solid #506070; border-radius: 5px; background-color: #4a5a6a; font-size: 0.95em; }
19
+ #stats-display, #inventory-display { margin-bottom: 10px; line-height: 1.8; }
20
+ #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); }
21
+ #stats-display strong, #inventory-display strong { color: #e0e0e0; margin-right: 6px; }
22
+ #inventory-display em { color: #aabbcc; font-style: normal; }
23
+ .item-tool { background-color: #a0522d; border-color: #cd853f; color: #fff;} /* Sienna/Peru */
24
+ .item-key { background-color: #ffd700; border-color: #f0e68c; color: #333;} /* Gold/Khaki */
25
+ .item-treasure { background-color: #20b2aa; border-color: #48d1cc; color: #fff;} /* LightSeaGreen/MediumTurquoise */
26
+ .item-food { background-color: #ff6347; border-color: #fa8072; color: #fff;} /* Tomato/Salmon */
27
+ .item-unknown { background-color: #778899; border-color: #b0c4de;} /* LightSlateGray/LightSteelBlue */
28
+
29
+ /* --- Choices & Messages --- */
30
+ #choices-container { margin-top: auto; padding-top: 20px; border-top: 2px solid #506070; }
31
+ #choices-container h3 { margin-top: 0; margin-bottom: 12px; color: #e0e0e0; font-size: 1.2em; font-weight: bold;}
32
+ #choices { display: flex; flex-direction: column; gap: 12px; }
33
+ .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; }
34
+ .choice-button:hover:not(:disabled) { background-color: #ffc107; color: #222; border-color: #ffca2c; transform: translateY(-1px); }
35
+ .choice-button:disabled { background-color: #4e5a66; color: #8a9aab; cursor: not-allowed; border-color: #607080; opacity: 0.7; }
36
+ .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; }
37
+ .message-info { color: #ccc; border-left-color: #666; }
38
+ .message-item { color: #9cf; border-left-color: #48a; }
39
+ .message-xp { color: #af8; border-left-color: #6a4; }
40
+ .message-warning { color: #f99; border-left-color: #c66; }
41
+
42
+ </style>
43
+ </head>
44
+ <body>
45
+ <div id="game-container">
46
+ <div id="scene-container"></div>
47
+ <div id="ui-container">
48
+ <h2 id="story-title">Adventure Awaits!</h2>
49
+ <div id="story-content"><p>Getting ready...</p></div>
50
+ <div id="stats-inventory-container">
51
+ <div id="stats-display"></div>
52
+ <div id="inventory-display"></div>
53
+ </div>
54
+ <div id="choices-container">
55
+ <h3>What adventure will you choose?</h3>
56
+ <div id="choices"></div>
57
+ </div>
58
+ </div>
59
+ </div>
60
+
61
+ <script type="importmap">
62
+ { "imports": {
63
+ "three": "https://unpkg.com/[email protected]/build/three.module.js",
64
+ "three/addons/": "https://unpkg.com/[email protected]/examples/jsm/"
65
+ }}
66
+ </script>
67
+
68
+ <script type="module">
69
+ import * as THREE from 'three';
70
+
71
+ console.log("Script module execution started.");
72
+
73
+ const sceneContainer = document.getElementById('scene-container');
74
+ const storyTitleElement = document.getElementById('story-title');
75
+ const storyContentElement = document.getElementById('story-content');
76
+ const choicesElement = document.getElementById('choices');
77
+ const statsElement = document.getElementById('stats-display');
78
+ const inventoryElement = document.getElementById('inventory-display');
79
+
80
+ let scene, camera, renderer, clock;
81
+ let currentSceneGroup = null; // Holds the group for the current scene's objects
82
+ let currentLights = [];
83
+ let currentMessage = ""; // Accumulates messages for UI update
84
+
85
+ const MAT = { // Material library
86
+ stone_grey: new THREE.MeshStandardMaterial({ color: 0x8a8a8a, roughness: 0.8 }),
87
+ stone_brown: new THREE.MeshStandardMaterial({ color: 0x9d8468, roughness: 0.85 }),
88
+ wood_light: new THREE.MeshStandardMaterial({ color: 0xcdaa7d, roughness: 0.7 }),
89
+ wood_dark: new THREE.MeshStandardMaterial({ color: 0x6f4e2d, roughness: 0.75 }),
90
+ leaf_green: new THREE.MeshStandardMaterial({ color: 0x4caf50, roughness: 0.6, side: THREE.DoubleSide }),
91
+ leaf_autumn: new THREE.MeshStandardMaterial({ color: 0xffa040, roughness: 0.65, side: THREE.DoubleSide }),
92
+ ground_dirt: new THREE.MeshStandardMaterial({ color: 0x8b5e3c, roughness: 0.9 }),
93
+ ground_grass: new THREE.MeshStandardMaterial({ color: 0x558b2f, roughness: 0.85 }),
94
+ metal_shiny: new THREE.MeshStandardMaterial({ color: 0xc0c0c0, roughness: 0.2, metalness: 0.8 }),
95
+ metal_rusty: new THREE.MeshStandardMaterial({ color: 0xb7410e, roughness: 0.9, metalness: 0.2 }),
96
+ crystal_blue: new THREE.MeshStandardMaterial({ color: 0x87cefa, roughness: 0.1, metalness: 0.1, transparent: true, opacity: 0.8, emissive:0x205080, emissiveIntensity: 0.4 }),
97
+ crystal_red: new THREE.MeshStandardMaterial({ color: 0xff7f7f, roughness: 0.1, metalness: 0.1, transparent: true, opacity: 0.8, emissive:0x802020, emissiveIntensity: 0.4 }),
98
+ bright_yellow: new THREE.MeshStandardMaterial({ color: 0xffeb3b, roughness: 0.6 }),
99
+ deep_purple: new THREE.MeshStandardMaterial({ color: 0x9c27b0, roughness: 0.7 }),
100
+ sky_blue: new THREE.MeshStandardMaterial({ color: 0x03a9f4, roughness: 0.5 }),
101
+ };
102
+
103
+ // --- Game State ---
104
+ let gameState = {}; // Initialized in startGame
105
+
106
+ // --- Item Data ---
107
+ const itemsData = {
108
+ "Wobbly Key": {type:"key", description:"Looks like it fits a jiggly lock."},
109
+ "Shiny Sprocket": {type:"treasure", description:"A gear that gleams."},
110
+ "Bouncy Mushroom": {type:"food", description:"Seems edible... maybe?"},
111
+ "Sturdy Stick": {type:"tool", description:"Good for poking things."},
112
+ };
113
+
114
+ // --- Procedural Assembly Shapes ---
115
+ const SHAPE_GENERATORS = {
116
+ 'pointy_cone': (size) => new THREE.ConeGeometry(size * 0.5, size * 1.2, 6 + Math.floor(Math.random() * 5)),
117
+ 'round_blob': (size) => new THREE.SphereGeometry(size * 0.6, 12, 8),
118
+ 'spinny_torus': (size) => new THREE.TorusGeometry(size * 0.5, size * 0.15, 8, 16),
119
+ 'boxy_chunk': (size) => new THREE.BoxGeometry(size, size * (0.8 + Math.random() * 0.4), size * (0.8 + Math.random() * 0.4)),
120
+ 'spiky_ball': (size) => new THREE.IcosahedronGeometry(size * 0.7, 0),
121
+ 'flat_plate': (size) => new THREE.BoxGeometry(size * 1.5, size * 0.1, size * 1.5),
122
+ 'tall_cylinder': (size) => new THREE.CylinderGeometry(size * 0.3, size * 0.3, size * 2.0, 8),
123
+ 'knotty_thing': (size) => new THREE.TorusKnotGeometry(size * 0.4, size * 0.1, 50, 8),
124
+ 'gem_shape': (size) => new THREE.OctahedronGeometry(size * 0.6, 0),
125
+ 'basic_tetra': (size) => new THREE.TetrahedronGeometry(size * 0.7, 0),
126
+ 'squashed_ball': (size) => new THREE.SphereGeometry(size * 0.6, 12, 8).scale(1, 0.6, 1),
127
+ 'holed_box': (size) => { // Example of more complex primitive combo
128
+ const shape = new THREE.Shape();
129
+ 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();
130
+ const hole = new THREE.Path();
131
+ hole.absellipse(0, 0, size*0.2, size*0.2, 0, Math.PI*2, false);
132
+ shape.holes.push(hole);
133
+ return new THREE.ExtrudeGeometry(shape, {depth: size*0.3, bevelEnabled: false});
134
+ }
135
+ };
136
+ const shapeKeys = Object.keys(SHAPE_GENERATORS);
137
+
138
+ // --- Game Data (Page-Based) ---
139
+ const gameData = {
140
+ 1: {
141
+ title: "The Giggling Grove",
142
+ 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>",
143
+ options: [ { text: "Follow the Wobbly Path", next: 2 }, { text: "Look for Shiny Things", next: 3 } ],
144
+ assemblyParams: {
145
+ baseShape: 'ground_grass', baseSize: 30,
146
+ mainShapes: ['tall_cylinder', 'squashed_ball'],
147
+ accents: ['pointy_cone'],
148
+ count: 25, scaleRange: [0.8, 2.5],
149
+ colorTheme: [MAT.wood_light, MAT.leaf_green, MAT.bright_yellow],
150
+ arrangement: 'scatter'
151
+ }
152
+ },
153
+ 2: {
154
+ title: "Deeper in the Grove",
155
+ content: "<p>The trees here have funny faces carved into them! One seems to be hiding something behind it.</p>",
156
+ options: [ { text: "Peek behind the tree", next: 4 }, { text: "Keep going wobble-ward", next: 5 }, { text: "Head back", next: 1 } ],
157
+ assemblyParams: {
158
+ baseShape: 'ground_dirt', baseSize: 25,
159
+ mainShapes: ['tall_cylinder', 'basic_tetra', 'round_blob'],
160
+ accents: ['spiky_ball'],
161
+ count: 20, scaleRange: [1.0, 3.0],
162
+ colorTheme: [MAT.wood_dark, MAT.leaf_autumn, MAT.deep_purple],
163
+ arrangement: 'cluster'
164
+ }
165
+ },
166
+ 3: {
167
+ title: "Shiny Spot",
168
+ content: "<p>Ooh, sparkly! You found a patch where little gem-like shapes are growing out of the ground.</p>",
169
+ options: [ { text: "Try to pick one", next: 6 }, { text: "Go back to the path", next: 1 } ],
170
+ assemblyParams: {
171
+ baseShape: 'ground_grass', baseSize: 15,
172
+ mainShapes: ['gem_shape', 'basic_tetra'],
173
+ accents: ['pointy_cone'],
174
+ count: 30, scaleRange: [0.2, 0.7],
175
+ colorTheme: [MAT.crystal_blue, MAT.crystal_red, MAT.stone_grey],
176
+ arrangement: 'patch', patchPos: {x:0, y:0.1, z:0}, patchRadius: 5
177
+ }
178
+ },
179
+ 4: {
180
+ title: "Tree's Secret",
181
+ content: "<p>Aha! Tucked behind the tree trunk, you found a Sturdy Stick!</p>",
182
+ options: [ { text: "Awesome! Go back.", next: 2 } ],
183
+ reward: { addItem: "Sturdy Stick" },
184
+ assemblyParams: { // Same as page 2, maybe slightly zoomed?
185
+ baseShape: 'ground_dirt', baseSize: 25,
186
+ mainShapes: ['tall_cylinder', 'basic_tetra', 'round_blob'],
187
+ accents: ['spiky_ball'],
188
+ count: 20, scaleRange: [1.0, 3.0],
189
+ colorTheme: [MAT.wood_dark, MAT.leaf_autumn, MAT.deep_purple],
190
+ arrangement: 'cluster'
191
+ }
192
+ },
193
+ 5: {
194
+ title: "The Wobble End",
195
+ content: "<p>The path ends at a tall tower made of wobbly, stacked blocks! It looks climbable, but tricky.</p>",
196
+ options: [ { text: "Try to climb (TBC)", next: 99 }, { text: "Go back", next: 2 } ],
197
+ assemblyParams: {
198
+ baseShape: 'ground_dirt', baseSize: 20,
199
+ mainShapes: ['boxy_chunk', 'holed_box'],
200
+ accents: ['spinny_torus'],
201
+ count: 15, scaleRange: [1.5, 2.5],
202
+ colorTheme: [MAT.stone_brown, MAT.stone_grey, MAT.metal_rusty],
203
+ arrangement: 'stack', stackHeight: 8
204
+ }
205
+ },
206
+ 6: {
207
+ title: "Picked a Gem!",
208
+ content: "<p>Success! You plucked a pretty crystal from the ground. It feels warm.</p>",
209
+ options: [ { text: "Cool! Go back.", next: 1 } ],
210
+ reward: { addItem: "Cave Crystal" }, // Re-use item name
211
+ assemblyParams: { // Same as page 3
212
+ baseShape: 'ground_grass', baseSize: 15,
213
+ mainShapes: ['gem_shape', 'basic_tetra'],
214
+ accents: ['pointy_cone'],
215
+ count: 30, scaleRange: [0.2, 0.7],
216
+ colorTheme: [MAT.crystal_blue, MAT.crystal_red, MAT.stone_grey],
217
+ arrangement: 'patch', patchPos: {x:0, y:0.1, z:0}, patchRadius: 5
218
+ }
219
+ },
220
+ 99: {
221
+ title: "Adventure Paused!",
222
+ content: "<p>Wow, what an adventure! That's all for now, but maybe more fun awaits another day?</p>",
223
+ options: [ { text: "Start Over?", next: 1 } ],
224
+ assemblyParams: { // Simple ending scene
225
+ baseShape: 'flat_plate', baseSize: 10,
226
+ mainShapes: ['basic_tetra'],
227
+ accents: [],
228
+ count: 5, scaleRange: [1, 2],
229
+ colorTheme: [MAT.sky_blue, MAT.bright_yellow],
230
+ arrangement: 'center_stack', stackHeight: 3
231
+ }
232
+ }
233
+ };
234
+
235
+ // --- Core Functions ---
236
+
237
+ function initThreeJS() {
238
+ console.log("initThreeJS started.");
239
+ scene = new THREE.Scene();
240
+ scene.background = new THREE.Color(0x2a3a4a); // Match body background
241
+ clock = new THREE.Clock();
242
+
243
+ const width = sceneContainer.clientWidth || 1;
244
+ const height = sceneContainer.clientHeight || 1;
245
+ camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 1000);
246
+ camera.position.set(0, 5, 10);
247
+ camera.lookAt(0, 1, 0);
248
+
249
+ renderer = new THREE.WebGLRenderer({ antialias: true });
250
+ renderer.setSize(width, height);
251
+ renderer.shadowMap.enabled = true;
252
+ renderer.shadowMap.type = THREE.PCFSoftShadowMap;
253
+ renderer.toneMapping = THREE.ACESFilmicToneMapping;
254
+ renderer.outputColorSpace = THREE.SRGBColorSpace;
255
+ sceneContainer.appendChild(renderer.domElement);
256
+
257
+ window.addEventListener('resize', onWindowResize, false);
258
+ setTimeout(onWindowResize, 100);
259
+ animate();
260
+ console.log("initThreeJS finished.");
261
+ }
262
+
263
+ function onWindowResize() {
264
+ if (!renderer || !camera || !sceneContainer) return;
265
+ const width = sceneContainer.clientWidth || 1;
266
+ const height = sceneContainer.clientHeight || 1;
267
+ if (width > 0 && height > 0) {
268
+ camera.aspect = width / height;
269
+ camera.updateProjectionMatrix();
270
+ renderer.setSize(width, height);
271
+ }
272
+ }
273
+
274
+ function animate() {
275
+ requestAnimationFrame(animate);
276
+ const delta = clock.getDelta();
277
+ const time = clock.getElapsedTime();
278
+
279
+ if (currentSceneGroup) {
280
+ currentSceneGroup.traverse(obj => {
281
+ if (obj.userData.update) obj.userData.update(time, delta);
282
+ });
283
+ }
284
+
285
+ if (renderer && scene && camera) renderer.render(scene, camera);
286
+ }
287
+
288
+ 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}) {
289
+ const mesh = new THREE.Mesh(geometry, material);
290
+ mesh.position.set(pos.x, pos.y, pos.z);
291
+ mesh.rotation.set(rot.x, rot.y, rot.z);
292
+ mesh.scale.set(scale.x, scale.y, scale.z);
293
+ mesh.castShadow = true; mesh.receiveShadow = true;
294
+ return mesh;
295
+ }
296
+
297
+ function setupLighting(type = 'default') {
298
+ currentLights.forEach(light => { if (light.parent) light.parent.remove(light); if(scene.children.includes(light)) scene.remove(light); });
299
+ currentLights = [];
300
+
301
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); // Slightly brighter ambient
302
+ scene.add(ambientLight);
303
+ currentLights.push(ambientLight);
304
+
305
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0); // Stronger directional
306
+ directionalLight.position.set(8, 15, 10);
307
+ directionalLight.castShadow = true;
308
+ directionalLight.shadow.mapSize.set(1024, 1024);
309
+ directionalLight.shadow.camera.near = 0.5; directionalLight.shadow.camera.far = 50;
310
+ const sb = 20;
311
+ directionalLight.shadow.camera.left = -sb; directionalLight.shadow.camera.right = sb;
312
+ directionalLight.shadow.camera.top = sb; directionalLight.shadow.camera.bottom = -sb;
313
+ directionalLight.shadow.bias = -0.0005;
314
+ scene.add(directionalLight);
315
+ currentLights.push(directionalLight);
316
+ }
317
+
318
+ // --- Procedural Assembly Function ---
319
+ function createProceduralAssembly(params) {
320
+ console.log("Creating procedural assembly with params:", params);
321
+ const group = new THREE.Group();
322
+ const {
323
+ baseShape = 'ground_grass', baseSize = 20,
324
+ mainShapes = ['boxy_chunk'], accents = ['pointy_cone'],
325
+ count = 10, scaleRange = [0.5, 1.5],
326
+ colorTheme = [MAT.stone_grey, MAT.wood_light, MAT.leaf_green],
327
+ arrangement = 'scatter', // scatter, cluster, stack, patch, center_stack
328
+ stackHeight = 5, // for stack/center_stack
329
+ clusterRadius = 5, // for cluster
330
+ patchPos = {x:0, y:0, z:0}, patchRadius = 3 // for patch
331
+ } = params;
332
+
333
+ // 1. Create Base
334
+ let baseMesh;
335
+ if (baseShape.startsWith('ground_')) {
336
+ baseMesh = createGround(MAT[baseShape] || MAT.ground_grass, baseSize);
337
+ group.add(baseMesh);
338
+ } else {
339
+ const baseGeo = SHAPE_GENERATORS[baseShape] ? SHAPE_GENERATORS[baseShape](baseSize) : new THREE.BoxGeometry(baseSize, 0.2, baseSize);
340
+ baseMesh = createMesh(baseGeo, colorTheme[0] || MAT.stone_grey, {y:0.1});
341
+ baseMesh.receiveShadow = true; baseMesh.castShadow = false;
342
+ group.add(baseMesh);
343
+ }
344
+
345
+ // 2. Generate Objects
346
+ const allShapes = [...mainShapes, ...accents];
347
+ let lastY = 0; // For stacking
348
+
349
+ for (let i = 0; i < count; i++) {
350
+ const shapeKey = allShapes[Math.floor(Math.random() * allShapes.length)];
351
+ const geoFunc = SHAPE_GENERATORS[shapeKey];
352
+ if (!geoFunc) { console.warn(`Shape generator not found for key: ${shapeKey}`); continue; }
353
+
354
+ const scale = scaleRange[0] + Math.random() * (scaleRange[1] - scaleRange[0]);
355
+ const geometry = geoFunc(scale);
356
+ const material = colorTheme[Math.floor(Math.random() * colorTheme.length)];
357
+ const mesh = createMesh(geometry, material);
358
+
359
+ // Calculate position based on arrangement
360
+ let position = { x:0, y:0, z:0 };
361
+ geometry.computeBoundingBox();
362
+ const height = (geometry.boundingBox.max.y - geometry.boundingBox.min.y) * mesh.scale.y;
363
+
364
+ switch(arrangement) {
365
+ case 'stack':
366
+ position.y = lastY + height / 2;
367
+ position.x = (Math.random() - 0.5) * 0.5; // Slight offset
368
+ position.z = (Math.random() - 0.5) * 0.5;
369
+ lastY += height * 0.9; // Stack slightly overlapping
370
+ if (j > stackHeight) break; // Limit height
371
+ break;
372
+ case 'center_stack':
373
+ position.y = lastY + height / 2;
374
+ lastY += height * 0.95;
375
+ if (i >= stackHeight) { // Only stack up to height, then stop adding
376
+ continue; // Skip adding more meshes if stack is full
377
+ }
378
+ break;
379
+ case 'cluster':
380
+ const angle = Math.random() * Math.PI * 2;
381
+ const radius = Math.random() * clusterRadius;
382
+ position.x = Math.cos(angle) * radius;
383
+ position.z = Math.sin(angle) * radius;
384
+ position.y = height / 2; // Place on ground
385
+ break;
386
+ case 'patch':
387
+ const pAngle = Math.random() * Math.PI * 2;
388
+ const pRadius = Math.random() * patchRadius;
389
+ position.x = patchPos.x + Math.cos(pAngle) * pRadius;
390
+ position.z = patchPos.z + Math.sin(pAngle) * pRadius;
391
+ position.y = (patchPos.y || 0) + height / 2; // Place relative to patch center Y
392
+ break;
393
+ case 'scatter':
394
+ default:
395
+ position.x = (Math.random() - 0.5) * baseSize * 0.8;
396
+ position.z = (Math.random() - 0.5) * baseSize * 0.8;
397
+ position.y = height / 2; // Place on ground
398
+ break;
399
+ }
400
+ mesh.position.set(position.x, position.y, position.z);
401
+
402
+ // Random rotation
403
+ mesh.rotation.set(
404
+ Math.random() * (arrangement === 'stack' || arrangement === 'center_stack' ? 0.1 : Math.PI), // Less random rotation if stacked
405
+ Math.random() * Math.PI * 2,
406
+ Math.random() * (arrangement === 'stack' || arrangement === 'center_stack' ? 0.1 : Math.PI)
407
+ );
408
+
409
+ // Add simple animation (optional)
410
+ if (Math.random() < 0.3) { // 30% chance of animation
411
+ mesh.userData.rotSpeed = (Math.random() - 0.5) * 0.5; // Slow rotation
412
+ mesh.userData.update = (time) => {
413
+ mesh.rotation.y += mesh.userData.rotSpeed * clock.getDelta();
414
+ };
415
+ }
416
+
417
+ group.add(mesh);
418
+ }
419
+
420
+ return group;
421
+ }
422
+
423
+
424
+ // --- Scene Management (Back to Page-Based) ---
425
+ function updateScene(assemblyParams) {
426
+ console.log("updateScene called");
427
+ // Clear previous scene objects
428
+ if (currentSceneGroup) {
429
+ currentSceneGroup.traverse(child => {
430
+ if (child.isMesh) {
431
+ if(child.geometry) child.geometry.dispose();
432
+ // Assume materials in MAT are shared, don't dispose them unless cloned per object
433
+ }
434
+ });
435
+ scene.remove(currentSceneGroup); // Remove the group from the main scene
436
+ currentSceneGroup = null;
437
+ }
438
+ // Clear previous lights (except maybe a default ambient?)
439
+ setupLighting('default'); // Reset to default lighting for each scene load
440
+
441
+ if (!assemblyParams) {
442
+ console.warn("No assemblyParams provided, creating default scene.");
443
+ assemblyParams = { baseShape: 'ground_dirt', count: 5 }; // Basic default
444
+ }
445
+
446
+ try {
447
+ currentSceneGroup = createProceduralAssembly(assemblyParams);
448
+ scene.add(currentSceneGroup); // Add the new group
449
+ console.log("New scene group added.");
450
+ } catch (error) {
451
+ console.error("Error creating procedural assembly:", error);
452
+ // Optionally add an error indicator scene
453
+ }
454
+ }
455
+
456
+
457
+ // --- Game Logic ---
458
+ function startGame() {
459
+ console.log("startGame called.");
460
+ const defaultChar = {
461
+ name: "Player",
462
+ stats: { hp: 20, maxHp: 20, xp: 0, strength: 10, dexterity: 10, constitution: 10, intelligence: 10, wisdom: 10, charisma: 10 },
463
+ inventory: ["Sturdy Stick"] // Start with an item
464
+ };
465
+ gameState = {
466
+ currentPageId: 1, // Start at page 1
467
+ character: JSON.parse(JSON.stringify(defaultChar))
468
+ };
469
+ renderPage(gameState.currentPageId);
470
+ console.log("startGame finished.");
471
+ }
472
+
473
+ function handleChoiceClick(choiceData) {
474
+ console.log("Choice clicked:", choiceData);
475
+ currentMessage = ""; // Clear previous messages
476
+
477
+ let nextPageId = parseInt(choiceData.next);
478
+ const currentPageData = gameData[gameState.currentPageId];
479
+ const targetPageData = gameData[nextPageId];
480
+
481
+ if (!targetPageData) {
482
+ console.error(`Invalid next page ID: ${nextPageId}`);
483
+ currentMessage = `<p class="message message-warning">Oops! That path seems to lead nowhere yet.</p>`;
484
+ nextPageId = gameState.currentPageId; // Stay on current page on error
485
+ }
486
+
487
+ // Apply rewards from *current* page choice before transitioning
488
+ if (currentPageData && currentPageData.options) {
489
+ const chosenOption = currentPageData.options.find(opt => opt.next === choiceData.next); // Find the specific option clicked
490
+ if (chosenOption && chosenOption.reward) {
491
+ console.log("Applying reward:", chosenOption.reward);
492
+ if(chosenOption.reward.addItem && itemsData[chosenOption.reward.addItem]) {
493
+ if (!gameState.character.inventory.includes(chosenOption.reward.addItem)) {
494
+ gameState.character.inventory.push(chosenOption.reward.addItem);
495
+ currentMessage += `<p class="message message-item">You found a ${chosenOption.reward.addItem}!</p>`;
496
+ } else {
497
+ currentMessage += `<p class="message message-info">You found another ${chosenOption.reward.addItem}, but can't carry more.</p>`;
498
+ }
499
+ }
500
+ if(chosenOption.reward.xp) {
501
+ gameState.character.stats.xp += chosenOption.reward.xp;
502
+ currentMessage += `<p class="message message-xp">You gained ${chosenOption.reward.xp} XP!</p>`;
503
+ }
504
+ // Add stat gain logic here if needed for specific rewards
505
+ }
506
+ }
507
+
508
+
509
+ gameState.currentPageId = nextPageId;
510
+ renderPage(nextPageId);
511
+ }
512
+
513
+ function renderPage(pageId) {
514
+ console.log(`Rendering page ${pageId}`);
515
+ const pageData = gameData[pageId];
516
+
517
+ if (!pageData) {
518
+ console.error(`No page data for ID: ${pageId}`);
519
+ storyTitleElement.textContent = "Lost!";
520
+ storyContentElement.innerHTML = currentMessage + `<p>Somehow you've wandered off the map!</p>`;
521
+ choicesElement.innerHTML = `<button class="choice-button" onclick="handleChoiceClick({ next: 1 })">Go Back to Start</button>`;
522
+ updateStatsDisplay(); updateInventoryDisplay(); updateActionInfo();
523
+ updateScene({ baseShape: 'ground_dirt', count: 1, mainShapes: ['basic_tetra'], colorTheme: [MAT.metal_rusty] }); // Simple error scene
524
+ return;
525
+ }
526
+
527
+ storyTitleElement.textContent = pageData.title || "An Unnamed Place";
528
+ storyContentElement.innerHTML = currentMessage + (pageData.content || "<p>...</p>");
529
+ choicesElement.innerHTML = ''; // Clear previous choices
530
+
531
+ if (pageData.options && pageData.options.length > 0) {
532
+ pageData.options.forEach(option => {
533
+ const button = document.createElement('button');
534
+ button.classList.add('choice-button');
535
+ button.textContent = option.text;
536
+ button.onclick = () => handleChoiceClick(option); // Pass the whole option
537
+ choicesElement.appendChild(button);
538
+ });
539
+ } else {
540
+ // Maybe always add a restart button if no other options?
541
+ const button = document.createElement('button');
542
+ button.classList.add('choice-button');
543
+ button.textContent = "The End (Restart?)";
544
+ button.onclick = () => handleChoiceClick({ next: 1 });
545
+ choicesElement.appendChild(button);
546
+ }
547
+
548
+ updateStatsDisplay();
549
+ updateInventoryDisplay();
550
+ updateActionInfo();
551
+ updateScene(pageData.assemblyParams); // Update 3D scene based on page params
552
+ }
553
+ // Make handleChoiceClick globally accessible for inline handlers
554
+ window.handleChoiceClick = handleChoiceClick;
555
+
556
+ function updateStatsDisplay() {
557
+ if (!gameState.character || !statsElement) return;
558
+ const stats = gameState.character.stats;
559
+ const hpColor = stats.hp / stats.maxHp < 0.3 ? '#f88' : (stats.hp / stats.maxHp < 0.6 ? '#fd5' : '#8f8');
560
+
561
+ statsElement.innerHTML = `<strong>Stats:</strong>
562
+ <span style="color:${hpColor}">HP: ${stats.hp}/${stats.maxHp}</span> <span>XP: ${stats.xp}</span><br>
563
+ <span>Str: ${stats.strength}</span> <span>Dex: ${stats.dexterity}</span> <span>Con: ${stats.constitution}</span>
564
+ <span>Int: ${stats.intelligence}</span> <span>Wis: ${stats.wisdom}</span> <span>Cha: ${stats.charisma}</span>`;
565
+ }
566
+
567
+ function updateInventoryDisplay() {
568
+ if (!gameState.character || !inventoryElement) return;
569
+ let invHtml = '<strong>Inventory:</strong> ';
570
+ if (gameState.character.inventory.length === 0) {
571
+ invHtml += '<em>Empty</em>';
572
+ } else {
573
+ gameState.character.inventory.forEach(item => {
574
+ const itemDef = itemsData[item] || { type: 'unknown', description: '???' };
575
+ const itemClass = `item-${itemDef.type || 'unknown'}`;
576
+ invHtml += `<span class="item-tag ${itemClass}" title="${itemDef.description}">${item}</span>`;
577
+ });
578
+ }
579
+ inventoryElement.innerHTML = invHtml;
580
+ }
581
+
582
+ function updateActionInfo() {
583
+ if (!actionInfoElement || !gameState ) return;
584
+ actionInfoElement.textContent = `Location: ${gameData[gameState.currentPageId]?.title || 'Unknown'}`;
585
+ }
586
+
587
+
588
+ // --- Initialization ---
589
+ document.addEventListener('DOMContentLoaded', () => {
590
+ console.log("DOM Ready - Initializing Wacky Shapes Adventure.");
591
+ try {
592
+ initThreeJS();
593
+ if (!scene || !camera || !renderer) throw new Error("Three.js failed to initialize.");
594
+ startGame(); // Start game directly
595
+ console.log("Game world initialized and started.");
596
+ } catch (error) {
597
+ console.error("Initialization failed:", error);
598
+ storyTitleElement.textContent = "Initialization Error";
599
+ 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>`;
600
+ if(sceneContainer) sceneContainer.innerHTML = '<p style="color:red; padding: 20px;">3D Scene Failed</p>';
601
+ document.getElementById('stats-inventory-container').style.display = 'none';
602
+ document.getElementById('choices-container').style.display = 'none';
603
+ }
604
+ });
605
+
606
+ </script>
607
+ </body>
608
+ </html>