awacke1 commited on
Commit
d23d185
·
verified ·
1 Parent(s): 37a085b

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +371 -473
index.html CHANGED
@@ -1,17 +1,13 @@
1
  <!DOCTYPE html>
2
  <html>
3
  <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>Shared World Builder</title>
7
  <style>
8
  body { margin: 0; overflow: hidden; }
9
  canvas { display: block; }
10
- #info { position: absolute; top: 10px; left: 10px; color: white; background: rgba(0,0,0,0.7); padding: 10px; }
11
  </style>
12
- </head>
13
  <body>
14
- <div id="info">Click to place selected object or move player, right-click to delete.</div>
15
  <script type="importmap">
16
  {
17
  "imports": {
@@ -24,21 +20,31 @@
24
  <script type="module">
25
  import * as THREE from 'three';
26
 
27
- let scene, camera, renderer;
28
  let raycaster, mouse;
29
- let worldObjects = new Map();
30
- let groundMeshes = {};
31
 
32
- // Access State from Streamlit
 
 
 
 
 
 
 
 
 
33
  const myUsername = window.USERNAME || `User_${Math.random().toString(36).substring(2, 6)}`;
34
- let selectedObjectType = window.SELECTED_OBJECT_TYPE || "None";
 
35
  const plotWidth = window.PLOT_WIDTH || 50.0;
36
  const plotDepth = window.PLOT_DEPTH || 50.0;
37
- const worldState = window.WORLD_STATE || { objects: {}, players: {}, action_history: [], selected_object: "None" };
38
 
39
- // Materials
40
  const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x55aa55, roughness: 0.9, metalness: 0.1, side: THREE.DoubleSide });
41
  const placeholderGroundMaterial = new THREE.MeshStandardMaterial({ color: 0x448844, roughness: 0.95, metalness: 0.1, side: THREE.DoubleSide });
 
42
  const objectMaterials = {
43
  'wood': new THREE.MeshStandardMaterial({ color: 0x8B4513, roughness: 0.9 }),
44
  'leaf': new THREE.MeshStandardMaterial({ color: 0x228B22, roughness: 0.8 }),
@@ -48,27 +54,25 @@
48
  'brick': new THREE.MeshStandardMaterial({ color: 0x9B4C43, roughness: 0.85 }),
49
  'metal': new THREE.MeshStandardMaterial({ color: 0xcccccc, roughness: 0.4, metalness: 0.8 }),
50
  'gem': new THREE.MeshStandardMaterial({ color: 0x4FFFFF, roughness: 0.1, metalness: 0.2, transparent: true, opacity: 0.8 }),
51
- 'light': new THREE.MeshBasicMaterial({ color: 0xFFFF88 })
 
52
  };
53
 
54
- const VALID_OBJECT_TYPES = [
55
- "Tree", "Rock", "Simple House", "Pine Tree", "Brick Wall", "Sphere", "Cube",
56
- "Cylinder", "Cone", "Torus", "Mushroom", "Cactus", "Campfire", "Star", "Gem",
57
- "Tower", "Barrier", "Fountain", "Lantern", "Sign Post"
58
- ];
59
 
60
  function init() {
61
  scene = new THREE.Scene();
62
  scene.background = new THREE.Color(0xabcdef);
63
 
64
  const aspect = window.innerWidth / window.innerHeight;
65
- camera = new THREE.PerspectiveCamera(60, aspect, 0.1, 5000);
66
  camera.position.set(plotWidth / 2, 15, plotDepth / 2 + 20);
67
  camera.lookAt(plotWidth/2, 0, plotDepth/2);
68
  scene.add(camera);
69
 
70
  setupLighting();
71
- createGroundPlane(0, 0, false);
 
 
72
 
73
  raycaster = new THREE.Raycaster();
74
  mouse = new THREE.Vector2();
@@ -79,169 +83,212 @@
79
  renderer.shadowMap.type = THREE.PCFSoftShadowMap;
80
  document.body.appendChild(renderer.domElement);
81
 
 
 
 
 
82
  document.addEventListener('mousemove', onMouseMove, false);
83
- document.addEventListener('click', onDocumentClick, false);
84
- document.addEventListener('contextmenu', onRightClick, false);
85
  window.addEventListener('resize', onWindowResize, false);
 
 
86
 
87
- window.updateSelectedObjectType = updateSelectedObjectType;
 
 
 
 
88
 
89
- // Initialize scene with world state
90
- loadWorldState();
91
-
92
- console.log(`Three.js Initialized for user: ${myUsername}`);
93
  animate();
94
  }
95
 
96
- function setupLighting() {
97
- const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
98
- scene.add(ambientLight);
99
- const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2);
100
- directionalLight.position.set(75, 150, 100);
101
- directionalLight.castShadow = true;
102
- directionalLight.shadow.mapSize.width = 2048;
103
- directionalLight.shadow.mapSize.height = 2048;
104
- directionalLight.shadow.camera.near = 10;
105
- directionalLight.shadow.camera.far = 400;
106
- directionalLight.shadow.camera.left = -150;
107
- directionalLight.shadow.camera.right = 150;
108
- directionalLight.shadow.camera.top = 150;
109
- directionalLight.shadow.camera.bottom = -150;
110
- directionalLight.shadow.bias = -0.002;
111
- scene.add(directionalLight);
112
- const hemiLight = new THREE.HemisphereLight(0xabcdef, 0x55aa55, 0.5);
113
- scene.add(hemiLight);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  }
115
 
116
- function createGroundPlane(gridX, gridZ, isPlaceholder) {
117
- const gridKey = `${gridX}_${gridZ}`;
118
- if (groundMeshes[gridKey]) return groundMeshes[gridKey];
119
- const groundGeometry = new THREE.PlaneGeometry(plotWidth, plotDepth);
120
- const material = isPlaceholder ? placeholderGroundMaterial : groundMaterial;
121
- const groundMesh = new THREE.Mesh(groundGeometry, material);
122
- groundMesh.rotation.x = -Math.PI / 2;
123
- groundMesh.position.y = -0.05;
124
- groundMesh.position.x = gridX * plotWidth + plotWidth / 2.0;
125
- groundMesh.position.z = gridZ * plotDepth + plotDepth / 2.0;
126
- groundMesh.receiveShadow = true;
127
- groundMesh.userData.gridKey = gridKey;
128
- groundMesh.userData.isPlaceholder = isPlaceholder;
129
- groundMesh.name = 'ground';
130
- scene.add(groundMesh);
131
- groundMeshes[gridKey] = groundMesh;
132
- return groundMesh;
133
  }
134
 
