Spaces:
Sleeping
Sleeping
| <html> | |
| <head> | |
| <title>Shared World Builder</title> | |
| <style> | |
| body { margin: 0; overflow: hidden; } | |
| canvas { display: block; } | |
| </style> | |
| </head> | |
| <body> | |
| <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'; | |
| let scene, camera, renderer, playerMesh; | |
| let raycaster, mouse; | |
| const keysPressed = {}; | |
| const playerSpeed = 0.15; | |
| // --- World State (Managed by Server via WebSocket) --- | |
| const worldObjects = new Map(); // Map<obj_id, THREE.Object3D> | |
| const groundMeshes = {}; // Map<gridKey, THREE.Mesh> | |
| // --- WebSocket --- | |
| let socket = null; | |
| let connectionRetries = 0; | |
| const MAX_RETRIES = 5; | |
| // --- Access State from Streamlit (Injected) --- | |
| const myUsername = window.USERNAME || `User_${Math.random().toString(36).substring(2, 6)}`; | |
| const websocketUrl = window.WEBSOCKET_URL || "ws://localhost:8765"; | |
| let selectedObjectType = window.SELECTED_OBJECT_TYPE || "None"; // Can be updated | |
| const plotWidth = window.PLOT_WIDTH || 50.0; | |
| const plotDepth = window.PLOT_DEPTH || 50.0; | |
| // --- Materials --- | |
| const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x55aa55, roughness: 0.9, metalness: 0.1, side: THREE.DoubleSide }); | |
| const placeholderGroundMaterial = new THREE.MeshStandardMaterial({ color: 0x448844, roughness: 0.95, metalness: 0.1, side: THREE.DoubleSide }); | |
| // Basic material cache/reuse | |
| const objectMaterials = { | |
| 'wood': new THREE.MeshStandardMaterial({ color: 0x8B4513, roughness: 0.9 }), | |
| 'leaf': new THREE.MeshStandardMaterial({ color: 0x228B22, roughness: 0.8 }), | |
| 'stone': new THREE.MeshStandardMaterial({ color: 0xaaaaaa, roughness: 0.8, metalness: 0.1 }), | |
| 'house_wall': new THREE.MeshStandardMaterial({ color: 0xffccaa, roughness: 0.8 }), | |
| 'house_roof': new THREE.MeshStandardMaterial({ color: 0xaa5533, roughness: 0.7 }), | |
| 'brick': new THREE.MeshStandardMaterial({ color: 0x9B4C43, roughness: 0.85 }), | |
| 'metal': new THREE.MeshStandardMaterial({ color: 0xcccccc, roughness: 0.4, metalness: 0.8 }), | |
| 'gem': new THREE.MeshStandardMaterial({ color: 0x4FFFFF, roughness: 0.1, metalness: 0.2, transparent: true, opacity: 0.8 }), | |
| 'light': new THREE.MeshBasicMaterial({ color: 0xFFFF88 }), // For light sources | |
| // Add more reusable materials | |
| }; | |
| function init() { | |
| scene = new THREE.Scene(); | |
| scene.background = new THREE.Color(0xabcdef); | |
| const aspect = window.innerWidth / window.innerHeight; | |
| camera = new THREE.PerspectiveCamera(60, aspect, 0.1, 5000); // Increased far plane | |
| camera.position.set(plotWidth / 2, 15, plotDepth / 2 + 20); | |
| camera.lookAt(plotWidth/2, 0, plotDepth/2); | |
| scene.add(camera); | |
| setupLighting(); | |
| // Don't setup initial ground here, wait for WebSocket initial state? Or create base? | |
| createGroundPlane(0, 0, false); // Create the origin ground at least | |
| setupPlayer(); | |
| raycaster = new THREE.Raycaster(); | |
| mouse = new THREE.Vector2(); | |
| renderer = new THREE.WebGLRenderer({ antialias: true }); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| renderer.shadowMap.enabled = true; | |
| renderer.shadowMap.type = THREE.PCFSoftShadowMap; | |
| document.body.appendChild(renderer.domElement); | |
| // --- Initialize WebSocket Connection --- | |
| connectWebSocket(); | |
| // Event Listeners | |
| document.addEventListener('mousemove', onMouseMove, false); | |
| document.addEventListener('click', onDocumentClick, false); // Place object | |
| window.addEventListener('resize', onWindowResize, false); | |
| document.addEventListener('keydown', onKeyDown); | |
| document.addEventListener('keyup', onKeyUp); | |
| // --- Define global functions needed by Python --- | |
| window.teleportPlayer = teleportPlayer; | |
| // Removed getSaveDataAndPosition - saving now server-side via WS | |
| // Removed resetNewlyPlacedObjects - no longer needed | |
| window.updateSelectedObjectType = updateSelectedObjectType; // Still needed | |
| console.log(`Three.js Initialized for user: ${myUsername}. Connecting to ${websocketUrl}...`); | |
| animate(); | |
| } | |
| // --- WebSocket Logic --- | |
| function connectWebSocket() { | |
| console.log("Attempting WebSocket connection..."); | |
| socket = new WebSocket(websocketUrl); | |
| socket.onopen = () => { | |
| console.log("WebSocket connection established."); | |
| connectionRetries = 0; | |
| // Request initial state? Server sends it automatically now. | |
| // socket.send(JSON.stringify({ type: "request_initial_state" })); | |
| }; | |
| socket.onmessage = (event) => { | |
| try { | |
| const data = JSON.parse(event.data); | |
| // console.log("WebSocket message received:", data); // Debugging | |
| handleWebSocketMessage(data); | |
| } catch (e) { | |
| console.error("Failed to parse WebSocket message:", event.data, e); | |
| } | |
| }; | |
| socket.onerror = (error) => { | |
| console.error("WebSocket error:", error); | |
| // Consider showing an error message to the user in Streamlit? | |
| }; | |
| socket.onclose = (event) => { | |
| console.warn(`WebSocket connection closed. Code: ${event.code}, Reason: ${event.reason}. Clean: ${event.wasClean}`); | |
| socket = null; | |
| // Implement reconnection strategy | |
| if (connectionRetries < MAX_RETRIES) { | |
| connectionRetries++; | |
| const delay = Math.pow(2, connectionRetries) * 1000; // Exponential backoff | |
| console.log(`Attempting reconnection in ${delay / 1000}s...`); | |
| setTimeout(connectWebSocket, delay); | |
| } else { | |
| console.error("WebSocket reconnection failed after max retries."); | |
| // Inform user connection lost - maybe via Streamlit call? | |
| } | |
| }; | |
| } | |
| function sendWebSocketMessage(type, payload) { | |
| if (socket && socket.readyState === WebSocket.OPEN) { | |
| const message = JSON.stringify({ type, payload }); | |
| socket.send(message); | |
| } else { | |
| console.warn("WebSocket not open. Message not sent:", type, payload); | |
| // Optionally queue messages to send on reconnect? | |
| } | |
| } | |
| function handleWebSocketMessage(data) { | |
| const { type, payload } = data; | |
| switch (type) { | |
| case "initial_state": | |
| console.log(`Received initial world state with ${Object.keys(payload).length} objects.`); | |
| // Clear existing objects (except player?) before loading initial state | |
| clearWorldObjects(); | |
| for (const obj_id in payload) { | |
| createAndPlaceObject(payload[obj_id], false); // false = not newly placed | |
| } | |
| // Setup ground based on loaded objects' positions? Or separate metadata needed? | |
| // For now, rely on dynamic ground expansion. | |
| break; | |
| case "object_placed": | |
| console.log(`Object placed by ${payload.username}:`, payload.object_data); | |
| // Add or update the object in the scene | |
| createAndPlaceObject(payload.object_data, false); // false = not newly placed by *this* client | |
| break; | |
| case "object_deleted": | |
| console.log(`Object deleted by ${payload.username}:`, payload.obj_id); | |
| removeObjectById(payload.obj_id); | |
| break; | |
| case "user_join": | |
| console.log(`User joined: ${payload.username} (${payload.id})`); | |
| // Optionally display user join message in chat tab or 3D world? | |
| break; | |
| case "user_leave": | |
| console.log(`User left: ${payload.username} (${payload.id})`); | |
| // Optionally display user leave message | |
| break; | |
| case "user_rename": | |
| console.log(`User ${payload.old_username} is now ${payload.new_username}`); | |
| break; | |
| case "chat_message": | |
| console.log(`Chat from ${payload.username}: ${payload.message}`); | |
| // Handle displaying chat in the Streamlit Chat tab (Python side handles this) | |
| break; | |
| // Add handlers for other message types | |
| default: | |
| console.warn("Received unknown WebSocket message type:", type); | |
| } | |
| } | |
| function clearWorldObjects() { | |
| console.log("Clearing existing world objects..."); | |
| for (const [obj_id, mesh] of worldObjects.entries()) { | |
| scene.remove(mesh); | |
| // Optional: Dispose geometry/material for memory management | |
| // disposeObject3D(mesh); | |
| } | |
| worldObjects.clear(); | |
| // Also clear ground meshes? Or keep them? Keep for now. | |
| } | |
| function removeObjectById(obj_id) { | |
| if (worldObjects.has(obj_id)) { | |
| const mesh = worldObjects.get(obj_id); | |
| scene.remove(mesh); | |
| // disposeObject3D(mesh); // Optional cleanup | |
| worldObjects.delete(obj_id); | |
| console.log(`Removed object ${obj_id} from scene.`); | |
| } else { | |
| console.warn(`Attempted to remove non-existent object ID: ${obj_id}`); | |
| } | |
| } | |
| // --- Standard Setup Functions --- | |
| function setupLighting() { /* ... (Keep as before) ... */ | |
| const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); scene.add(ambientLight); | |
| 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); | |
| const hemiLight = new THREE.HemisphereLight( 0xabcdef, 0x55aa55, 0.5 ); scene.add( hemiLight ); | |
| } | |
| function setupPlayer() { /* ... (Keep as before) ... */ | |
| 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); | |
| } | |
| function createGroundPlane(gridX, gridZ, isPlaceholder) { /* ... (Keep as before) ... */ | |
| const gridKey = `${gridX}_${gridZ}`; if (groundMeshes[gridKey]) return groundMeshes[gridKey]; | |
| 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; | |
| } | |
| // --- Object Creation & Placement (Modified for WebSocket & New Primitives) --- | |
| // Central function to add/update objects based on data | |
| function createAndPlaceObject(objData, isNewlyPlacedLocally) { // isNewlyPlacedLocally not really used now | |
| if (!objData || !objData.obj_id || !objData.type) { | |
| console.warn("Invalid object data:", objData); | |
| return null; | |
| } | |
| // Check if object already exists (update vs create) | |
| let mesh = worldObjects.get(objData.obj_id); | |
| let isNew = false; | |
| if (mesh) { | |
| // Update existing mesh position/rotation if different | |
| if (mesh.position.distanceToSquared(objData.position) > 0.001) { | |
| mesh.position.set(objData.position.x, objData.position.y, objData.position.z); | |
| } | |
| if (objData.rotation && ( | |
| Math.abs(mesh.rotation.x - objData.rotation._x) > 0.01 || | |
| Math.abs(mesh.rotation.y - objData.rotation._y) > 0.01 || | |
| Math.abs(mesh.rotation.z - objData.rotation._z) > 0.01 )) { | |
| mesh.rotation.set(objData.rotation._x, objData.rotation._y, objData.rotation._z, objData.rotation._order || 'XYZ'); | |
| } | |
| // Could add logic here to update geometry/material if type changes? Unlikely. | |
| // console.log(`Updated object ${objData.obj_id}`); | |
| } else { | |
| // Create new mesh | |
| mesh = createPrimitiveMesh(objData.type); // Use the new factory function | |
| if (!mesh) return null; // Failed to create mesh type | |
| isNew = true; | |
| mesh.userData.obj_id = objData.obj_id; // Assign ID from data | |
| mesh.userData.type = objData.type; | |
| mesh.position.set(objData.position.x, objData.position.y, objData.position.z); | |
| if (objData.rotation) { | |
| mesh.rotation.set(objData.rotation._x, objData.rotation._y, objData.rotation._z, objData.rotation._order || 'XYZ'); | |
| } | |
| scene.add(mesh); | |
| worldObjects.set(objData.obj_id, mesh); // Add to our map | |
| // console.log(`Created new object ${objData.obj_id} (${objData.type})`); | |
| } | |
| return mesh; | |
| } | |
| // Factory function for creating meshes based on type name | |
| function createPrimitiveMesh(type) { | |
| let mesh = null; | |
| let geometry, material, material2; // Declare vars | |
| // Use reusable materials where possible | |
| const wood = objectMaterials.wood; | |
| const leaf = objectMaterials.leaf; | |
| const stone = objectMaterials.stone; | |
| const house_wall = objectMaterials.house_wall; | |
| const house_roof = objectMaterials.house_roof; | |
| const brick = objectMaterials.brick; | |
| const metal = objectMaterials.metal; | |
| const gem = objectMaterials.gem; | |
| const lightMat = objectMaterials.light; | |
| try { // Wrap in try-catch for safety if geometry fails | |
| switch(type) { | |
| // --- Original Primitives --- | |
| case "Tree": | |
| mesh = new THREE.Group(); | |
| geometry = new THREE.CylinderGeometry(0.3, 0.4, 2, 8); material = wood; | |
| const trunk = new THREE.Mesh(geometry, material); trunk.position.y = 1; trunk.castShadow=true; trunk.receiveShadow=true; mesh.add(trunk); | |
| geometry = new THREE.IcosahedronGeometry(1.2, 0); material = leaf; | |
| const canopy = new THREE.Mesh(geometry, material); canopy.position.y = 2.8; canopy.castShadow=true; canopy.receiveShadow=false; mesh.add(canopy); | |
| break; | |
| case "Rock": | |
| geometry = new THREE.IcosahedronGeometry(0.7, 1); material = stone; | |
| // Optional: Deform geometry slightly (can be slow if done often) | |
| mesh = new THREE.Mesh(geometry, material); mesh.castShadow = true; mesh.receiveShadow = true; | |
| mesh.scale.set(1, Math.random()*0.4 + 0.8, 1); // Vary shape slightly | |
| break; | |
| case "Simple House": | |
| mesh = new THREE.Group(); | |
| geometry = new THREE.BoxGeometry(2, 1.5, 2.5); material = house_wall; | |
| const body = new THREE.Mesh(geometry, material); body.position.y = 0.75; body.castShadow = true; body.receiveShadow = true; mesh.add(body); | |
| geometry = new THREE.ConeGeometry(1.8, 1, 4); material = house_roof; | |
| 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); | |
| break; | |
| case "Fence Post": // Keep original simple fence post | |
| geometry = new THREE.BoxGeometry(0.2, 1.5, 0.2); material = wood; | |
| mesh = new THREE.Mesh(geometry, material); mesh.position.y = 0.75; mesh.castShadow = true; mesh.receiveShadow = true; | |
| break; | |
| // --- New Primitives --- | |
| case "Pine Tree": // Example: Cone for canopy | |
| mesh = new THREE.Group(); | |
| geometry = new THREE.CylinderGeometry(0.2, 0.3, 2.5, 8); material = wood; | |
| const pineTrunk = new THREE.Mesh(geometry, material); pineTrunk.position.y = 1.25; pineTrunk.castShadow=true; pineTrunk.receiveShadow=true; mesh.add(pineTrunk); | |
| geometry = new THREE.ConeGeometry(1, 2.5, 8); material = leaf; | |
| 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); | |
| break; | |
| case "Brick Wall": // Simple box with brick color | |
| geometry = new THREE.BoxGeometry(3, 2, 0.3); material = brick; | |
| mesh = new THREE.Mesh(geometry, material); mesh.position.y = 1; mesh.castShadow = true; mesh.receiveShadow = true; | |
| break; | |
| case "Sphere": | |
| geometry = new THREE.SphereGeometry(0.8, 16, 12); material = metal; | |
| mesh = new THREE.Mesh(geometry, material); mesh.position.y = 0.8; mesh.castShadow = true; mesh.receiveShadow = true; | |
| break; | |
| case "Cube": // Simple cube | |
| geometry = new THREE.BoxGeometry(1, 1, 1); material = stone; // Re-use stone | |
| mesh = new THREE.Mesh(geometry, material); mesh.position.y = 0.5; mesh.castShadow = true; mesh.receiveShadow = true; | |
| break; | |
| case "Cylinder": | |
| geometry = new THREE.CylinderGeometry(0.5, 0.5, 1.5, 16); material = metal; | |
| mesh = new THREE.Mesh(geometry, material); mesh.position.y = 0.75; mesh.castShadow = true; mesh.receiveShadow = true; | |
| break; | |
| case "Cone": | |
| geometry = new THREE.ConeGeometry(0.6, 1.2, 16); material = house_roof; // Re-use roof | |
| mesh = new THREE.Mesh(geometry, material); mesh.position.y = 0.6; mesh.castShadow = true; mesh.receiveShadow = true; | |
| break; | |
| case "Torus": // Donut shape | |
| geometry = new THREE.TorusGeometry(0.6, 0.2, 8, 24); material = gem; // Use gem material | |
| mesh = new THREE.Mesh(geometry, material); mesh.position.y = 0.7; mesh.castShadow = true; mesh.receiveShadow = true; | |
| mesh.rotation.x = Math.PI / 2; // Stand it up | |
| break; | |
| case "Mushroom": | |
| mesh = new THREE.Group(); | |
| geometry = new THREE.CylinderGeometry(0.15, 0.1, 0.6, 8); material = house_wall; // Cream stem | |
| const stem = new THREE.Mesh(geometry, material); stem.position.y = 0.3; stem.castShadow = true; stem.receiveShadow = true; mesh.add(stem); | |
| geometry = new THREE.SphereGeometry(0.4, 16, 8, 0, Math.PI * 2, 0, Math.PI / 2); material = house_roof; // Red cap | |
| const cap = new THREE.Mesh(geometry, material); cap.position.y = 0.6; cap.castShadow = true; cap.receiveShadow = false; mesh.add(cap); | |
| break; | |
| case "Cactus": // Simple segmented cactus | |
| mesh = new THREE.Group(); material = leaf; // Green material | |
| geometry = new THREE.CylinderGeometry(0.3, 0.3, 1.5, 8); | |
| const main = new THREE.Mesh(geometry, material); main.position.y = 0.75; main.castShadow = true; main.receiveShadow = true; mesh.add(main); | |
| geometry = new THREE.CylinderGeometry(0.2, 0.2, 0.8, 8); | |
| 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); | |
| 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); | |
| break; | |
| case "Campfire": | |
| mesh = new THREE.Group(); | |
| material = wood; geometry = new THREE.CylinderGeometry(0.1, 0.1, 0.8, 5); | |
| const log1 = new THREE.Mesh(geometry, material); log1.rotation.x = Math.PI/2; log1.position.set(0, 0.1, 0.2); mesh.add(log1); | |
| 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); | |
| 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); | |
| material2 = lightMat; geometry = new THREE.ConeGeometry(0.2, 0.5, 8); // Simple flame | |
| const flame = new THREE.Mesh(geometry, material2); flame.position.y = 0.35; mesh.add(flame); | |
| // Add shadows later if needed | |
| break; | |
| case "Star": | |
| geometry = new THREE.SphereGeometry(0.5, 4, 2); // Low poly sphere looks star-like | |
| material = lightMat; | |
| mesh = new THREE.Mesh(geometry, material); mesh.position.y = 1; | |
| break; | |
| case "Gem": | |
| geometry = new THREE.OctahedronGeometry(0.6, 0); material = gem; | |
| mesh = new THREE.Mesh(geometry, material); mesh.position.y = 0.6; mesh.castShadow = true; mesh.receiveShadow = true; | |
| break; | |
| case "Tower": // Simple cylinder tower | |
| geometry = new THREE.CylinderGeometry(1, 1.2, 5, 8); material = stone; | |
| mesh = new THREE.Mesh(geometry, material); mesh.position.y = 2.5; mesh.castShadow = true; mesh.receiveShadow = true; | |
| break; | |
| case "Barrier": // Simple box barrier | |
| geometry = new THREE.BoxGeometry(2, 0.5, 0.5); material = metal; | |
| mesh = new THREE.Mesh(geometry, material); mesh.position.y = 0.25; mesh.castShadow = true; mesh.receiveShadow = true; | |
| break; | |
| case "Fountain": // Placeholder: Tiered cylinders | |
| mesh = new THREE.Group(); material = stone; | |
| geometry = new THREE.CylinderGeometry(1.5, 1.5, 0.3, 16); | |
| const baseF = new THREE.Mesh(geometry, material); baseF.position.y = 0.15; mesh.add(baseF); | |
| geometry = new THREE.CylinderGeometry(0.8, 0.8, 0.5, 16); | |
| const midF = new THREE.Mesh(geometry, material); midF.position.y = 0.3+0.25; mesh.add(midF); | |
| geometry = new THREE.CylinderGeometry(0.4, 0.4, 0.7, 16); | |
| const topF = new THREE.Mesh(geometry, material); topF.position.y = 0.8+0.35; mesh.add(topF); | |
| mesh.castShadow = true; mesh.receiveShadow = true; // Apply to group? | |
| break; | |
| case "Lantern": | |
| mesh = new THREE.Group(); material = metal; | |
| geometry = new THREE.BoxGeometry(0.4, 0.6, 0.4); | |
| const bodyL = new THREE.Mesh(geometry, material); bodyL.position.y = 0.3; mesh.add(bodyL); | |
| geometry = new THREE.SphereGeometry(0.15); material2 = lightMat; | |
| const lightL = new THREE.Mesh(geometry, material2); lightL.position.y = 0.3; mesh.add(lightL); | |
| mesh.castShadow = true; // Group casts shadow? | |
| break; | |
| case "Sign Post": | |
| mesh = new THREE.Group(); material = wood; | |
| geometry = new THREE.CylinderGeometry(0.05, 0.05, 1.8, 8); | |
| const postS = new THREE.Mesh(geometry, material); postS.position.y = 0.9; mesh.add(postS); | |
| geometry = new THREE.BoxGeometry(0.8, 0.4, 0.05); | |
| const signS = new THREE.Mesh(geometry, material); signS.position.y = 1.5; mesh.add(signS); | |
| mesh.castShadow = true; mesh.receiveShadow = true; | |
| break; | |
| default: | |
| console.warn("Unknown primitive type for mesh creation:", type); | |
| return null; // Return null if type not found | |
| } | |
| } catch (e) { | |
| console.error(`Error creating geometry/mesh for type ${type}:`, e); | |
| return null; | |
| } | |
| // Common post-creation steps (if mesh created) | |
| if (mesh) { | |
| // Set default userData structure (will be overwritten by createAndPlaceObject) | |
| mesh.userData = { type: type }; | |
| // Ensure position is defaulted reasonably if created standalone | |
| if (!mesh.position.y && mesh.geometry) { | |
| mesh.geometry.computeBoundingBox(); | |
| mesh.position.y = (mesh.geometry.boundingBox.max.y - mesh.geometry.boundingBox.min.y) / 2; | |
| } | |
| } | |
| return mesh; | |
| } | |
| // --- Event Handlers --- | |
| function onMouseMove(event) { /* ... (Keep as before) ... */ | |
| mouse.x = (event.clientX / window.innerWidth) * 2 - 1; mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; | |
| } | |
| function onDocumentClick(event) { | |
| if (selectedObjectType === "None" || !selectedObjectType) return; | |
| const groundCandidates = Object.values(groundMeshes); | |
| if (groundCandidates.length === 0) return; | |
| raycaster.setFromCamera(mouse, camera); | |
| const intersects = raycaster.intersectObjects(groundCandidates); | |
| if (intersects.length > 0) { | |
| const intersectPoint = intersects[0].point; | |
| // Prepare object data for the server | |
| const newObjData = { | |
| obj_id: THREE.MathUtils.generateUUID(), // Generate unique ID client-side | |
| type: selectedObjectType, | |
| position: { x: intersectPoint.x, y: 0, z: intersectPoint.z }, // Base position on ground | |
| rotation: { _x: 0, _y: Math.random() * Math.PI * 2, _z: 0, _order: 'XYZ' } // Random Y rotation | |
| }; | |
| // Adjust Y position based on object type AFTER getting the type | |
| // This should ideally use the geometry's bounding box, but hardcoding for now | |
| const tempMesh = createPrimitiveMesh(selectedObjectType); // Create temporarily to get height? Costly. | |
| if (tempMesh && tempMesh.geometry) { | |
| tempMesh.geometry.computeBoundingBox(); | |
| const height = tempMesh.geometry.boundingBox.max.y - tempMesh.geometry.boundingBox.min.y; | |
| // Assume origin is at the center Y for most default geometries | |
| newObjData.position.y = (height / 2) + intersectPoint.y + 0.01; // Place base slightly above ground | |
| } else { | |
| // Fallback if mesh creation failed or no geometry | |
| newObjData.position.y = 0.5 + intersectPoint.y; // Default lift | |
| } | |
| console.log(`Placing ${selectedObjectType} (${newObjData.obj_id}) at`, newObjData.position); | |
| // 1. Add object visually immediately (Optimistic Update) | |
| createAndPlaceObject(newObjData, true); // Mark as locally placed initially? Not needed now. | |
| // 2. Send placement message to server via WebSocket | |
| sendWebSocketMessage("place_object", { | |
| username: myUsername, | |
| object_data: newObjData | |
| }); | |
| // 3. No need for local saving (sessionStorage) anymore | |
| } | |
| } | |
| function onKeyDown(event) { /* ... (Keep as before) ... */ keysPressed[event.code] = true; } | |
| function onKeyUp(event) { /* ... (Keep as before) ... */ keysPressed[event.code] = false; } | |
| function onWindowResize() { /* ... (Keep as before) ... */ camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); } | |
| // --- Functions called by Python --- | |
| function teleportPlayer(targetX, targetZ) { /* ... (Keep as before) ... */ | |
| 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."); } | |
| } | |
| function updateSelectedObjectType(newType) { // Renamed from previous attempt | |
| console.log("JS updateSelectedObjectType received:", newType); | |
| selectedObjectType = newType; | |
| // Optionally provide visual feedback (e.g., change cursor) | |
| } | |
| // --- Animation Loop & Helpers --- | |
| function updatePlayerMovement() { /* ... (Keep as before, includes checkAndExpandGroundVisuals) ... */ | |
| 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; | |
| 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(); } | |
| } | |
| function checkAndExpandGroundVisuals() { /* ... (Keep as before) ... */ | |
| if (!playerMesh) return; const currentGridX = Math.floor(playerMesh.position.x / plotWidth); const currentGridZ = Math.floor(playerMesh.position.z / plotDepth); const viewDistanceGrids = 3; // Expand further? | |
| 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); } } } | |
| } | |
| function updateCamera() { /* ... (Keep as before) ... */ | |
| 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); | |
| } | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| updatePlayerMovement(); | |
| updateCamera(); | |
| renderer.render(scene, camera); | |
| } | |
| // --- Start --- | |
| init(); | |
| </script> | |
| </body> | |
| </html> |