Spaces:
Sleeping
Sleeping
File size: 19,907 Bytes
06b17d1 4788bb9 06b17d1 d1944fb 84a18e0 06b17d1 d1944fb 4788bb9 d1944fb 06b17d1 4788bb9 046998c d1944fb 06b17d1 4788bb9 84a18e0 d1944fb 4788bb9 06b17d1 d1944fb 06b17d1 d1944fb 06b17d1 d1944fb 046998c 06b17d1 d1944fb 06b17d1 d1944fb 06b17d1 d1944fb 06b17d1 4788bb9 06b17d1 d1944fb 4788bb9 d1944fb 4788bb9 d1944fb 4788bb9 d1944fb 4788bb9 d1944fb |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 |
<!DOCTYPE html>
<html>
<head>
<title>Three.js Synced World (DB Backend)</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';
// --- Variables ---
let scene, camera, renderer, playerMesh;
let raycaster, mouse;
const keysPressed = {};
const playerSpeed = 0.15;
let newlyPlacedObjects = []; // For sessionStorage
const placeholderPlots = new Set();
const groundMeshes = {};
const allRenderedObjects = {}; // Tracks all current objects by ID
const SESSION_STORAGE_KEY = 'unsavedDbWorldState_v2';
// --- State from Python ---
const allInitialObjects = window.ALL_INITIAL_OBJECTS || [];
const plotsMetadata = window.PLOTS_METADATA || [];
const selectedObjectType = window.SELECTED_OBJECT_TYPE || "None";
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 });
// --- Initialization ---
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, 4000);
camera.position.set(plotWidth / 2, 15, plotDepth / 2 + 20); // Start looking at first plot
camera.lookAt(plotWidth / 2, 0, plotDepth/2);
scene.add(camera);
setupLighting();
setupInitialGround(); // Creates ground based on plotsMetadata
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);
loadInitialObjects(); // Loads objects from DB data
restoreUnsavedState(); // Loads unsaved from sessionStorage
// Event Listeners
document.addEventListener('mousemove', onMouseMove, false);
document.addEventListener('click', onDocumentClick, false);
window.addEventListener('resize', onWindowResize, false);
document.addEventListener('keydown', onKeyDown);
document.addEventListener('keyup', onKeyUp);
// Define global functions for Python
window.teleportPlayer = teleportPlayer;
window.getSaveDataAndPosition = getSaveDataAndPosition;
console.log("Three.js Initialized (DB Backend v2). World ready.");
animate();
}
// --- Setup Functions ---
function setupLighting() { const a=new THREE.AmbientLight(0xffffff,0.5); scene.add(a); const d=new THREE.DirectionalLight(0xffffff,1.0); d.position.set(50,150,100); d.castShadow=true; d.shadow.mapSize.width=4096; d.shadow.mapSize.height=4096; d.shadow.camera.near=0.5; d.shadow.camera.far=500; d.shadow.camera.left=-150; d.shadow.camera.right=150; d.shadow.camera.top=150; d.shadow.camera.bottom=-150; d.shadow.bias=-0.001; scene.add(d); }
function setupInitialGround() { plotsMetadata.forEach(p => {createGroundPlane(p.grid_x,p.grid_z,false);}); if(plotsMetadata.length===0) {createGroundPlane(0,0,false);} }
function createGroundPlane(gx,gz,isPlaceholder) { const k=`${gx}_${gz}`; if(groundMeshes[k]) return; const geo=new THREE.PlaneGeometry(plotWidth,plotDepth); const mat=isPlaceholder?placeholderGroundMaterial:groundMaterial; const mesh=new THREE.Mesh(geo,mat); mesh.rotation.x=-Math.PI/2; mesh.position.y=-0.05; mesh.position.x=gx*plotWidth+plotWidth/2; mesh.position.z=gz*plotDepth+plotDepth/2; mesh.receiveShadow=true; mesh.userData.gridKey=k; scene.add(mesh); groundMeshes[k]=mesh; if(isPlaceholder){placeholderPlots.add(k);} }
function setupPlayer() { const g=new THREE.CapsuleGeometry(0.4,0.8,4,8); const m=new THREE.MeshStandardMaterial({color:0x0000ff,roughness:0.6}); playerMesh=new THREE.Mesh(g,m); playerMesh.position.set(plotWidth/2, 0.8, plotDepth/2); playerMesh.castShadow=true; playerMesh.receiveShadow=true; scene.add(playerMesh); }
// --- Object Loading / State Management ---
function loadInitialObjects() { console.log(`Loading ${allInitialObjects.length} initial objects.`); clearAllRenderedObjects(); allInitialObjects.forEach(d => { createAndPlaceObject(d, false); }); console.log("Finished initial load."); }
function clearAllRenderedObjects() { Object.values(allRenderedObjects).forEach(o => { if(o.parent) o.parent.remove(o); /* Remove safely */ }); for (const k in allRenderedObjects) delete allRenderedObjects[k]; newlyPlacedObjects = []; }
function createAndPlaceObject(objData, isNewObjectForSession) { let obj=null; switch(objData.type){case "Simple House":obj=createSimpleHouse();break; case "Tree":obj=createTree();break; case "Rock":obj=createRock();break; case "Fence Post":obj=createFencePost();break; default: return null;} if(obj){ obj.userData.obj_id = objData.obj_id || obj.userData.obj_id; if(allRenderedObjects[obj.userData.obj_id]){console.warn(`Duplicate obj ID load skipped: ${obj.userData.obj_id}`); return null;} if(objData.position&&objData.position.x!==undefined){obj.position.set(objData.position.x,objData.position.y,objData.position.z);} else if(objData.pos_x!==undefined){obj.position.set(objData.pos_x,objData.pos_y,objData.pos_z);} else {obj.position.set(0,0.5,0);} if(objData.rotation){obj.rotation.set(objData.rotation._x,objData.rotation._y,objData.rotation._z,objData.rotation._order||'XYZ');} else if(objData.rot_x!==undefined){obj.rotation.set(objData.rot_x,objData.rot_y,objData.rot_z,objData.rot_order||'XYZ');} scene.add(obj); allRenderedObjects[obj.userData.obj_id]=obj; if(isNewObjectForSession){newlyPlacedObjects.push(obj);} return obj; } return null; }
function saveUnsavedState() { try { const d = newlyPlacedObjects.map(o => ({obj_id:o.userData.obj_id, type:o.userData.type, position:{x:o.position.x,y:o.position.y,z:o.position.z}, rotation:{_x:o.rotation.x,_y:o.rotation.y,_z:o.rotation.z,_order:o.rotation.order}})); sessionStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(d)); } catch(e) { console.error("Session save error:", e); } }
function restoreUnsavedState() { try { const s=sessionStorage.getItem(SESSION_STORAGE_KEY); if(s) { const d=JSON.parse(s); if(Array.isArray(d)) { let c=0; d.forEach(o => { if(createAndPlaceObject(o, true)) c++;}); console.log(`Restored ${c} unsaved objects.`); } } } catch(e) { console.error("Session restore error:", e); sessionStorage.removeItem(SESSION_STORAGE_KEY); } }
// --- Object Creation Primitives ---
function createObjectBase(type) { return { userData: { type: type, obj_id: THREE.MathUtils.generateUUID() } }; }
function createSimpleHouse() { const base = createObjectBase("Simple House"); const group = new THREE.Group(); Object.assign(group, base); const mat1=new THREE.MeshStandardMaterial({color:0xffccaa,roughness:0.8}), mat2=new THREE.MeshStandardMaterial({color:0xaa5533,roughness:0.7}); const m1=new THREE.Mesh(new THREE.BoxGeometry(2,1.5,2.5),mat1); m1.position.y=0.75;m1.castShadow=true;m1.receiveShadow=true;group.add(m1); const m2=new THREE.Mesh(new THREE.ConeGeometry(1.8,1,4),mat2); m2.position.y=1.5+0.5;m2.rotation.y=Math.PI/4;m2.castShadow=true;m2.receiveShadow=true;group.add(m2); return group; }
function createTree() { const base=createObjectBase("Tree"); const group=new THREE.Group(); Object.assign(group,base); const mat1=new THREE.MeshStandardMaterial({color:0x8B4513,roughness:0.9}), mat2=new THREE.MeshStandardMaterial({color:0x228B22,roughness:0.8}); const m1=new THREE.Mesh(new THREE.CylinderGeometry(0.3,0.4,2,8),mat1); m1.position.y=1; m1.castShadow=true;m1.receiveShadow=true;group.add(m1); const m2=new THREE.Mesh(new THREE.IcosahedronGeometry(1.2,0),mat2); m2.position.y=2.8; m2.castShadow=true;m2.receiveShadow=true;group.add(m2); return group; }
function createRock() { const base=createObjectBase("Rock"); const mat=new THREE.MeshStandardMaterial({color:0xaaaaaa,roughness:0.8,metalness:0.1}); const rock=new THREE.Mesh(new THREE.IcosahedronGeometry(0.7,0),mat); Object.assign(rock,base); rock.position.y=0.35; rock.rotation.set(Math.random()*Math.PI, Math.random()*Math.PI, 0); rock.castShadow=true;rock.receiveShadow=true; return rock; }
function createFencePost() { // Continuing from where it cut off
const base=createObjectBase("Fence Post");
const mat=new THREE.MeshStandardMaterial({color:0xdeb887, roughness:0.9}); // BurlyWood color
const post=new THREE.Mesh(new THREE.BoxGeometry(0.2, 1.5, 0.2), mat);
Object.assign(post, base); // Add userData to the mesh itself
post.position.y= 1.5 / 2; // Position base at y=0
post.castShadow=true;
post.receiveShadow=true;
return post;
}
// --- Event Handlers ---
function onMouseMove(event) {
// Update mouse vector for raycasting
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
}
function onDocumentClick(event) {
if (selectedObjectType === "None") return; // Don't place if 'None' selected
// Determine which ground mesh(es) to check for intersection
const groundCandidates = Object.values(groundMeshes);
if (groundCandidates.length === 0) {
console.warn("No ground exists to place objects on.");
return;
}
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(groundCandidates); // Check all ground planes
if (intersects.length > 0) {
// Found an intersection point on a ground plane
const intersectPoint = intersects[0].point;
let newObjectToPlace = null;
// Create the selected object type
switch (selectedObjectType) {
case "Simple House": newObjectToPlace = createSimpleHouse(); break;
case "Tree": newObjectToPlace = createTree(); break;
case "Rock": newObjectToPlace = createRock(); break;
case "Fence Post": newObjectToPlace = createFencePost(); break;
default: console.warn("Attempted to place unknown object type:", selectedObjectType); return;
}
if (newObjectToPlace) {
// Position the new object at the click point
newObjectToPlace.position.copy(intersectPoint);
// Adjust Y position slightly so it's definitely above the ground plane
newObjectToPlace.position.y = Math.max(0.01, newObjectToPlace.position.y);
scene.add(newObjectToPlace);
// Add to tracked lists
allRenderedObjects[newObjectToPlace.userData.obj_id] = newObjectToPlace;
newlyPlacedObjects.push(newObjectToPlace);
// Save the updated unsaved state to sessionStorage
saveUnsavedState();
console.log(`Placed new ${selectedObjectType}. Total rendered: ${Object.keys(allRenderedObjects).length}, Unsaved in session: ${newlyPlacedObjects.length}`);
}
}
}
function onKeyDown(event) { keysPressed[event.code] = true; }
function onKeyUp(event) { keysPressed[event.code] = false; }
// --- Functions called by Python ---
function teleportPlayer(targetX, targetZ) {
console.log(`JS teleportPlayer called: Target X=${targetX}, Z=${targetZ}`);
if (playerMesh) {
playerMesh.position.set(targetX, playerMesh.position.y, targetZ); // Set X and Z
// Instantly snap camera to new player position
const offset = new THREE.Vector3(0, 15, 20); // Camera offset
const cameraTargetPosition = playerMesh.position.clone().add(offset);
camera.position.copy(cameraTargetPosition);
camera.lookAt(playerMesh.position); // Look at player
console.log("Player teleported to:", playerMesh.position);
} else {
console.error("Player mesh not found for teleport.");
}
}
function getSaveDataAndPosition() {
if (!playerMesh) {
console.error("Player mesh missing, cannot determine save plot.");
return JSON.stringify({ playerPosition: {x:0,y:0,z:0}, objectsToSave: [] });
}
const playerPos = { x: playerMesh.position.x, y: playerMesh.position.y, z: playerMesh.position.z };
const currentGridX = Math.floor(playerPos.x / plotWidth);
const currentGridZ = Math.floor(playerPos.z / plotDepth);
const minX = currentGridX * plotWidth, maxX = minX + plotWidth;
const minZ = currentGridZ * plotDepth, maxZ = minZ + plotDepth;
console.log(`getSaveData: Player in grid [${currentGridX}, ${currentGridZ}]. Filtering objects within X:[${minX.toFixed(1)},${maxX.toFixed(1)}), Z:[${minZ.toFixed(1)},${maxZ.toFixed(1)})`);
// Filter ALL currently rendered objects to find those within these boundaries
const objectsInPlot = Object.values(allRenderedObjects).filter(obj =>
obj.position.x >= minX && obj.position.x < maxX &&
obj.position.z >= minZ && obj.position.z < maxZ
).map(obj => { // Serialize the filtered objects
if (!obj.userData || !obj.userData.type || !obj.userData.obj_id) {
console.warn("Skipping object with missing user data during save serialization:", obj);
return null;
}
const rotation = { _x: obj.rotation.x, _y: obj.rotation.y, _z: obj.rotation.z, _order: obj.rotation.order };
return { // Send WORLD coordinates
obj_id: obj.userData.obj_id, type: obj.userData.type,
position: { x: obj.position.x, y: obj.position.y, z: obj.position.z },
rotation: rotation
};
}).filter(obj => obj !== null); // Filter out any nulls from warnings
const payload = {
playerPosition: playerPos,
objectsToSave: objectsInPlot // Contains all relevant objects for the current plot
};
console.log(`Prepared payload with ${objectsInPlot.length} objects for saving plot (${currentGridX},${currentGridZ}).`);
return JSON.stringify(payload);
}
// --- Animation Loop ---
function updatePlayerMovement() {
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)); // W/S moves along camera forward/backward
worldMove.add(right.multiplyScalar(-moveDirection.x)); // A/D moves along camera right/left
worldMove.normalize().multiplyScalar(playerSpeed);
playerMesh.position.add(worldMove);
playerMesh.position.y = Math.max(0.8, playerMesh.position.y); // Keep player base y near 0.8
checkAndExpandGround(); // Check if new ground needs to be created visually
}
}
function checkAndExpandGround() {
if (!playerMesh) return;
const currentGridX = Math.floor(playerMesh.position.x / plotWidth);
const currentGridZ = Math.floor(playerMesh.position.z / plotDepth);
// Check immediate neighbors and one step further maybe? Check radius 1 for now.
for (let dx = -1; dx <= 1; dx++) {
for (let dz = -1; dz <= 1; dz++) {
// if (dx === 0 && dz === 0) continue; // Also check current cell just in case
const checkX = currentGridX + dx;
const checkZ = currentGridZ + dz;
const gridKey = `${checkX}_${checkZ}`;
// If no ground mesh exists for this grid cell yet...
if (!groundMeshes[gridKey]) {
// Check if it corresponds to a SAVED plot (metadata from Python)
const isSavedPlot = plotsMetadata.some(plot => plot.grid_x === checkX && plot.grid_z === checkZ);
// If it's NOT a saved plot, create a visual placeholder
if (!isSavedPlot) {
createGroundPlane(checkX, checkZ, true); // true = is placeholder
}
// If it IS a saved plot but the mesh is missing (e.g. after clear), recreate it
// This shouldn't happen often with current logic but adds robustness
else {
createGroundPlane(checkX, checkZ, false);
}
}
}
}
}
function updateCamera() {
if (!playerMesh) return;
const offset = new THREE.Vector3(0, 15, 20); // Fixed third-person offset
const targetPosition = playerMesh.position.clone().add(offset);
// Smoothly interpolate camera position
camera.position.lerp(targetPosition, 0.08);
// Always look at the player's current position
camera.lookAt(playerMesh.position);
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function animate() {
requestAnimationFrame(animate);
updatePlayerMovement(); // Includes ground expansion check
updateCamera();
renderer.render(scene, camera);
}
// --- Start the application ---
init();
</script>
</body>
</html> |