135
- function loadWorldState() {
136
- clearWorldObjects();
137
- const objects = worldState.objects || {};
138
- console.log(`Loading ${Object.keys(objects).length} objects from WORLD_STATE`);
139
- for (const obj_id in objects) {
140
- const objData = objects[obj_id];
141
- if (!objData || !objData.obj_id || !objData.type || !VALID_OBJECT_TYPES.includes(objData.type) || !objData.position) {
142
- console.warn(`Skipping invalid object: ${obj_id}`, objData);
143
- continue;
144
- }
145
- const mesh = createAndPlaceObject(objData, false);
146
- if (!mesh) {
147
- console.warn(`Failed to create mesh for object: ${obj_id}`, objData);
148
- }
149
- }
150
- const players = worldState.players || {};
151
- console.log(`Loading ${Object.keys(players).length} players from WORLD_STATE`);
152
- for (const username in players) {
153
- const pos = players[username].position;
154
- if (pos && typeof pos.x === 'number' && typeof pos.y === 'number' && typeof pos.z === 'number') {
155
- createPlayerMarker(username, pos);
156
- } else {
157
- console.warn(`Invalid player position for ${username}:`, pos);
158
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  }
160
- selectedObjectType = worldState.selected_object || "None";
161
- console.log(`Loaded selected tool: ${selectedObjectType}`);
162
- const actionHistory = worldState.action_history || [];
163
- console.log(`Loaded ${actionHistory.length} action history entries`);
164
  }
165
 
166
  function clearWorldObjects() {
167
- console.log("Clearing existing world objects...");
168
- for (const [obj_id, mesh] of worldObjects.entries()) {
169
- scene.remove(mesh);
170
- }
171
- worldObjects.clear();
172
- scene.children.forEach(child => {
173
- if (child.userData.isPlayerMarker) {
174
- scene.remove(child);
175
- }
176
- });
177
  }
178
 
179
- function createPlayerMarker(username, position) {
180
- const geometry = new THREE.SphereGeometry(0.3, 8, 8);
181
- const material = new THREE.MeshBasicMaterial({ color: username === myUsername ? 0x0000ff : 0xff0000 });
182
- const marker = new THREE.Mesh(geometry, material);
183
- marker.position.set(position.x, position.y + 0.5, position.z);
184
- marker.userData.isPlayerMarker = true;
185
- marker.userData.username = username;
186
- scene.add(marker);
187
- console.log(`Created player marker for ${username} at`, position);
 
188
  }
189
 
190
- function createAndPlaceObject(objData, isNewlyPlacedLocally) {
191
- if (!objData || !objData.obj_id || !objData.type || !objData.position) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
  console.warn("Invalid object data:", objData);
193
  return null;
194
  }
195
 
 
196
  let mesh = worldObjects.get(objData.obj_id);
197
  let isNew = false;
198
 
199
  if (mesh) {
200
- if (mesh.position.distanceToSquared(new THREE.Vector3(objData.position.x, objData.position.y, objData.position.z)) > 0.001) {
201
- mesh.position.set(objData.position.x, objData.position.y, objData.position.z);
202
- }
203
- if (objData.rotation && (
204
- Math.abs(mesh.rotation.x - (objData.rotation._x || 0)) > 0.01 ||
205
- Math.abs(mesh.rotation.y - (objData.rotation._y || 0)) > 0.01 ||
206
- Math.abs(mesh.rotation.z - (objData.rotation._z || 0)) > 0.01 )) {
207
- mesh.rotation.set(
208
- objData.rotation._x || 0,
209
- objData.rotation._y || 0,
210
- objData.rotation._z || 0,
211
- objData.rotation._order || 'XYZ'
212
- );
213
- }
214
  } else {
215
- mesh = createPrimitiveMesh(objData.type);
216
- if (!mesh) return null;
217
- isNew = true;
218
- mesh.userData.obj_id = objData.obj_id;
219
- mesh.userData.type = objData.type;
220
- mesh.position.set(objData.position.x, objData.position.y, objData.position.z);
221
- if (objData.rotation) {
222
- mesh.rotation.set(
223
- objData.rotation._x || 0,
224
- objData.rotation._y || 0,
225
- objData.rotation._z || 0,
226
- objData.rotation._order || 'XYZ'
227
- );
228
- }
229
- scene.add(mesh);
230
- worldObjects.set(objData.obj_id, mesh);
231
- console.log(`Created new object ${objData.obj_id} (${objData.type}) at`, objData.position);
232
  }
233
  return mesh;
234
  }
