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>