Happy-Valley-Game / index.html
awacke1's picture
Update index.html
d1944fb verified
raw
history blame
19.9 kB
<!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>