235
 
 
236
  function createPrimitiveMesh(type) {
237
- if (!VALID_OBJECT_TYPES.includes(type)) {
238
- console.warn(`Invalid object type for mesh creation: ${type}`);
239
- return null;
240
- }
241
-
242
  let mesh = null;
243
- let geometry, material, material2;
244
 
 
245
  const wood = objectMaterials.wood;
246
  const leaf = objectMaterials.leaf;
247
  const stone = objectMaterials.stone;
@@ -252,280 +299,168 @@
252
  const gem = objectMaterials.gem;
253
  const lightMat = objectMaterials.light;
254
 
255
- try {
256
  switch(type) {
 
257
  case "Tree":
258
  mesh = new THREE.Group();
259
- geometry = new THREE.CylinderGeometry(0.3, 0.4, 2, 8);
260
- material = wood;
261
- const trunk = new THREE.Mesh(geometry, material);
262
- trunk.position.y = 1;
263
- trunk.castShadow = true;
264
- trunk.receiveShadow = true;
265
- mesh.add(trunk);
266
- geometry = new THREE.IcosahedronGeometry(1.2, 0);
267
- material = leaf;
268
- const canopy = new THREE.Mesh(geometry, material);
269
- canopy.position.y = 2.8;
270
- canopy.castShadow = true;
271
- canopy.receiveShadow = false;
272
- mesh.add(canopy);
273
  break;
274
  case "Rock":
275
- geometry = new THREE.IcosahedronGeometry(0.7, 1);
276
- material = stone;
277
- mesh = new THREE.Mesh(geometry, material);
278
- mesh.castShadow = true;
279
- mesh.receiveShadow = true;
280
- mesh.scale.set(1, Math.random()*0.4 + 0.8, 1);
281
  break;
282
  case "Simple House":
283
  mesh = new THREE.Group();
284
- geometry = new THREE.BoxGeometry(2, 1.5, 2.5);
285
- material = house_wall;
286
- const body = new THREE.Mesh(geometry, material);
287
- body.position.y = 0.75;
288
- body.castShadow = true;
289
- body.receiveShadow = true;
290
- mesh.add(body);
291
- geometry = new THREE.ConeGeometry(1.8, 1, 4);
292
- material = house_roof;
293
- const roof = new THREE.Mesh(geometry, material);
294
- roof.position.y = 1.5 + 0.5;
295
- roof.rotation.y = Math.PI / 4;
296
- roof.castShadow = true;
297
- roof.receiveShadow = false;
298
- mesh.add(roof);
299
  break;
300
- case "Pine Tree":
 
 
301
  mesh = new THREE.Group();
302
- geometry = new THREE.CylinderGeometry(0.2, 0.3, 2.5, 8);
303
- material = wood;
304
- const pineTrunk = new THREE.Mesh(geometry, material);
305
- pineTrunk.position.y = 1.25;
306
- pineTrunk.castShadow = true;
307
- pineTrunk.receiveShadow = true;
308
- mesh.add(pineTrunk);
309
- geometry = new THREE.ConeGeometry(1, 2.5, 8);
310
- material = leaf;
311
- const pineCanopy = new THREE.Mesh(geometry, material);
312
- pineCanopy.position.y = 2.5 + (2.5/2) - 0.5;
313
- pineCanopy.castShadow = true;
314
- pineCanopy.receiveShadow = false;
315
- mesh.add(pineCanopy);
316
  break;
317
- case "Brick Wall":
318
- geometry = new THREE.BoxGeometry(3, 2, 0.3);
319
- material = brick;
320
- mesh = new THREE.Mesh(geometry, material);
321
- mesh.position.y = 1;
322
- mesh.castShadow = true;
323
- mesh.receiveShadow = true;
324
  break;
325
  case "Sphere":
326
- geometry = new THREE.SphereGeometry(0.8, 16, 12);
327
- material = metal;
328
- mesh = new THREE.Mesh(geometry, material);
329
- mesh.position.y = 0.8;
330
- mesh.castShadow = true;
331
- mesh.receiveShadow = true;
332
  break;
333
- case "Cube":
334
- geometry = new THREE.BoxGeometry(1, 1, 1);
335
- material = stone;
336
- mesh = new THREE.Mesh(geometry, material);
337
- mesh.position.y = 0.5;
338
- mesh.castShadow = true;
339
- mesh.receiveShadow = true;
340
  break;
341
  case "Cylinder":
342
- geometry = new THREE.CylinderGeometry(0.5, 0.5, 1.5, 16);
343
- material = metal;
344
- mesh = new THREE.Mesh(geometry, material);
345
- mesh.position.y = 0.75;
346
- mesh.castShadow = true;
347
- mesh.receiveShadow = true;
348
  break;
349
  case "Cone":
350
- geometry = new THREE.ConeGeometry(0.6, 1.2, 16);
351
- material = house_roof;
352
- mesh = new THREE.Mesh(geometry, material);
353
- mesh.position.y = 0.6;
354
- mesh.castShadow = true;
355
- mesh.receiveShadow = true;
356
  break;
357
- case "Torus":
358
- geometry = new THREE.TorusGeometry(0.6, 0.2, 8, 24);
359
- material = gem;
360
- mesh = new THREE.Mesh(geometry, material);
361
- mesh.position.y = 0.7;
362
- mesh.castShadow = true;
363
- mesh.receiveShadow = true;
364
- mesh.rotation.x = Math.PI / 2;
365
  break;
366
  case "Mushroom":
367
- mesh = new THREE.Group();
368
- geometry = new THREE.CylinderGeometry(0.15, 0.1, 0.6, 8);
369
- material = house_wall;
370
- const stem = new THREE.Mesh(geometry, material);
371
- stem.position.y = 0.3;
372
- stem.castShadow = true;
373
- stem.receiveShadow = true;
374
- mesh.add(stem);
375
- geometry = new THREE.SphereGeometry(0.4, 16, 8, 0, Math.PI * 2, 0, Math.PI / 2);
376
- material = house_roof;
377
- const cap = new THREE.Mesh(geometry, material);
378
- cap.position.y = 0.6;
379
- cap.castShadow = true;
380
- cap.receiveShadow = false;
381
- mesh.add(cap);
382
- break;
383
- case "Cactus":
384
- mesh = new THREE.Group();
385
- material = leaf;
386
  geometry = new THREE.CylinderGeometry(0.3, 0.3, 1.5, 8);
387
- const main = new THREE.Mesh(geometry, material);
388
- main.position.y = 0.75;
389
- main.castShadow = true;
390
- main.receiveShadow = true;
391
- mesh.add(main);
392
  geometry = new THREE.CylinderGeometry(0.2, 0.2, 0.8, 8);
393
- const arm1 = new THREE.Mesh(geometry, material);
394
- arm1.position.set(0.3, 1, 0);
395
- arm1.rotation.z = Math.PI / 4;
396
- arm1.castShadow = true;
397
- arm1.receiveShadow = true;
398
- mesh.add(arm1);
399
- const arm2 = new THREE.Mesh(geometry, material);
400
- arm2.position.set(-0.3, 0.8, 0);
401
- arm2.rotation.z = -Math.PI / 4;
402
- arm2.castShadow = true;
403
- arm2.receiveShadow = true;
404
- mesh.add(arm2);
405
  break;
406
  case "Campfire":
407
- mesh = new THREE.Group();
408
- material = wood;
409
- geometry = new THREE.CylinderGeometry(0.1, 0.1, 0.8, 5);
410
- const log1 = new THREE.Mesh(geometry, material);
411
- log1.rotation.x = Math.PI/2;
412
- log1.position.set(0, 0.1, 0.2);
413
- mesh.add(log1);
414
- const log2 = new THREE.Mesh(geometry, material);
415
- log2.rotation.set(Math.PI/2, 0, Math.PI/3);
416
- log2.position.set(0.2*Math.cos(Math.PI/6), 0.1, -0.2*Math.sin(Math.PI/6));
417
- mesh.add(log2);
418
- const log3 = new THREE.Mesh(geometry, material);
419
- log3.rotation.set(Math.PI/2, 0, -Math.PI/3);
420
- log3.position.set(-0.2*Math.cos(Math.PI/6), 0.1, -0.2*Math.sin(Math.PI/6));
421
- mesh.add(log3);
422
- material2 = lightMat;
423
- geometry = new THREE.ConeGeometry(0.2, 0.5, 8);
424
- const flame = new THREE.Mesh(geometry, material2);
425
- flame.position.y = 0.35;
426
- mesh.add(flame);
427
- break;
428
  case "Star":
429
- geometry = new THREE.SphereGeometry(0.5, 4, 2);
430
- material = lightMat;
431
- mesh = new THREE.Mesh(geometry, material);
432
- mesh.position.y = 1;
433
- break;
434
  case "Gem":
435
- geometry = new THREE.OctahedronGeometry(0.6, 0);
436
- material = gem;
437
- mesh = new THREE.Mesh(geometry, material);
438
- mesh.position.y = 0.6;
439
- mesh.castShadow = true;
440
- mesh.receiveShadow = true;
441
- break;
442
- case "Tower":
443
- geometry = new THREE.CylinderGeometry(1, 1.2, 5, 8);
444
- material = stone;
445
- mesh = new THREE.Mesh(geometry, material);
446
- mesh.position.y = 2.5;
447
- mesh.castShadow = true;
448
- mesh.receiveShadow = true;
449
- break;
450
- case "Barrier":
451
- geometry = new THREE.BoxGeometry(2, 0.5, 0.5);
452
- material = metal;
453
- mesh = new THREE.Mesh(geometry, material);
454
- mesh.position.y = 0.25;
455
- mesh.castShadow = true;
456
- mesh.receiveShadow = true;
457
- break;
458
- case "Fountain":
459
- mesh = new THREE.Group();
460
- material = stone;
461
- geometry = new THREE.CylinderGeometry(1.5, 1.5, 0.3, 16);
462
- const baseF = new THREE.Mesh(geometry, material);
463
- baseF.position.y = 0.15;
464
- mesh.add(baseF);
465
- geometry = new THREE.CylinderGeometry(0.8, 0.8, 0.5, 16);
466
- const midF = new THREE.Mesh(geometry, material);
467
- midF.position.y = 0.3 + 0.25;
468
- mesh.add(midF);
469
- geometry = new THREE.CylinderGeometry(0.4, 0.4, 0.7, 16);
470
- const topF = new THREE.Mesh(geometry, material);
471
- topF.position.y = 0.8 + 0.35;
472
- mesh.add(topF);
473
- mesh.castShadow = true;
474
- mesh.receiveShadow = true;
475
- break;
476
- case "Lantern":
477
- mesh = new THREE.Group();
478
- material = metal;
479
- geometry = new THREE.BoxGeometry(0.4, 0.6, 0.4);
480
- const bodyL = new THREE.Mesh(geometry, material);
481
- bodyL.position.y = 0.3;
482
- mesh.add(bodyL);
483
- geometry = new THREE.SphereGeometry(0.15);
484
- material2 = lightMat;
485
- const lightL = new THREE.Mesh(geometry, material2);
486
- lightL.position.y = 0.3;
487
- mesh.add(lightL);
488
- mesh.castShadow = true;
489
- break;
490
- case "Sign Post":
491
- mesh = new THREE.Group();
492
- material = wood;
493
- geometry = new THREE.CylinderGeometry(0.05, 0.05, 1.8, 8);
494
- const postS = new THREE.Mesh(geometry, material);
495
- postS.position.y = 0.9;
496
- mesh.add(postS);
497
- geometry = new THREE.BoxGeometry(0.8, 0.4, 0.05);
498
- const signS = new THREE.Mesh(geometry, material);
499
- signS.position.y = 1.5;
500
- mesh.add(signS);
501
- mesh.castShadow = true;
502
- mesh.receiveShadow = true;
503
- break;
504
  default:
505
- console.warn("Unexpected object type:", type);
506
- return null;
507
  }
508
  } catch (e) {
509
  console.error(`Error creating geometry/mesh for type ${type}:`, e);
510
  return null;
511
  }
512
 
 
513
  if (mesh) {
 
514
  mesh.userData = { type: type };
 
515
  if (!mesh.position.y && mesh.geometry) {
516
- mesh.geometry.computeBoundingBox();
517
- mesh.position.y = (mesh.geometry.boundingBox.max.y - mesh.geometry.boundingBox.min.y) / 2;
518
  }
519
  }
520
  return mesh;
521
  }
522
 
