Spaces:
Sleeping
Sleeping
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Shared 3D World Builder</title> | |
<style> | |
body { margin: 0; overflow: hidden; } | |
canvas { display: block; } | |
#info { position: absolute; top: 10px; left: 10px; color: white; background: rgba(0,0,0,0.7); padding: 10px; } | |
</style> | |
</head> | |
<body> | |
<div id="info">Use WASD to move, mouse to look, click to place object, right-click to delete.</div> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script> | |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/controls/PointerLockControls.js"></script> | |
<script> | |
let scene, camera, renderer, controls, websocket, selectedObjectType = 'None', worldObjects = {}; | |
let moveForward = false, moveBackward = false, moveLeft = false, moveRight = false; | |
const username = window.USERNAME || 'Anonymous'; | |
const websocketUrl = window.WEBSOCKET_URL || 'ws://localhost:8765'; | |
const plotWidth = window.PLOT_WIDTH || 50.0; | |
const plotDepth = window.PLOT_DEPTH || 50.0; | |
function init() { | |
scene = new THREE.Scene(); | |
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); | |
renderer = new THREE.WebGLRenderer(); | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
document.body.appendChild(renderer.domElement); | |
controls = new THREE.PointerLockControls(camera, document.body); | |
scene.add(controls.getObject()); | |
camera.position.y = 1.6; | |
const groundGeometry = new THREE.PlaneGeometry(plotWidth, plotDepth); | |
const groundMaterial = new THREE.MeshBasicMaterial({ color: 0x888888 }); | |
const ground = new THREE.Mesh(groundGeometry, groundMaterial); | |
ground.rotation.x = -Math.PI / 2; | |
scene.add(ground); | |
const gridHelper = new THREE.GridHelper(plotWidth, plotWidth / 2); | |
scene.add(gridHelper); | |
document.addEventListener('click', () => controls.lock()); | |
document.addEventListener('keydown', onKeyDown); | |
document.addEventListener('keyup', onKeyUp); | |
document.addEventListener('mousedown', onMouseDown); | |
window.addEventListener('resize', () => { | |
camera.aspect = window.innerWidth / window.innerHeight; | |
camera.updateProjectionMatrix(); | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
}); | |
initWebSocket(); | |
animate(); | |
} | |
function initWebSocket() { | |
websocket = new WebSocket(websocketUrl); | |
websocket.onopen = () => console.log('WebSocket connected'); | |
websocket.onmessage = (event) => { | |
const data = JSON.parse(event.data); | |
if (data.type === 'initial_state' || data.type === 'object_placed') { | |
const objects = data.type === 'initial_state' ? data.payload : { [data.payload.object_data.obj_id]: data.payload.object_data }; | |
for (let obj_id in objects) { | |
if (!worldObjects[obj_id]) { | |
worldObjects[obj_id] = objects[obj_id]; | |
addObjectToScene(objects[obj_id]); | |
} | |
} | |
} else if (data.type === 'object_deleted') { | |
if (worldObjects[data.payload.obj_id]) { | |
removeObjectFromScene(data.payload.obj_id); | |
delete worldObjects[data.payload.obj_id]; | |
} | |
} else if (data.type === 'player_moved') { | |
// Handle player movement visualization if needed | |
} else if (data.type === 'user_join' || data.type === 'user_leave' || data.type === 'user_rename') { | |
console.log(`${data.payload.username} ${data.type === 'user_join' ? 'joined' : data.type === 'user_leave' ? 'left' : 'renamed to ' + data.payload.new_username}`); | |
} | |
}; | |
websocket.onclose = () => console.log('WebSocket closed'); | |
websocket.onerror = (error) => console.error('WebSocket error:', error); | |
} | |
function updateSelectedObjectType(newType) { | |
selectedObjectType = newType; | |
console.log(`Tool changed to: ${newType}`); | |
} | |
function addObjectToScene(objData) { | |
let geometry, material, mesh; | |
switch (objData.type) { | |
case 'Cube': | |
geometry = new THREE.BoxGeometry(1, 1, 1); | |
material = new THREE.MeshBasicMaterial({ color: 0x00ff00 }); | |
break; | |
case 'Sphere': | |
geometry = new THREE.SphereGeometry(0.5, 32, 32); | |
material = new THREE.MeshBasicMaterial({ color: 0xff0000 }); | |
break; | |
case 'Cylinder': | |
geometry = new THREE.CylinderGeometry(0.5, 0.5, 1, 32); | |
material = new THREE.MeshBasicMaterial({ color: 0x0000ff }); | |
break; | |
case 'Cone': | |
geometry = new THREE.ConeGeometry(0.5, 1, 32); | |
material = new THREE.MeshBasicMaterial({ color: 0xffff00 }); | |
break; | |
case 'Torus': | |
geometry = new THREE.TorusGeometry(0.4, 0.1, 16, 100); | |
material = new THREE.MeshBasicMaterial({ color: 0xff00ff }); | |
break; | |
case 'Tree': | |
geometry = new THREE.ConeGeometry(0.5, 2, 8); | |
material = new THREE.MeshBasicMaterial({ color: 0x228B22 }); | |
break; | |
case 'Rock': | |
geometry = new THREE.DodecahedronGeometry(0.5); | |
material = new THREE.MeshBasicMaterial({ color: 0x808080 }); | |
break; | |
case 'Simple House': | |
geometry = new THREE.BoxGeometry(2, 1.5, 2); | |
material = new THREE.MeshBasicMaterial({ color: 0xA52A2A }); | |
break; | |
case 'Pine Tree': | |
geometry = new THREE.ConeGeometry(0.3, 1.5, 8); | |
material = new THREE.MeshBasicMaterial({ color: 0x006400 }); | |
break; | |
case 'Brick Wall': | |
geometry = new THREE.BoxGeometry(3, 1, 0.2); | |
material = new THREE.MeshBasicMaterial({ color: 0x8B4513 }); | |
break; | |
case 'Mushroom': | |
geometry = new THREE.SphereGeometry(0.5, 32, 16, 0, Math.PI); | |
material = new THREE.MeshBasicMaterial({ color: 0xF5F5DC }); | |
break; | |
case 'Cactus': | |
geometry = new THREE.CylinderGeometry(0.2, 0.2, 1.5, 8); | |
material = new THREE.MeshBasicMaterial({ color: 0x2E8B57 }); | |
break; | |
case 'Campfire': | |
geometry = new THREE.DodecahedronGeometry(0.3); | |
material = new THREE.MeshBasicMaterial({ color: 0xFF4500 }); | |
break; | |
case 'Star': | |
geometry = new THREE.SphereGeometry(0.3, 4, 2); | |
material = new THREE.MeshBasicMaterial({ color: 0xFFFF00 }); | |
break; | |
case 'Gem': | |
geometry = new THREE.OctahedronGeometry(0.4); | |
material = new THREE.MeshBasicMaterial({ color: 0x00CED1 }); | |
break; | |
case 'Tower': | |
geometry = new THREE.CylinderGeometry(0.3, 0.5, 2, 8); | |
material = new THREE.MeshBasicMaterial({ color: 0x708090 }); | |
break; | |
case 'Barrier': | |
geometry = new THREE.BoxGeometry(2, 0.5, 0.2); | |
material = new THREE.MeshBasicMaterial({ color: 0x696969 }); | |
break; | |
case 'Fountain': | |
geometry = new THREE.CylinderGeometry(0.5, 0.3, 1, 32); | |
material = new THREE.MeshBasicMaterial({ color: 0x4682B4 }); | |
break; | |
case 'Lantern': | |
geometry = new THREE.BoxGeometry(0.3, 0.5, 0.3); | |
material = new THREE.MeshBasicMaterial({ color: 0xFFD700 }); | |
break; | |
case 'Sign Post': | |
geometry = new THREE.BoxGeometry(0.2, 1, 0.2); | |
material = new THREE.MeshBasicMaterial({ color: 0x8B4513 }); | |
break; | |
default: | |
return; | |
} | |
mesh = new THREE.Mesh(geometry, material); | |
mesh.position.set(objData.position.x, objData.position.y, objData.position.z); | |
mesh.rotation.set(objData.rotation.x, objData.rotation.y, objData.rotation.z); | |
mesh.name = objData.obj_id; | |
scene.add(mesh); | |
} | |
function removeObjectFromScene(obj_id) { | |
const object = scene.getObjectByName(obj_id); | |
if (object) { | |
scene.remove(object); | |
} | |
} | |
function onKeyDown(event) { | |
switch (event.code) { | |
case 'KeyW': moveForward = true; break; | |
case 'KeyS': moveBackward = true; break; | |
case 'KeyA': moveLeft = true; break; | |
case 'KeyD': moveRight = true; break; | |
} | |
} | |
function onKeyUp(event) { | |
switch (event.code) { | |
case 'KeyW': moveForward = false; break; | |
case 'KeyS': moveBackward = false; break; | |
case 'KeyA': moveLeft = false; break; | |
case 'KeyD': moveRight = false; break; | |
} | |
} | |
function onMouseDown(event) { | |
if (!controls.isLocked) return; | |
if (event.button === 0 && selectedObjectType !== 'None') { | |
const raycaster = new THREE.Raycaster(); | |
const mouse = new THREE.Vector2(0, 0); | |
raycaster.setFromCamera(mouse, camera); | |
const intersects = raycaster.intersectObjects(scene.children); | |
for (let intersect of intersects) { | |
if (intersect.object !== controls.getObject()) { | |
const position = intersect.point; | |
position.y += 0.5; | |
placeObject(position); | |
break; | |
} | |
} | |
} else if (event.button === 2) { | |
const raycaster = new THREE.Raycaster(); | |
const mouse = new THREE.Vector2(0, 0); | |
raycaster.setFromCamera(mouse, camera); | |
const intersects = raycaster.intersectObjects(scene.children); | |
for (let intersect of intersects) { | |
if (intersect.object !== controls.getObject() && intersect.object.name) { | |
websocket.send(JSON.stringify({ | |
type: 'delete_object', | |
payload: { obj_id: intersect.object.name, username } | |
})); | |
break; | |
} | |
} | |
} | |
} | |
function placeObject(position) { | |
if (selectedObjectType === 'None') return; | |
const obj_id = `obj_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`; | |
const objectData = { | |
obj_id: obj_id, | |
type: selectedObjectType, | |
position: { x: position.x, y: position.y, z: position.z }, | |
rotation: { x: 0, y: 0, z: 0 } | |
}; | |
websocket.send(JSON.stringify({ | |
type: 'place_object', | |
payload: { username, object_data: objectData } | |
})); | |
} | |
function animate() { | |
requestAnimationFrame(animate); | |
const delta = 0.1; | |
const velocity = new THREE.Vector3(); | |
if (moveForward) velocity.z -= 0.1; | |
if (moveBackward) velocity.z += 0.1; | |
if (moveLeft) velocity.x -= 0.1; | |
if (moveRight) velocity.x += 0.1; | |
controls.getObject().position.add(velocity); | |
const pos = controls.getObject().position; | |
const rot = controls.getObject().rotation; | |
websocket.send(JSON.stringify({ | |
type: 'player_position', | |
payload: { | |
username, | |
position: { x: pos.x, y: pos.y, z: pos.z }, | |
rotation: { x: rot.x, y: rot.y, z: rot.z } | |
} | |
})); | |
renderer.render(scene, camera); | |
} | |
init(); | |
</script> | |
</body> | |
</html> |