Happy-Valley-Game / index.html
awacke1's picture
Update index.html
006833b verified
raw
history blame
15.6 kB
<!DOCTYPE html>
<html>
<head>
<title>Three.js World Builder</title>
<style>
body { margin: 0; overflow: hidden; }
canvas { display: block; }
/* Style for the save button */
#saveButton {
position: absolute;
top: 10px;
left: 10px;
padding: 10px 15px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
z-index: 10; /* Ensure it's above the canvas */
}
#saveButton:hover {
background-color: #45a049;
}
</style>
</head>
<body>
<button id="saveButton">💾 Save Work</button>
<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, groundMesh, playerMesh;
let raycaster, mouse;
const placedObjects = [];
const keysPressed = {}; // Track key presses for smooth movement
const playerSpeed = 0.15;
// --- Access State from Streamlit (set via injected script) ---
const selectedObjectType = window.SELECTED_OBJECT_TYPE || "None";
const initialObjects = window.INITIAL_OBJECTS || [];
const currentSpaceId = window.CURRENT_SPACE_ID || null;
const currentSpaceName = window.CURRENT_SPACE_NAME || ""; // Get name for save redirect
function init() {
scene = new THREE.Scene();
scene.background = new THREE.Color(0xabcdef);
const aspect = window.innerWidth / window.innerHeight;
const viewSize = 30;
// Using PerspectiveCamera now for better depth perception with movement
camera = new THREE.PerspectiveCamera(60, aspect, 0.1, 1000);
camera.position.set(0, 15, 20); // Position behind and above origin
camera.lookAt(0, 0, 0);
scene.add(camera);
setupLighting();
setupGround();
setupPlayer(); // Create player representation
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);
// --- Load Initial Objects ---
loadInitialObjects();
// --- 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);
document.getElementById('saveButton').addEventListener('click', onSaveClick);
animate();
}
function setupLighting() {
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0);
directionalLight.position.set(15, 30, 20);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = 2048;
directionalLight.shadow.mapSize.height = 2048;
directionalLight.shadow.camera.near = 0.5;
directionalLight.shadow.camera.far = 100;
directionalLight.shadow.camera.left = -30;
directionalLight.shadow.camera.right = 30;
directionalLight.shadow.camera.top = 30;
directionalLight.shadow.camera.bottom = -30;
directionalLight.shadow.bias = -0.001;
scene.add(directionalLight);
}
function setupGround() {
const groundGeometry = new THREE.PlaneGeometry(50, 50); // Size matches constant in Python
const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x55aa55, roughness: 0.9, metalness: 0.1 });
groundMesh = new THREE.Mesh(groundGeometry, groundMaterial);
groundMesh.rotation.x = -Math.PI / 2;
groundMesh.position.y = -0.05;
groundMesh.receiveShadow = true;
scene.add(groundMesh);
}
function setupPlayer() {
// Simple Capsule or Box primitive for player
const playerGeo = new THREE.CapsuleGeometry(0.4, 0.8, 4, 8); // Radius, Height
const playerMat = new THREE.MeshStandardMaterial({ color: 0x0000ff, roughness: 0.6 });
playerMesh = new THREE.Mesh(playerGeo, playerMat);
playerMesh.position.set(0, 0.4 + 0.8/2, 5); // Start position (Y adjusted for capsule base)
playerMesh.castShadow = true;
playerMesh.receiveShadow = true;
scene.add(playerMesh);
}
function loadInitialObjects() {
console.log("Loading initial objects:", initialObjects);
initialObjects.forEach(objData => {
let loadedObject = null;
switch (objData.type) {
case "Simple House": loadedObject = createSimpleHouse(); break;
case "Tree": loadedObject = createTree(); break;
case "Rock": loadedObject = createRock(); break;
case "Fence Post": loadedObject = createFencePost(); break;
// Add cases for any other types you save
}
if (loadedObject && objData.position) {
// Apply saved position and rotation
loadedObject.position.set(objData.position.x, objData.position.y, objData.position.z);
if (objData.rotation) {
loadedObject.rotation.set(objData.rotation._x, objData.rotation._y, objData.rotation._z, objData.rotation._order);
} else if (objData.quaternion) { // Handle if saved as quaternion
loadedObject.quaternion.set(objData.quaternion._x, objData.quaternion._y, objData.quaternion._z, objData.quaternion._w);
}
scene.add(loadedObject);
placedObjects.push(loadedObject); // Track loaded objects
}
});
}
// --- Object Creation Functions (Keep these from previous step) ---
function createSimpleHouse() { /* ... copy from previous ... */
const group = new THREE.Group();
const mainMaterial = new THREE.MeshStandardMaterial({ color: 0xffccaa, roughness: 0.8 });
const roofMaterial = new THREE.MeshStandardMaterial({ color: 0xaa5533, roughness: 0.7 });
const base = new THREE.Mesh(new THREE.BoxGeometry(2, 1.5, 2.5), mainMaterial);
base.position.y = 1.5 / 2; base.castShadow = true; base.receiveShadow = true; group.add(base);
const roof = new THREE.Mesh(new THREE.ConeGeometry(1.8, 1, 4), roofMaterial);
roof.position.y = 1.5 + 1 / 2; roof.rotation.y = Math.PI / 4; roof.castShadow = true; roof.receiveShadow = true; group.add(roof);
group.userData.type = "Simple House"; // Store type for saving
return group;
}
function createTree() { /* ... copy from previous ... */
const group = new THREE.Group();
const trunkMaterial = new THREE.MeshStandardMaterial({ color: 0x8B4513, roughness: 0.9 });
const leavesMaterial = new THREE.MeshStandardMaterial({ color: 0x228B22, roughness: 0.8 });
const trunk = new THREE.Mesh(new THREE.CylinderGeometry(0.3, 0.4, 2, 8), trunkMaterial);
trunk.position.y = 2 / 2; trunk.castShadow = true; trunk.receiveShadow = true; group.add(trunk);
const leaves = new THREE.Mesh(new THREE.IcosahedronGeometry(1.2, 0), leavesMaterial);
leaves.position.y = 2 + 0.8; leaves.castShadow = true; leaves.receiveShadow = true; group.add(leaves);
group.userData.type = "Tree";
return group;
}
function createRock() { /* ... copy from previous ... */
const rockMaterial = new THREE.MeshStandardMaterial({ color: 0xaaaaaa, roughness: 0.8, metalness: 0.1 });
const rock = new THREE.Mesh(new THREE.IcosahedronGeometry(0.7, 0), rockMaterial);
rock.position.y = 0.7 / 2; rock.rotation.x = Math.random() * Math.PI; rock.rotation.y = Math.random() * Math.PI;
rock.castShadow = true; rock.receiveShadow = true;
rock.userData.type = "Rock";
return rock;
}
function createFencePost() { /* ... copy from previous ... */
const postMaterial = new THREE.MeshStandardMaterial({ color: 0xdeb887, roughness: 0.9 });
const post = new THREE.Mesh(new THREE.BoxGeometry(0.2, 1.5, 0.2), postMaterial);
post.position.y = 1.5 / 2; post.castShadow = true; post.receiveShadow = true;
post.userData.type = "Fence Post";
return post;
}
// --- Event Handlers ---
function onMouseMove(event) {
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
// Prevent placing if clicking the save button
if (event.target.id === 'saveButton') return;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObject(groundMesh);
if (intersects.length > 0) {
const intersectPoint = intersects[0].point;
let newObject = null;
switch (selectedObjectType) { // Use create functions
case "Simple House": newObject = createSimpleHouse(); break;
case "Tree": newObject = createTree(); break;
case "Rock": newObject = createRock(); break;
case "Fence Post": newObject = createFencePost(); break;
default: console.warn("Unknown object type:", selectedObjectType); return;
}
if (newObject) {
newObject.position.copy(intersectPoint);
// Adjust Y based on object's geometry center if needed (already done in create funcs mostly)
if (newObject.geometry && newObject.geometry.boundingBox) {
// Optional fine-tuning if origin isn't at base
} else if (newObject.type === "Group") {
// Assume group origin is logical base
}
scene.add(newObject);
placedObjects.push(newObject);
}
}
}
function onKeyDown(event) { keysPressed[event.code] = true; }
function onKeyUp(event) { keysPressed[event.code] = false; }
function onSaveClick() {
console.log("Save button clicked.");
const objectsToSave = placedObjects.map(obj => {
// IMPORTANT: Need to know the TYPE of the object placed
// We added `userData.type` in the create functions
const type = obj.userData.type || "Unknown"; // Get stored type
// Simplify rotation: Use Euler angles which are often easier to serialize/deserialize than Quaternions
const rotation = {
_x: obj.rotation.x,
_y: obj.rotation.y,
_z: obj.rotation.z,
_order: obj.rotation.order // Important!
};
return {
type: type,
position: { x: obj.position.x, y: obj.position.y, z: obj.position.z },
// Store Euler rotation instead of quaternion for simplicity here
rotation: rotation
// quaternion: { _x: obj.quaternion.x, _y: obj.quaternion.y, _z: obj.quaternion.z, _w: obj.quaternion.w } // Alternative
};
}).filter(obj => obj.type !== "Unknown"); // Don't save unknowns
const saveDataJson = JSON.stringify(objectsToSave);
const saveDataEncoded = encodeURIComponent(saveDataJson);
// Construct URL with save data, space ID, and name
const params = new URLSearchParams();
params.set('save_data', saveDataEncoded);
if (currentSpaceId) {
params.set('space_id', currentSpaceId);
}
if (currentSpaceName) { // Send current name from sidebar input
params.set('space_name', encodeURIComponent(currentSpaceName));
}
console.log("Redirecting to save...");
// Trigger reload with query parameters
window.location.search = params.toString();
}
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) {
moveDirection.normalize().multiplyScalar(playerSpeed);
// Apply movement relative to player's current orientation (if needed)
// For simple world-axis movement:
playerMesh.position.add(moveDirection);
// Collision detection would go here!
// Basic ground clamping (prevent falling through)
playerMesh.position.y = Math.max(playerMesh.position.y, 0.4 + 0.8/2); // Adjust based on capsule size
}
}
function updateCamera() {
if (!playerMesh) return;
// Simple third-person follow cam
const offset = new THREE.Vector3(0, 5, 10); // Camera distance from player
// Can be enhanced to use player rotation later
const targetPosition = playerMesh.position.clone().add(offset);
// Smooth camera movement (Lerp)
camera.position.lerp(targetPosition, 0.1); // Adjust lerp factor for smoothness
camera.lookAt(playerMesh.position); // Always look at the player
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function animate() {
requestAnimationFrame(animate);
updatePlayerMovement();
updateCamera(); // Make camera follow player
renderer.render(scene, camera);
}
// --- Start the app ---
init();
</script>
</body>
</html>