523
- function onMouseMove(event) {
524
- mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
525
- mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
 
526
  }
527
 
528
  function onDocumentClick(event) {
 
 
529
  const groundCandidates = Object.values(groundMeshes);
530
  if (groundCandidates.length === 0) return;
531
 
@@ -535,116 +470,79 @@
535
  if (intersects.length > 0) {
536
  const intersectPoint = intersects[0].point;
537
 
538
- if (selectedObjectType !== "None" && selectedObjectType) {
539
- // Place object
540
- const newObjData = {
541
- obj_id: THREE.MathUtils.generateUUID(),
542
- type: selectedObjectType,
543
- position: { x: intersectPoint.x, y: 0, z: intersectPoint.z },
544
- rotation: { _x: 0, _y: Math.random() * Math.PI * 2, _z: 0, _order: 'XYZ' }
545
- };
546
-
547
- const tempMesh = createPrimitiveMesh(selectedObjectType);
548
- if (tempMesh && tempMesh.geometry) {
549
- tempMesh.geometry.computeBoundingBox();
550
- const height = tempMesh.geometry.boundingBox.max.y - tempMesh.geometry.boundingBox.min.y;
551
- newObjData.position.y = (height / 2) + intersectPoint.y + 0.01;
552
- } else {
553
- newObjData.position.y = 0.5 + intersectPoint.y;
554
- }
555
-
556
- console.log(`Placing ${selectedObjectType} (${newObjData.obj_id}) at`, newObjData.position);
557
-
558
- const mesh = createAndPlaceObject(newObjData, true);
559
- if (mesh) {
560
- window.parent.postMessage({
561
- type: 'place_object',
562
- payload: { username: myUsername, object_data: newObjData }
563
- }, '*');
564
- }
565
  } else {
566
- // Move player
567
- const newPosition = {
568
- x: intersectPoint.x,
569
- y: 0.5,
570
- z: intersectPoint.z
571
- };
572
- console.log(`Moving player ${myUsername} to`, newPosition);
573
-
574
- window.parent.postMessage({
575
- type: 'move_player',
576
- payload: { username: myUsername, position: newPosition }
577
- }, '*');
578
  }
579
- }
580
- }
581
 
582
- function onRightClick(event) {
583
- event.preventDefault();
584
- raycaster.setFromCamera(mouse, camera);
585
- const intersects = raycaster.intersectObjects(Array.from(worldObjects.values()));
586
 
587
- if (intersects.length > 0) {
588
- const obj_id = intersects[0].object.userData.obj_id;
589
- console.log(`Deleting object ${obj_id}`);
590
- removeObjectById(obj_id);
591
- window.parent.postMessage({
592
- type: 'delete_object',
593
- payload: { username: myUsername, obj_id: obj_id }
594
- }, '*');
595
- }
596
- }
597
 
598
- function removeObjectById(obj_id) {
599
- if (worldObjects.has(obj_id)) {
600
- const mesh = worldObjects.get(obj_id);
601
- scene.remove(mesh);
602
- worldObjects.delete(obj_id);
603
- console.log(`Removed object ${obj_id} from scene.`);
604
- } else {
605
- console.warn(`Attempted to remove non-existent object ID: ${obj_id}`);
606
  }
607
  }
608
 
609
- function onWindowResize() {
610
- camera.aspect = window.innerWidth / window.innerHeight;
611
- camera.updateProjectionMatrix();
612
- renderer.setSize(window.innerWidth, window.innerHeight);
 
 
 
 
 
 
 
 
613
  }
614
 
615
- function updateSelectedObjectType(newType) {
616
- console.log("JS updateSelectedObjectType received:", newType);
617
- selectedObjectType = newType;
618
- window.parent.postMessage({
619
- type: 'tool_change',
620
- payload: { username: myUsername, tool: newType }
621
- }, '*');
 
 
 
 
622
  }
623
 
624
  function animate() {
625
  requestAnimationFrame(animate);
 
 
626
  renderer.render(scene, camera);
627
  }
628
 
