Happy-Valley-Game / index.html
awacke1's picture
Update index.html
aa5ba8e verified
raw
history blame
30.9 kB
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Shared 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">Click to place selected object or move player, right-click to delete.</div>
<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;
let raycaster, mouse;
let worldObjects = new Map();
let groundMeshes = {};
// Access State from Streamlit
const myUsername = window.USERNAME || `User_${Math.random().toString(36).substring(2, 6)}`;
let selectedObjectType = window.SELECTED_OBJECT_TYPE || "None";
const plotWidth = window.PLOT_WIDTH || 50.0;
const plotDepth = window.PLOT_DEPTH || 50.0;
const worldState = window.WORLD_STATE || { objects: {}, players: {}, action_history: [] };
// 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 });
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 })
};
const VALID_OBJECT_TYPES = [
"Tree", "Rock", "Simple House", "Pine Tree", "Brick Wall", "Sphere", "Cube",
"Cylinder", "Cone", "Torus", "Mushroom", "Cactus", "Campfire", "Star", "Gem",
"Tower", "Barrier", "Fountain", "Lantern", "Sign Post"
];
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);
camera.position.set(plotWidth / 2, 15, plotDepth / 2 + 20);
camera.lookAt(plotWidth/2, 0, plotDepth/2);
scene.add(camera);
setupLighting();
createGroundPlane(0, 0, false);
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);
document.addEventListener('mousemove', onMouseMove, false);
document.addEventListener('click', onDocumentClick, false);
document.addEventListener('contextmenu', onRightClick, false);
window.addEventListener('resize', onWindowResize, false);
window.updateSelectedObjectType = updateSelectedObjectType;
// Initialize scene with world state
loadWorldState();
console.log(`Three.js Initialized for user: ${myUsername}`);
animate();
}
function setupLighting() {
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 createGroundPlane(gridX, gridZ, isPlaceholder) {
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;
groundMesh.name = 'ground';
scene.add(groundMesh);
groundMeshes[gridKey] = groundMesh;
return groundMesh;
}
function loadWorldState() {
clearWorldObjects();
const objects = worldState.objects || {};
console.log(`Loading ${Object.keys(objects).length} objects from WORLD_STATE`);
for (const obj_id in objects) {
const objData = objects[obj_id];
if (!objData || !objData.obj_id || !objData.type || !VALID_OBJECT_TYPES.includes(objData.type) || !objData.position) {
console.warn(`Skipping invalid object: ${obj_id}`, objData);
continue;
}
const mesh = createAndPlaceObject(objData, false);
if (!mesh) {
console.warn(`Failed to create mesh for object: ${obj_id}`, objData);
}
}
const players = worldState.players || {};
console.log(`Loading ${Object.keys(players).length} players from WORLD_STATE`);
for (const username in players) {
const pos = players[username].position;
if (pos && typeof pos.x === 'number' && typeof pos.y === 'number' && typeof pos.z === 'number') {
createPlayerMarker(username, pos);
} else {
console.warn(`Invalid player position for ${username}:`, pos);
}
}
const actionHistory = worldState.action_history || [];
console.log(`Loaded ${actionHistory.length} action history entries`);
}
function clearWorldObjects() {
console.log("Clearing existing world objects...");
for (const [obj_id, mesh] of worldObjects.entries()) {
scene.remove(mesh);
}
worldObjects.clear();
scene.children.forEach(child => {
if (child.userData.isPlayerMarker) {
scene.remove(child);
}
});
}
function createPlayerMarker(username, position) {
const geometry = new THREE.SphereGeometry(0.3, 8, 8);
const material = new THREE.MeshBasicMaterial({ color: username === myUsername ? 0x0000ff : 0xff0000 });
const marker = new THREE.Mesh(geometry, material);
marker.position.set(position.x, position.y + 0.5, position.z);
marker.userData.isPlayerMarker = true;
marker.userData.username = username;
scene.add(marker);
console.log(`Created player marker for ${username} at`, position);
}
function createAndPlaceObject(objData, isNewlyPlacedLocally) {
if (!objData || !objData.obj_id || !objData.type || !objData.position) {
console.warn("Invalid object data:", objData);
return null;
}
let mesh = worldObjects.get(objData.obj_id);
let isNew = false;
if (mesh) {
if (mesh.position.distanceToSquared(new THREE.Vector3(objData.position.x, objData.position.y, objData.position.z)) > 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)) > 0.01 ||
Math.abs(mesh.rotation.y - (objData.rotation._y || 0)) > 0.01 ||
Math.abs(mesh.rotation.z - (objData.rotation._z || 0)) > 0.01 )) {
mesh.rotation.set(
objData.rotation._x || 0,
objData.rotation._y || 0,
objData.rotation._z || 0,
objData.rotation._order || 'XYZ'
);
}
} else {
mesh = createPrimitiveMesh(objData.type);
if (!mesh) return null;
isNew = true;
mesh.userData.obj_id = objData.obj_id;
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 || 0,
objData.rotation._y || 0,
objData.rotation._z || 0,
objData.rotation._order || 'XYZ'
);
}
scene.add(mesh);
worldObjects.set(objData.obj_id, mesh);
console.log(`Created new object ${objData.obj_id} (${objData.type}) at`, objData.position);
}
return mesh;
}
function createPrimitiveMesh(type) {
if (!VALID_OBJECT_TYPES.includes(type)) {
console.warn(`Invalid object type for mesh creation: ${type}`);
return null;
}
let mesh = null;
let geometry, material, material2;
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 {
switch(type) {
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;
mesh = new THREE.Mesh(geometry, material);
mesh.castShadow = true;
mesh.receiveShadow = true;
mesh.scale.set(1, Math.random()*0.4 + 0.8, 1);
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 "Pine Tree":
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":
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":
geometry = new THREE.BoxGeometry(1, 1, 1);
material = 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;
mesh = new THREE.Mesh(geometry, material);
mesh.position.y = 0.6;
mesh.castShadow = true;
mesh.receiveShadow = true;
break;
case "Torus":
geometry = new THREE.TorusGeometry(0.6, 0.2, 8, 24);
material = gem;
mesh = new THREE.Mesh(geometry, material);
mesh.position.y = 0.7;
mesh.castShadow = true;
mesh.receiveShadow = true;
mesh.rotation.x = Math.PI / 2;
break;
case "Mushroom":
mesh = new THREE.Group();
geometry = new THREE.CylinderGeometry(0.15, 0.1, 0.6, 8);
material = house_wall;
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;
const cap = new THREE.Mesh(geometry, material);
cap.position.y = 0.6;
cap.castShadow = true;
cap.receiveShadow = false;
mesh.add(cap);
break;
case "Cactus":
mesh = new THREE.Group();
material = leaf;
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);
const flame = new THREE.Mesh(geometry, material2);
flame.position.y = 0.35;
mesh.add(flame);
break;
case "Star":
geometry = new THREE.SphereGeometry(0.5, 4, 2);
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":
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":
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":
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;
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;
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("Unexpected object type:", type);
return null;
}
} catch (e) {
console.error(`Error creating geometry/mesh for type ${type}:`, e);
return null;
}
if (mesh) {
mesh.userData = { type: type };
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;
}
function onMouseMove(event) {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
}
function onDocumentClick(event) {
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;
if (selectedObjectType !== "None" && selectedObjectType) {
// Place object
const newObjData = {
obj_id: THREE.MathUtils.generateUUID(),
type: selectedObjectType,
position: { x: intersectPoint.x, y: 0, z: intersectPoint.z },
rotation: { _x: 0, _y: Math.random() * Math.PI * 2, _z: 0, _order: 'XYZ' }
};
const tempMesh = createPrimitiveMesh(selectedObjectType);
if (tempMesh && tempMesh.geometry) {
tempMesh.geometry.computeBoundingBox();
const height = tempMesh.geometry.boundingBox.max.y - tempMesh.geometry.boundingBox.min.y;
newObjData.position.y = (height / 2) + intersectPoint.y + 0.01;
} else {
newObjData.position.y = 0.5 + intersectPoint.y;
}
console.log(`Placing ${selectedObjectType} (${newObjData.obj_id}) at`, newObjData.position);
const mesh = createAndPlaceObject(newObjData, true);
if (mesh) {
window.parent.postMessage({
type: 'place_object',
payload: { username: myUsername, object_data: newObjData }
}, '*');
}
} else {
// Move player
const newPosition = {
x: intersectPoint.x,
y: 0.5,
z: intersectPoint.z
};
console.log(`Moving player ${myUsername} to`, newPosition);
window.parent.postMessage({
type: 'move_player',
payload: { username: myUsername, position: newPosition }
}, '*');
}
}
}
function onRightClick(event) {
event.preventDefault();
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(Array.from(worldObjects.values()));
if (intersects.length > 0) {
const obj_id = intersects[0].object.userData.obj_id;
console.log(`Deleting object ${obj_id}`);
removeObjectById(obj_id);
window.parent.postMessage({
type: 'delete_object',
payload: { username: myUsername, obj_id: obj_id }
}, '*');
}
}
function removeObjectById(obj_id) {
if (worldObjects.has(obj_id)) {
const mesh = worldObjects.get(obj_id);
scene.remove(mesh);
worldObjects.delete(obj_id);
console.log(`Removed object ${obj_id} from scene.`);
} else {
console.warn(`Attempted to remove non-existent object ID: ${obj_id}`);
}
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function updateSelectedObjectType(newType) {
console.log("JS updateSelectedObjectType received:", newType);
selectedObjectType = newType;
}
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
// Listen for messages from Streamlit
window.addEventListener('message', (event) => {
const data = event.data;
if (data.type === 'place_object') {
const mesh = createAndPlaceObject(data.payload.object_data, false);
if (!mesh) {
console.warn(`Failed to place object from message:`, data.payload.object_data);
}
} else if (data.type === 'delete_object') {
removeObjectById(data.payload.obj_id);
} else if (data.type === 'move_player') {
const username = data.payload.username;
const position = data.payload.position;
scene.children.forEach(child => {
if (child.userData.isPlayerMarker && child.userData.username === username) {
child.position.set(position.x, position.y + 0.5, position.z);
console.log(`Updated player marker for ${username} to`, position);
}
});
}
});
init();
</script>
</body>
</html>