629
- // Listen for messages from Streamlit
630
- window.addEventListener('message', (event) => {
631
- const data = event.data;
632
- if (data.type === 'place_object') {
633
- const mesh = createAndPlaceObject(data.payload.object_data, false);
634
- if (!mesh) {
635
- console.warn(`Failed to place object from message:`, data.payload.object_data);
636
- }
637
- } else if (data.type === 'delete_object') {
638
- removeObjectById(data.payload.obj_id);
639
- } else if (data.type === 'move_player') {
640
- const username = data.payload.username;
641
- const position = data.payload.position;
642
- scene.children.forEach(child => {
643
- if (child.userData.isPlayerMarker && child.userData.username === username) {
644
- child.position.set(position.x, position.y + 0.5, position.z);
645
- console.log(`Updated player marker for ${username} to`, position);
646
- }
647
- });
648
- } else if (data.type === 'tool_change') {
649
- selectedObjectType = data.payload.tool;
650
- console.log(`Updated selected tool from message:
 
1
  <!DOCTYPE html>
2
  <html>
3
  <head>
 
 
4
  <title>Shared World Builder</title>
5
  <style>
6
  body { margin: 0; overflow: hidden; }
7
  canvas { display: block; }
 
8
  </style>
9
+ </head>
10
  <body>
 
11
  <script type="importmap">
12
  {
13
  "imports": {
 
20
  <script type="module">
21
  import * as THREE from 'three';
22
 
23
+ let scene, camera, renderer, playerMesh;
24
  let raycaster, mouse;
25
+ const keysPressed = {};
26
+ const playerSpeed = 0.15;
27
 
28
+ // --- World State (Managed by Server via WebSocket) ---
29
+ const worldObjects = new Map(); // Map<obj_id, THREE.Object3D>
30
+ const groundMeshes = {}; // Map<gridKey, THREE.Mesh>
31
+
32
+ // --- WebSocket ---
33
+ let socket = null;
34
+ let connectionRetries = 0;
35
+ const MAX_RETRIES = 5;
36
+
37
+ // --- Access State from Streamlit (Injected) ---
38
  const myUsername = window.USERNAME || `User_${Math.random().toString(36).substring(2, 6)}`;
39
+ const websocketUrl = window.WEBSOCKET_URL || "ws://localhost:8765";
40
+ let selectedObjectType = window.SELECTED_OBJECT_TYPE || "None"; // Can be updated
41
  const plotWidth = window.PLOT_WIDTH || 50.0;
42
  const plotDepth = window.PLOT_DEPTH || 50.0;
 
43
 
44
+ // --- Materials ---
45
  const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x55aa55, roughness: 0.9, metalness: 0.1, side: THREE.DoubleSide });
46
  const placeholderGroundMaterial = new THREE.MeshStandardMaterial({ color: 0x448844, roughness: 0.95, metalness: 0.1, side: THREE.DoubleSide });
47
+ // Basic material cache/reuse
48
  const objectMaterials = {
49
  'wood': new THREE.MeshStandardMaterial({ color: 0x8B4513, roughness: 0.9 }),
50
  'leaf': new THREE.MeshStandardMaterial({ color: 0x228B22, roughness: 0.8 }),
 
54
  'brick': new THREE.MeshStandardMaterial({ color: 0x9B4C43, roughness: 0.85 }),
55
  'metal': new THREE.MeshStandardMaterial({ color: 0xcccccc, roughness: 0.4, metalness: 0.8 }),
56
  'gem': new THREE.MeshStandardMaterial({ color: 0x4FFFFF, roughness: 0.1, metalness: 0.2, transparent: true, opacity: 0.8 }),
57
+ 'light': new THREE.MeshBasicMaterial({ color: 0xFFFF88 }), // For light sources
58
+ // Add more reusable materials
59
  };
60
 
 
 
 
 
 
61
 
62
  function init() {
63
  scene = new THREE.Scene();
64
  scene.background = new THREE.Color(0xabcdef);
65
 
66
  const aspect = window.innerWidth / window.innerHeight;
67
+ camera = new THREE.PerspectiveCamera(60, aspect, 0.1, 5000); // Increased far plane
68
  camera.position.set(plotWidth / 2, 15, plotDepth / 2 + 20);
69
  camera.lookAt(plotWidth/2, 0, plotDepth/2);
70
  scene.add(camera);
71
 
72
  setupLighting();
73
+ // Don't setup initial ground here, wait for WebSocket initial state? Or create base?
74
+ createGroundPlane(0, 0, false); // Create the origin ground at least
75
+ setupPlayer();
76
 
77
  raycaster = new THREE.Raycaster();
78
  mouse = new THREE.Vector2();
 
83
  renderer.shadowMap.type = THREE.PCFSoftShadowMap;
84
  document.body.appendChild(renderer.domElement);
85
 
86
+ // --- Initialize WebSocket Connection ---
87
+ connectWebSocket();
88
+
89
+ // Event Listeners
90
  document.addEventListener('mousemove', onMouseMove, false);
91
+ document.addEventListener('click', onDocumentClick, false); // Place object
 
92
  window.addEventListener('resize', onWindowResize, false);
93
+ document.addEventListener('keydown', onKeyDown);
94
+ document.addEventListener('keyup', onKeyUp);
95
 
96
+ // --- Define global functions needed by Python ---
97
+ window.teleportPlayer = teleportPlayer;
98
+ // Removed getSaveDataAndPosition - saving now server-side via WS
99
+ // Removed resetNewlyPlacedObjects - no longer needed
100
+ window.updateSelectedObjectType = updateSelectedObjectType; // Still needed
101
 
102
+ console.log(`Three.js Initialized for user: ${myUsername}. Connecting to ${websocketUrl}...`);
 
 
 
103
  animate();
104
  }
105
 
106
+ // --- WebSocket Logic ---
107
+ function connectWebSocket() {
108
+ console.log("Attempting WebSocket connection...");
109
+ socket = new WebSocket(websocketUrl);
110
+
111
+ socket.onopen = () => {
112
+ console.log("WebSocket connection established.");
113
+ connectionRetries = 0;
114
+ // Request initial state? Server sends it automatically now.
115
+ // socket.send(JSON.stringify({ type: "request_initial_state" }));
116
+ };
117
+
118
+ socket.onmessage = (event) => {
119
+ try {
120
+ const data = JSON.parse(event.data);
121
+ // console.log("WebSocket message received:", data); // Debugging
122
+ handleWebSocketMessage(data);
123
+ } catch (e) {
124
+ console.error("Failed to parse WebSocket message:", event.data, e);
125
+ }
126
+ };
127
+
128
+ socket.onerror = (error) => {
129
+ console.error("WebSocket error:", error);
130
+ // Consider showing an error message to the user in Streamlit?
131
+ };
132
+
133
+ socket.onclose = (event) => {
134
+ console.warn(`WebSocket connection closed. Code: ${event.code}, Reason: ${event.reason}. Clean: ${event.wasClean}`);
135
+ socket = null;
136
+ // Implement reconnection strategy
137
+ if (connectionRetries < MAX_RETRIES) {
138
+ connectionRetries++;
139
+ const delay = Math.pow(2, connectionRetries) * 1000; // Exponential backoff
140
+ console.log(`Attempting reconnection in ${delay / 1000}s...`);
141
+ setTimeout(connectWebSocket, delay);
142
+ } else {
143
+ console.error("WebSocket reconnection failed after max retries.");
144
+ // Inform user connection lost - maybe via Streamlit call?
145
+ }
146
+ };
147
  }
148
 
149
+ function sendWebSocketMessage(type, payload) {
150
+ if (socket && socket.readyState === WebSocket.OPEN) {
151
+ const message = JSON.stringify({ type, payload });
152
+ socket.send(message);
153
+ } else {
154
+ console.warn("WebSocket not open. Message not sent:", type, payload);
155
+ // Optionally queue messages to send on reconnect?
156
+ }
 
 
 
 
 
 
 
 
 
157
  }
158
 
159
+ function handleWebSocketMessage(data) {
160
+ const { type, payload } = data;
161
+
162
+ switch (type) {
163
+ case "initial_state":
164
+ console.log(`Received initial world state with ${Object.keys(payload).length} objects.`);
165
+ // Clear existing objects (except player?) before loading initial state
166
+ clearWorldObjects();
167
+ for (const obj_id in payload) {
168
+ createAndPlaceObject(payload[obj_id], false); // false = not newly placed
169
+ }
170
+ // Setup ground based on loaded objects' positions? Or separate metadata needed?
171
+ // For now, rely on dynamic ground expansion.
172
+ break;
173
+ case "object_placed":
174
+ console.log(`Object placed by ${payload.username}:`, payload.object_data);
175
+ // Add or update the object in the scene
176
+ createAndPlaceObject(payload.object_data, false); // false = not newly placed by *this* client
177
+ break;
178
+ case "object_deleted":
179
+ console.log(`Object deleted by ${payload.username}:`, payload.obj_id);
180
+ removeObjectById(payload.obj_id);
181
+ break;
182
+ case "user_join":
183
+ console.log(`User joined: ${payload.username} (${payload.id})`);
184
+ // Optionally display user join message in chat tab or 3D world?
185
+ break;
186
+ case "user_leave":
187
+ console.log(`User left: ${payload.username} (${payload.id})`);
188
+ // Optionally display user leave message
189
+ break;
190
+ case "user_rename":
191
+ console.log(`User ${payload.old_username} is now ${payload.new_username}`);
192
+ break;
193
+ case "chat_message":
194
+ console.log(`Chat from ${payload.username}: ${payload.message}`);
195
+ // Handle displaying chat in the Streamlit Chat tab (Python side handles this)
196
+ break;
197
+ // Add handlers for other message types
198
+ default:
199
+ console.warn("Received unknown WebSocket message type:", type);
200
  }
 
 
 
 
201
  }
202
 
203
  function clearWorldObjects() {
204
+ console.log("Clearing existing world objects...");
205
+ for (const [obj_id, mesh] of worldObjects.entries()) {
206
+ scene.remove(mesh);
207
+ // Optional: Dispose geometry/material for memory management
208
+ // disposeObject3D(mesh);
209
+ }
210
+ worldObjects.clear();
211
+ // Also clear ground meshes? Or keep them? Keep for now.
 
 
212
  }
213
 
214
+ function removeObjectById(obj_id) {
215
+ if (worldObjects.has(obj_id)) {
216
+ const mesh = worldObjects.get(obj_id);
217
+ scene.remove(mesh);
218
+ // disposeObject3D(mesh); // Optional cleanup
219
+ worldObjects.delete(obj_id);
220
+ console.log(`Removed object ${obj_id} from scene.`);
221
+ } else {
222
+ console.warn(`Attempted to remove non-existent object ID: ${obj_id}`);
223
+ }
224
  }
225
 
226
+ // --- Standard Setup Functions ---
227
+ function setupLighting() { /* ... (Keep as before) ... */
228
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); scene.add(ambientLight);
229
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2); directionalLight.position.set(75, 150, 100); directionalLight.castShadow = true; directionalLight.shadow.mapSize.width = 2048; directionalLight.shadow.mapSize.height = 2048; directionalLight.shadow.camera.near = 10; directionalLight.shadow.camera.far = 400; directionalLight.shadow.camera.left = -150; directionalLight.shadow.camera.right = 150; directionalLight.shadow.camera.top = 150; directionalLight.shadow.camera.bottom = -150; directionalLight.shadow.bias = -0.002; scene.add(directionalLight);
230
+ const hemiLight = new THREE.HemisphereLight( 0xabcdef, 0x55aa55, 0.5 ); scene.add( hemiLight );
231
+ }
232
+ function setupPlayer() { /* ... (Keep as before) ... */
233
+ const playerGeo = new THREE.CapsuleGeometry(0.4, 0.8, 4, 8); const playerMat = new THREE.MeshStandardMaterial({ color: 0x0055ff, roughness: 0.6 }); playerMesh = new THREE.Mesh(playerGeo, playerMat); playerMesh.position.set(plotWidth / 2, 0.8, plotDepth / 2); playerMesh.castShadow = true; playerMesh.receiveShadow = false; scene.add(playerMesh);
234
+ }
235
+ function createGroundPlane(gridX, gridZ, isPlaceholder) { /* ... (Keep as before) ... */
236
+ const gridKey = `${gridX}_${gridZ}`; if (groundMeshes[gridKey]) return groundMeshes[gridKey];
237
+ const groundGeometry = new THREE.PlaneGeometry(plotWidth, plotDepth); const material = isPlaceholder ? placeholderGroundMaterial : groundMaterial; const groundMesh = new THREE.Mesh(groundGeometry, material); groundMesh.rotation.x = -Math.PI / 2; groundMesh.position.y = -0.05; groundMesh.position.x = gridX * plotWidth + plotWidth / 2.0; groundMesh.position.z = gridZ * plotDepth + plotDepth / 2.0; groundMesh.receiveShadow = true; groundMesh.userData.gridKey = gridKey; groundMesh.userData.isPlaceholder = isPlaceholder; scene.add(groundMesh); groundMeshes[gridKey] = groundMesh; return groundMesh;
238
+ }
239
+
240
+ // --- Object Creation & Placement (Modified for WebSocket & New Primitives) ---
241
+
242
+ // Central function to add/update objects based on data
243
+ function createAndPlaceObject(objData, isNewlyPlacedLocally) { // isNewlyPlacedLocally not really used now
244
+ if (!objData || !objData.obj_id || !objData.type) {
245
  console.warn("Invalid object data:", objData);
246
  return null;
247
  }
248
 
249
+ // Check if object already exists (update vs create)
250
  let mesh = worldObjects.get(objData.obj_id);
251
  let isNew = false;
252
 
253
  if (mesh) {
254
+ // Update existing mesh position/rotation if different
255
+ if (mesh.position.distanceToSquared(objData.position) > 0.001) {
256
+ mesh.position.set(objData.position.x, objData.position.y, objData.position.z);
257
+ }
258
+ if (objData.rotation && (
259
+ Math.abs(mesh.rotation.x - objData.rotation._x) > 0.01 ||
260
+ Math.abs(mesh.rotation.y - objData.rotation._y) > 0.01 ||
261
+ Math.abs(mesh.rotation.z - objData.rotation._z) > 0.01 )) {
262
+ mesh.rotation.set(objData.rotation._x, objData.rotation._y, objData.rotation._z, objData.rotation._order || 'XYZ');
263
+ }
264
+ // Could add logic here to update geometry/material if type changes? Unlikely.
265
+ // console.log(`Updated object ${objData.obj_id}`);
 
 
266
  } else {
267
+ // Create new mesh
268
+ mesh = createPrimitiveMesh(objData.type); // Use the new factory function
269
+ if (!mesh) return null; // Failed to create mesh type
270
+
271
+ isNew = true;
272
+ mesh.userData.obj_id = objData.obj_id; // Assign ID from data
273
+ mesh.userData.type = objData.type;
274
+ mesh.position.set(objData.position.x, objData.position.y, objData.position.z);
275
+ if (objData.rotation) {
276
+ mesh.rotation.set(objData.rotation._x, objData.rotation._y, objData.rotation._z, objData.rotation._order || 'XYZ');
277
+ }
278
+
279
+ scene.add(mesh);
280
+ worldObjects.set(objData.obj_id, mesh); // Add to our map
281
+ // console.log(`Created new object ${objData.obj_id} (${objData.type})`);
 
 
282
  }
283
  return mesh;
284
  }
285
 
286
+ // Factory function for creating meshes based on type name
287
  function createPrimitiveMesh(type) {
 
 
 
 
 
288
  let mesh = null;
289
+ let geometry, material, material2; // Declare vars
290
 
291
+ // Use reusable materials where possible
292
  const wood = objectMaterials.wood;
293
  const leaf = objectMaterials.leaf;
294
  const stone = objectMaterials.stone;
 
299
  const gem = objectMaterials.gem;
300
  const lightMat = objectMaterials.light;
301
 
302
+ try { // Wrap in try-catch for safety if geometry fails
303
  switch(type) {
304
+ // --- Original Primitives ---
305
  case "Tree":
306
  mesh = new THREE.Group();
307
+ geometry = new THREE.CylinderGeometry(0.3, 0.4, 2, 8); material = wood;
308
+ const trunk = new THREE.Mesh(geometry, material); trunk.position.y = 1; trunk.castShadow=true; trunk.receiveShadow=true; mesh.add(trunk);
309
+ geometry = new THREE.IcosahedronGeometry(1.2, 0); material = leaf;
310
+ const canopy = new THREE.Mesh(geometry, material); canopy.position.y = 2.8; canopy.castShadow=true; canopy.receiveShadow=false; mesh.add(canopy);
 
 
 
 
 
 
 
 
 
 
311
  break;
312
  case "Rock":
313
+ geometry = new THREE.IcosahedronGeometry(0.7, 1); material = stone;
314
+ // Optional: Deform geometry slightly (can be slow if done often)
315
+ mesh = new THREE.Mesh(geometry, material); mesh.castShadow = true; mesh.receiveShadow = true;
316
+ mesh.scale.set(1, Math.random()*0.4 + 0.8, 1); // Vary shape slightly
 
 
317
  break;
318
  case "Simple House":
319
  mesh = new THREE.Group();
320
+ geometry = new THREE.BoxGeometry(2, 1.5, 2.5); material = house_wall;
321
+ const body = new THREE.Mesh(geometry, material); body.position.y = 0.75; body.castShadow = true; body.receiveShadow = true; mesh.add(body);
322
+ geometry = new THREE.ConeGeometry(1.8, 1, 4); material = house_roof;
323
+ const roof = new THREE.Mesh(geometry, material); roof.position.y = 1.5 + 0.5; roof.rotation.y = Math.PI / 4; roof.castShadow = true; roof.receiveShadow = false; mesh.add(roof);
324
+ break;
325
+ case "Fence Post": // Keep original simple fence post
326
+ geometry = new THREE.BoxGeometry(0.2, 1.5, 0.2); material = wood;
327
+ mesh = new THREE.Mesh(geometry, material); mesh.position.y = 0.75; mesh.castShadow = true; mesh.receiveShadow = true;
 
 
 
 
 
 
 
328
  break;
329
+
330
+ // --- New Primitives ---
331
+ case "Pine Tree": // Example: Cone for canopy
332
  mesh = new THREE.Group();
333
+ geometry = new THREE.CylinderGeometry(0.2, 0.3, 2.5, 8); material = wood;
334
+ const pineTrunk = new THREE.Mesh(geometry, material); pineTrunk.position.y = 1.25; pineTrunk.castShadow=true; pineTrunk.receiveShadow=true; mesh.add(pineTrunk);
335
+ geometry = new THREE.ConeGeometry(1, 2.5, 8); material = leaf;
336
+ const pineCanopy = new THREE.Mesh(geometry, material); pineCanopy.position.y = 2.5 + (2.5/2) - 0.5; pineCanopy.castShadow=true; pineCanopy.receiveShadow=false; mesh.add(pineCanopy);
 
 
 
 
 
 
 
 
 
 
337
  break;
338
+ case "Brick Wall": // Simple box with brick color
339
+ geometry = new THREE.BoxGeometry(3, 2, 0.3); material = brick;
340
+ mesh = new THREE.Mesh(geometry, material); mesh.position.y = 1; mesh.castShadow = true; mesh.receiveShadow = true;
 
 
 
 
341
  break;
342
  case "Sphere":
343
+ geometry = new THREE.SphereGeometry(0.8, 16, 12); material = metal;
344
+ mesh = new THREE.Mesh(geometry, material); mesh.position.y = 0.8; mesh.castShadow = true; mesh.receiveShadow = true;
 
 
 
 
345
  break;
346
+ case "Cube": // Simple cube
347
+ geometry = new THREE.BoxGeometry(1, 1, 1); material = stone; // Re-use stone
348
+ mesh = new THREE.Mesh(geometry, material); mesh.position.y = 0.5; mesh.castShadow = true; mesh.receiveShadow = true;
 
 
 
 
349
  break;
350
  case "Cylinder":
351
+ geometry = new THREE.CylinderGeometry(0.5, 0.5, 1.5, 16); material = metal;
352
+ mesh = new THREE.Mesh(geometry, material); mesh.position.y = 0.75; mesh.castShadow = true; mesh.receiveShadow = true;
 
 
 
 
353
  break;
354
  case "Cone":
355
+ geometry = new THREE.ConeGeometry(0.6, 1.2, 16); material = house_roof; // Re-use roof
356
+ mesh = new THREE.Mesh(geometry, material); mesh.position.y = 0.6; mesh.castShadow = true; mesh.receiveShadow = true;
 
 
 
 
357
  break;
358
+ case "Torus": // Donut shape
359
+ geometry = new THREE.TorusGeometry(0.6, 0.2, 8, 24); material = gem; // Use gem material
360
+ mesh = new THREE.Mesh(geometry, material); mesh.position.y = 0.7; mesh.castShadow = true; mesh.receiveShadow = true;
361
+ mesh.rotation.x = Math.PI / 2; // Stand it up
 
 
 
 
362
  break;
363
  case "Mushroom":
364
+ mesh = new THREE.Group();
365
+ geometry = new THREE.CylinderGeometry(0.15, 0.1, 0.6, 8); material = house_wall; // Cream stem
366
+ const stem = new THREE.Mesh(geometry, material); stem.position.y = 0.3; stem.castShadow = true; stem.receiveShadow = true; mesh.add(stem);
367
+ geometry = new THREE.SphereGeometry(0.4, 16, 8, 0, Math.PI * 2, 0, Math.PI / 2); material = house_roof; // Red cap
368
+ const cap = new THREE.Mesh(geometry, material); cap.position.y = 0.6; cap.castShadow = true; cap.receiveShadow = false; mesh.add(cap);
369
+ break;
370
+ case "Cactus": // Simple segmented cactus
371
+ mesh = new THREE.Group(); material = leaf; // Green material
 
 
 
 
 
 
 
 
 
 
 
372
  geometry = new THREE.CylinderGeometry(0.3, 0.3, 1.5, 8);
373
+ const main = new THREE.Mesh(geometry, material); main.position.y = 0.75; main.castShadow = true; main.receiveShadow = true; mesh.add(main);
 
 
 
 
374
  geometry = new THREE.CylinderGeometry(0.2, 0.2, 0.8, 8);
375
+ const arm1 = new THREE.Mesh(geometry, material); arm1.position.set(0.3, 1, 0); arm1.rotation.z = Math.PI / 4; arm1.castShadow = true; arm1.receiveShadow = true; mesh.add(arm1);
376
+ const arm2 = new THREE.Mesh(geometry, material); arm2.position.set(-0.3, 0.8, 0); arm2.rotation.z = -Math.PI / 4; arm2.castShadow = true; arm2.receiveShadow = true; mesh.add(arm2);
 
 
 
 
 
 
 
 
 
 
377
  break;
378
  case "Campfire":
379
+ mesh = new THREE.Group();
380
+ material = wood; geometry = new THREE.CylinderGeometry(0.1, 0.1, 0.8, 5);
381
+ const log1 = new THREE.Mesh(geometry, material); log1.rotation.x = Math.PI/2; log1.position.set(0, 0.1, 0.2); mesh.add(log1);
382
+ const log2 = new THREE.Mesh(geometry, material); log2.rotation.set(Math.PI/2, 0, Math.PI/3); log2.position.set(0.2*Math.cos(Math.PI/6), 0.1, -0.2*Math.sin(Math.PI/6)); mesh.add(log2);
383
+ const log3 = new THREE.Mesh(geometry, material); log3.rotation.set(Math.PI/2, 0, -Math.PI/3); log3.position.set(-0.2*Math.cos(Math.PI/6), 0.1, -0.2*Math.sin(Math.PI/6)); mesh.add(log3);
384
+ material2 = lightMat; geometry = new THREE.ConeGeometry(0.2, 0.5, 8); // Simple flame
385
+ const flame = new THREE.Mesh(geometry, material2); flame.position.y = 0.35; mesh.add(flame);
386
+ // Add shadows later if needed
387
+ break;
 
 
 
 
 
 
 
 
 
 
 
 
388
  case "Star":
389
+ geometry = new THREE.SphereGeometry(0.5, 4, 2); // Low poly sphere looks star-like
390
+ material = lightMat;
391
+ mesh = new THREE.Mesh(geometry, material); mesh.position.y = 1;
392
+ break;
 
393
  case "Gem":
394
+ geometry = new THREE.OctahedronGeometry(0.6, 0); material = gem;
395
+ mesh = new THREE.Mesh(geometry, material); mesh.position.y = 0.6; mesh.castShadow = true; mesh.receiveShadow = true;
396
+ break;
397
+ case "Tower": // Simple cylinder tower
398
+ geometry = new THREE.CylinderGeometry(1, 1.2, 5, 8); material = stone;
399
+ mesh = new THREE.Mesh(geometry, material); mesh.position.y = 2.5; mesh.castShadow = true; mesh.receiveShadow = true;
400
+ break;
401
+ case "Barrier": // Simple box barrier
402
+ geometry = new THREE.BoxGeometry(2, 0.5, 0.5); material = metal;
403
+ mesh = new THREE.Mesh(geometry, material); mesh.position.y = 0.25; mesh.castShadow = true; mesh.receiveShadow = true;
404
+ break;
405
+ case "Fountain": // Placeholder: Tiered cylinders
406
+ mesh = new THREE.Group(); material = stone;
407
+ geometry = new THREE.CylinderGeometry(1.5, 1.5, 0.3, 16);
408
+ const baseF = new THREE.Mesh(geometry, material); baseF.position.y = 0.15; mesh.add(baseF);
409
+ geometry = new THREE.CylinderGeometry(0.8, 0.8, 0.5, 16);
410
+ const midF = new THREE.Mesh(geometry, material); midF.position.y = 0.3+0.25; mesh.add(midF);
411
+ geometry = new THREE.CylinderGeometry(0.4, 0.4, 0.7, 16);
412
+ const topF = new THREE.Mesh(geometry, material); topF.position.y = 0.8+0.35; mesh.add(topF);
413
+ mesh.castShadow = true; mesh.receiveShadow = true; // Apply to group?
414
+ break;
415
+ case "Lantern":
416
+ mesh = new THREE.Group(); material = metal;
417
+ geometry = new THREE.BoxGeometry(0.4, 0.6, 0.4);
418
+ const bodyL = new THREE.Mesh(geometry, material); bodyL.position.y = 0.3; mesh.add(bodyL);
419
+ geometry = new THREE.SphereGeometry(0.15); material2 = lightMat;
420
+ const lightL = new THREE.Mesh(geometry, material2); lightL.position.y = 0.3; mesh.add(lightL);
421
+ mesh.castShadow = true; // Group casts shadow?
422
+ break;
423
+ case "Sign Post":
424
+ mesh = new THREE.Group(); material = wood;
425
+ geometry = new THREE.CylinderGeometry(0.05, 0.05, 1.8, 8);
426
+ const postS = new THREE.Mesh(geometry, material); postS.position.y = 0.9; mesh.add(postS);
427
+ geometry = new THREE.BoxGeometry(0.8, 0.4, 0.05);
428
+ const signS = new THREE.Mesh(geometry, material); signS.position.y = 1.5; mesh.add(signS);
429
+ mesh.castShadow = true; mesh.receiveShadow = true;
430
+ break;
431
+
432
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
433
  default:
434
+ console.warn("Unknown primitive type for mesh creation:", type);
435
+ return null; // Return null if type not found
436
  }
437
  } catch (e) {
438
  console.error(`Error creating geometry/mesh for type ${type}:`, e);
439
  return null;
440
  }
441
 
442
+ // Common post-creation steps (if mesh created)
443
  if (mesh) {
444
+ // Set default userData structure (will be overwritten by createAndPlaceObject)
445
  mesh.userData = { type: type };
446
+ // Ensure position is defaulted reasonably if created standalone
447
  if (!mesh.position.y && mesh.geometry) {
448
+ mesh.geometry.computeBoundingBox();
449
+ mesh.position.y = (mesh.geometry.boundingBox.max.y - mesh.geometry.boundingBox.min.y) / 2;
450
  }
451
  }
452
  return mesh;
453
  }
454
 
455
+
456
+ // --- Event Handlers ---
457
+ function onMouseMove(event) { /* ... (Keep as before) ... */
458
+ mouse.x = (event.clientX / window.innerWidth) * 2 - 1; mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
459
  }
460
 
461
  function onDocumentClick(event) {
462
+ if (selectedObjectType === "None" || !selectedObjectType) return;
463
+
464
  const groundCandidates = Object.values(groundMeshes);
465
  if (groundCandidates.length === 0) return;
466
 
 
470
  if (intersects.length > 0) {
471
  const intersectPoint = intersects[0].point;
472
 
473
+ // Prepare object data for the server
474
+ const newObjData = {
475
+ obj_id: THREE.MathUtils.generateUUID(), // Generate unique ID client-side
476
+ type: selectedObjectType,
477
+ position: { x: intersectPoint.x, y: 0, z: intersectPoint.z }, // Base position on ground
478
+ rotation: { _x: 0, _y: Math.random() * Math.PI * 2, _z: 0, _order: 'XYZ' } // Random Y rotation
479
+ };
480
+
481
+ // Adjust Y position based on object type AFTER getting the type
482
+ // This should ideally use the geometry's bounding box, but hardcoding for now
483
+ const tempMesh = createPrimitiveMesh(selectedObjectType); // Create temporarily to get height? Costly.
484
+ if (tempMesh && tempMesh.geometry) {
485
+ tempMesh.geometry.computeBoundingBox();
486
+ const height = tempMesh.geometry.boundingBox.max.y - tempMesh.geometry.boundingBox.min.y;
487
+ // Assume origin is at the center Y for most default geometries
488
+ newObjData.position.y = (height / 2) + intersectPoint.y + 0.01; // Place base slightly above ground
 
 
 
 
 
 
 
 
 
 
 
489
  } else {
490
+ // Fallback if mesh creation failed or no geometry
491
+ newObjData.position.y = 0.5 + intersectPoint.y; // Default lift
 
 
 
 
 
 
 
 
 
 
492
  }
 
 
493
 
494
+ console.log(`Placing ${selectedObjectType} (${newObjData.obj_id}) at`, newObjData.position);
 
 
 
495
 
496
+ // 1. Add object visually immediately (Optimistic Update)
497
+ createAndPlaceObject(newObjData, true); // Mark as locally placed initially? Not needed now.
 
 
 
 
 
 
 
 
498
 
499
+ // 2. Send placement message to server via WebSocket
500
+ sendWebSocketMessage("place_object", {
501
+ username: myUsername,
502
+ object_data: newObjData
503
+ });
504
+
505
+ // 3. No need for local saving (sessionStorage) anymore
 
506
  }
507
  }
508
 
509
+ function onKeyDown(event) { /* ... (Keep as before) ... */ keysPressed[event.code] = true; }
510
+ function onKeyUp(event) { /* ... (Keep as before) ... */ keysPressed[event.code] = false; }
511
+ function onWindowResize() { /* ... (Keep as before) ... */ camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); }
512
+
513
+ // --- Functions called by Python ---
514
+ function teleportPlayer(targetX, targetZ) { /* ... (Keep as before) ... */
515
+ console.log(`JS teleportPlayer called: x=${targetX}, z=${targetZ}`); if (playerMesh) { playerMesh.position.x = targetX; playerMesh.position.z = targetZ; const offset = new THREE.Vector3(0, 15, 20); const targetPosition = playerMesh.position.clone().add(offset); camera.position.copy(targetPosition); camera.lookAt(playerMesh.position); console.log("Player teleported to:", playerMesh.position); } else { console.error("Player mesh not found for teleport."); }
516
+ }
517
+ function updateSelectedObjectType(newType) { // Renamed from previous attempt
518
+ console.log("JS updateSelectedObjectType received:", newType);
519
+ selectedObjectType = newType;
520
+ // Optionally provide visual feedback (e.g., change cursor)
521
  }
522
 
523
+ // --- Animation Loop & Helpers ---
524
+ function updatePlayerMovement() { /* ... (Keep as before, includes checkAndExpandGroundVisuals) ... */
525
+ if (!playerMesh) return; const moveDirection = new THREE.Vector3(0, 0, 0); if (keysPressed['KeyW'] || keysPressed['ArrowUp']) moveDirection.z -= 1; if (keysPressed['KeyS'] || keysPressed['ArrowDown']) moveDirection.z += 1; if (keysPressed['KeyA'] || keysPressed['ArrowLeft']) moveDirection.x -= 1; if (keysPressed['KeyD'] || keysPressed['ArrowRight']) moveDirection.x += 1;
526
+ if (moveDirection.lengthSq() > 0) { const forward = new THREE.Vector3(); camera.getWorldDirection(forward); forward.y = 0; forward.normalize(); const right = new THREE.Vector3().crossVectors(camera.up, forward).normalize(); const worldMove = new THREE.Vector3(); worldMove.add(forward.multiplyScalar(-moveDirection.z)); worldMove.add(right.multiplyScalar(-moveDirection.x)); worldMove.normalize().multiplyScalar(playerSpeed); playerMesh.position.add(worldMove); playerMesh.position.y = Math.max(playerMesh.position.y, 0.8); checkAndExpandGroundVisuals(); }
527
+ }
528
+ function checkAndExpandGroundVisuals() { /* ... (Keep as before) ... */
529
+ if (!playerMesh) return; const currentGridX = Math.floor(playerMesh.position.x / plotWidth); const currentGridZ = Math.floor(playerMesh.position.z / plotDepth); const viewDistanceGrids = 3; // Expand further?
530
+ for (let dx = -viewDistanceGrids; dx <= viewDistanceGrids; dx++) { for (let dz = -viewDistanceGrids; dz <= viewDistanceGrids; dz++) { const checkX = currentGridX + dx; const checkZ = currentGridZ + dz; const gridKey = `${checkX}_${checkZ}`; if (!groundMeshes[gridKey]) { createGroundPlane(checkX, checkZ, true); } } }
531
+ }
532
+ function updateCamera() { /* ... (Keep as before) ... */
533
+ if (!playerMesh) return; const offset = new THREE.Vector3(0, 12, 18); const targetPosition = playerMesh.position.clone().add(offset); camera.position.lerp(targetPosition, 0.08); const lookAtTarget = playerMesh.position.clone().add(new THREE.Vector3(0, 0.5, 0)); camera.lookAt(lookAtTarget);
534
  }
535
 
536
  function animate() {
537
  requestAnimationFrame(animate);
538
+ updatePlayerMovement();
539
+ updateCamera();
540
  renderer.render(scene, camera);
541
  }
542
 
543
+ // --- Start ---
544
+ init();
545
+
546
+ </script>
547
+ </body>
548
+ </html>