Spaces:
Running
Running
<html> | |
<head> | |
<title>Three.js Infinite World</title> | |
<style> | |
body { margin: 0; overflow: hidden; } | |
canvas { display: block; } | |
</style> | |
<!-- New: Polling function for game state --> | |
<script> | |
// Poll the shared game state every 5 seconds (for demonstration) | |
function pollGameState() { | |
console.log("Polling updated game state:", window.GAME_STATE); | |
// Here you could update the scene based on the new state. | |
} | |
setInterval(pollGameState, 5000); | |
</script> | |
</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'; | |
let scene, camera, renderer, playerMesh; | |
let raycaster, mouse; | |
const keysPressed = {}; | |
const playerSpeed = 0.15; | |
let newlyPlacedObjects = []; // Track objects added THIS session for saving | |
const placeholderPlots = new Set(); | |
const groundMeshes = {}; // Store ground mesh references | |
// --- Session Storage Key --- | |
const SESSION_STORAGE_KEY = 'unsavedInfiniteWorldState'; | |
// --- Injected State from Streamlit --- | |
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; | |
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 | |
}); | |
// βββ Helper to tag every object/group with type & unique ID ββββββββββββββ | |
function createObjectBase(type) { | |
return { userData: { type: type, obj_id: THREE.MathUtils.generateUUID() } }; | |
} | |
// βββ Primitive creators βββββββββββββββββββββββββββββββββββββββββββββββββ | |
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 }); | |
const 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 = 1.5/2; | |
m1.castShadow = 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 = m2.receiveShadow = true; | |
group.add(m2); | |
return group; | |
} | |
function createTree() { | |
const base = createObjectBase("Tree"); | |
const group = new THREE.Group(); | |
Object.assign(group, base); | |
const matTrunk = new THREE.MeshStandardMaterial({ color: 0x8B4513, roughness: 0.9 }); | |
const matFoliage = new THREE.MeshStandardMaterial({ color: 0x228B22, roughness: 0.8 }); | |
const trunk = new THREE.Mesh(new THREE.CylinderGeometry(0.3, 0.4, 2, 8), matTrunk); | |
trunk.position.y = 1; | |
trunk.castShadow = trunk.receiveShadow = true; | |
group.add(trunk); | |
const foliage = new THREE.Mesh(new THREE.IcosahedronGeometry(1.2, 0), matFoliage); | |
foliage.position.y = 2.8; | |
foliage.castShadow = foliage.receiveShadow = true; | |
group.add(foliage); | |
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 = rock.receiveShadow = true; | |
return rock; | |
} | |
function createFencePost() { | |
const base = createObjectBase("Fence Post"); | |
const mat = new THREE.MeshStandardMaterial({ color: 0xdeb887, roughness: 0.9 }); | |
const post = new THREE.Mesh(new THREE.BoxGeometry(0.2, 1.5, 0.2), mat); | |
Object.assign(post, base); | |
post.position.y = 0.75; | |
post.castShadow = post.receiveShadow = true; | |
return post; | |
} | |
// βββ 1. Cyberpunk City Builder Kit ββββββββββββββββββββββββββββββββββββββββ | |
function createCyberpunkWallPanel({ | |
width=2, height=3, depth=0.2, | |
baseMat={ color:0x555555, metalness:0.8, roughness:0.4 }, | |
trimMat={ emissive:0x00ffff, emissiveIntensity:1.2 }, | |
position=new THREE.Vector3() | |
} = {}) { | |
const panelBase = new THREE.BoxGeometry(width, height, depth); | |
const panel = new THREE.Mesh(panelBase, new THREE.MeshStandardMaterial(baseMat)); | |
panel.position.copy(position); | |
return panel; | |
} | |
function createRooftopACUnit({ | |
width=1, height=0.5, depth=1, | |
matProps={ color:0x777777, roughness:0.7, metalness:0.3 }, | |
position=new THREE.Vector3() | |
} = {}) { | |
const unit = new THREE.Mesh( | |
new THREE.BoxGeometry(width, height, depth), | |
new THREE.MeshStandardMaterial(matProps) | |
); | |
unit.position.copy(position); | |
return unit; | |
} | |
function createHolographicWindowDisplay({ | |
width=1.5, height=1, position=new THREE.Vector3() | |
} = {}) { | |
const geom = new THREE.PlaneGeometry(width, height); | |
const mat = new THREE.MeshBasicMaterial({ | |
color:0xffffff, transparent:true, opacity:0.6 | |
}); | |
const disp = new THREE.Mesh(geom, mat); | |
disp.position.copy(position); | |
return disp; | |
} | |
function createGreebleSet({ | |
count=10, scale=0.2, position=new THREE.Vector3() | |
} = {}) { | |
const grp = new THREE.Group(); | |
for(let i=0; i<count; i++){ | |
const dx = (Math.random()-0.5)*2; | |
const dy = (Math.random()-0.5)*2; | |
const dz = (Math.random()-0.5)*0.2; | |
const g = new THREE.Mesh( | |
new THREE.BoxGeometry(scale, scale, scale), | |
new THREE.MeshStandardMaterial({ color:0x444444, roughness:0.6, metalness:0.4 }) | |
); | |
g.position.copy(position).add(new THREE.Vector3(dx,dy,dz)); | |
grp.add(g); | |
} | |
return grp; | |
} | |
function createCyberpunkKit() { | |
const base = createObjectBase("Cyberpunk City Builder Kit"); | |
const kit = new THREE.Group(); | |
Object.assign(kit, base); | |
kit.add(createCyberpunkWallPanel({ position:new THREE.Vector3(0,1,0) })); | |
kit.add(createRooftopACUnit({ position:new THREE.Vector3(3,0.5,0) })); | |
kit.add(createHolographicWindowDisplay({ position:new THREE.Vector3(6,1,0) })); | |
kit.add(createGreebleSet({ position:new THREE.Vector3(9,0,0) })); | |
return kit; | |
} | |
// βββ 2. POLYGON β Fantasy Kingdom Pack βββββββββββββββββββββββββββββββββββ | |
function createKingFigure({ position=new THREE.Vector3() } = {}) { | |
const base = createObjectBase("POLYGON - Fantasy Kingdom Pack"); | |
const mesh = new THREE.Mesh( | |
new THREE.CylinderGeometry(0.3,0.5,1.8,6), | |
new THREE.MeshStandardMaterial({ color:0xffd700, metalness:0.9, roughness:0.2 }) | |
); | |
Object.assign(mesh, base); | |
mesh.position.copy(position); | |
return mesh; | |
} | |
function createSoldierFigure({ position=new THREE.Vector3() } = {}) { | |
const base = createObjectBase("POLYGON - Fantasy Kingdom Pack"); | |
const mesh = new THREE.Mesh( | |
new THREE.BoxGeometry(0.5,1.6,0.4), | |
new THREE.MeshStandardMaterial({ color:0x888888, metalness:0.6, roughness:0.4 }) | |
); | |
Object.assign(mesh, base); | |
mesh.position.copy(position); | |
return mesh; | |
} | |
function createMageFigure({ position=new THREE.Vector3() } = {}) { | |
const base = createObjectBase("POLYGON - Fantasy Kingdom Pack"); | |
const mesh = new THREE.Mesh( | |
new THREE.ConeGeometry(0.4,1.5,6), | |
new THREE.MeshStandardMaterial({ color:0x9933ff, roughness:0.5 }) | |
); | |
Object.assign(mesh, base); | |
mesh.position.copy(position); | |
return mesh; | |
} | |
function createFantasyKingdomPack() { | |
const grp = new THREE.Group(); | |
Object.assign(grp, createObjectBase("POLYGON - Fantasy Kingdom Pack")); | |
grp.add(createKingFigure({ position:new THREE.Vector3(0,0,0) })); | |
grp.add(createSoldierFigure({ position:new THREE.Vector3(2,0,0) })); | |
grp.add(createMageFigure({ position:new THREE.Vector3(4,0,0) })); | |
return grp; | |
} | |
// βββ 3. HEROIC FANTASY CREATURES FULL PACK VOLΒ 1 βββββββββββββββββββββββββ | |
function createDemonicCreatureBase({ position=new THREE.Vector3() }={}) { | |
const base = createObjectBase("HEROIC FANTASY CREATURES FULL PACK VOLΒ 1"); | |
const mesh = new THREE.Mesh( | |
new THREE.SphereGeometry(0.8,8,6), | |
new THREE.MeshStandardMaterial({ color:0x550000, roughness:0.7 }) | |
); | |
Object.assign(mesh, base); | |
mesh.position.copy(position); | |
return mesh; | |
} | |
function createFantasyAnimalBase({ position=new THREE.Vector3() }={}) { | |
const base = createObjectBase("HEROIC FANTASY CREATURES FULL PACK VOLΒ 1"); | |
const mesh = new THREE.Mesh( | |
new THREE.BoxGeometry(1.2,0.6,2), | |
new THREE.MeshStandardMaterial({ color:0x665533, roughness:0.8 }) | |
); | |
Object.assign(mesh, base); | |
mesh.position.copy(position); | |
return mesh; | |
} | |
function createFantasyLizardBase({ position=new THREE.Vector3() }={}) { | |
const base = createObjectBase("HEROIC FANTASY CREATURES FULL PACK VOLΒ 1"); | |
const mesh = new THREE.Mesh( | |
new THREE.ConeGeometry(0.3,1,6), | |
new THREE.MeshStandardMaterial({ color:0x339933, roughness:0.6 }) | |
); | |
Object.assign(mesh, base); | |
mesh.position.copy(position); | |
return mesh; | |
} | |
function createLivingDeadBase({ position=new THREE.Vector3() }={}) { | |
const base = createObjectBase("HEROIC FANTASY CREATURES FULL PACK VOLΒ 1"); | |
const mesh = new THREE.Mesh( | |
new THREE.BoxGeometry(0.8,1.8,0.5), | |
new THREE.MeshStandardMaterial({ color:0x777777, roughness:0.9 }) | |
); | |
Object.assign(mesh, base); | |
mesh.position.copy(position); | |
return mesh; | |
} | |
function createFantasyVillainBase({ position=new THREE.Vector3() }={}) { | |
const base = createObjectBase("HEROIC FANTASY CREATURES FULL PACK VOLΒ 1"); | |
const mesh = new THREE.Mesh( | |
new THREE.OctahedronGeometry(0.9,0), | |
new THREE.MeshStandardMaterial({ color:0x220022, roughness:0.7 }) | |
); | |
Object.assign(mesh, base); | |
mesh.position.copy(position); | |
return mesh; | |
} | |
function createMythologicalCreatureBase({ position=new THREE.Vector3() }={}) { | |
const base = createObjectBase("HEROIC FANTASY CREATURES FULL PACK VOLΒ 1"); | |
const mesh = new THREE.Mesh( | |
new THREE.TorusKnotGeometry(0.5,0.2,100,16), | |
new THREE.MeshStandardMaterial({ color:0xdddd00, roughness:0.5 }) | |
); | |
Object.assign(mesh, base); | |
mesh.position.copy(position); | |
return mesh; | |
} | |
function createHeroicFantasyCreaturesPack() { | |
const grp = new THREE.Group(); | |
Object.assign(grp, createObjectBase("HEROIC FANTASY CREATURES FULL PACK VOLΒ 1")); | |
grp.add(createDemonicCreatureBase({ position:new THREE.Vector3(0,0,0) })); | |
grp.add(createFantasyAnimalBase({ position:new THREE.Vector3(3,0,0) })); | |
grp.add(createFantasyLizardBase({ position:new THREE.Vector3(6,0,0) })); | |
grp.add(createLivingDeadBase({ position:new THREE.Vector3(9,0,0) })); | |
grp.add(createFantasyVillainBase({ position:new THREE.Vector3(12,0,0) })); | |
grp.add(createMythologicalCreatureBase({ position:new THREE.Vector3(15,0,0) })); | |
return grp; | |
} | |
// βββ 4. POLYGON β Apocalypse Pack (Low Poly) βββββββββββββββββββββββββββββ | |
function createZombieFigure({ position=new THREE.Vector3() }={}) { | |
const base = createObjectBase("POLYGON - Apocalypse Pack"); | |
const mesh = new THREE.Mesh( | |
new THREE.BoxGeometry(0.5,1.7,0.4), | |
new THREE.MeshStandardMaterial({ color:0x445544, roughness:0.9 }) | |
); | |
Object.assign(mesh, base); | |
mesh.position.copy(position); | |
return mesh; | |
} | |
function createSurvivorFigure({ position=new THREE.Vector3() }={}) { | |
const base = createObjectBase("POLYGON - Apocalypse Pack"); | |
const mesh = new THREE.Mesh( | |
new THREE.BoxGeometry(0.5,1.7,0.4), | |
new THREE.MeshStandardMaterial({ color:0x885522, roughness:0.7 }) | |
); | |
Object.assign(mesh, base); | |
mesh.position.copy(position); | |
return mesh; | |
} | |
function createBackpack({ position=new THREE.Vector3() }={}) { | |
const base = createObjectBase("POLYGON - Apocalypse Pack"); | |
const mesh = new THREE.Mesh( | |
new THREE.BoxGeometry(0.6,0.8,0.3), | |
new THREE.MeshStandardMaterial({ color:0x333333, roughness:0.8 }) | |
); | |
Object.assign(mesh, base); | |
mesh.position.copy(position); | |
return mesh; | |
} | |
function createMakeshiftArmor({ position=new THREE.Vector3() }={}) { | |
const base = createObjectBase("POLYGON - Apocalypse Pack"); | |
const mesh = new THREE.Mesh( | |
new THREE.BoxGeometry(1,1.2,0.2), | |
new THREE.MeshStandardMaterial({ color:0x555555, roughness:0.9 }) | |
); | |
Object.assign(mesh, base); | |
mesh.position.copy(position); | |
return mesh; | |
} | |
function createBuggyFrame({ position=new THREE.Vector3() }={}) { | |
const base = createObjectBase("POLYGON - Apocalypse Pack"); | |
const mesh = new THREE.Mesh( | |
new THREE.BoxGeometry(2,0.5,1.2), | |
new THREE.MeshStandardMaterial({ color:0x666666, roughness:0.7 }) | |
); | |
Object.assign(mesh, base); | |
mesh.position.copy(position); | |
return mesh; | |
} | |
function createApocalypsePack() { | |
const grp = new THREE.Group(); | |
Object.assign(grp, createObjectBase("POLYGON - Apocalypse Pack")); | |
grp.add(createZombieFigure({ position:new THREE.Vector3(0,0,0) })); | |
grp.add(createSurvivorFigure({ position:new THREE.Vector3(3,0,0) })); | |
grp.add(createBackpack({ position:new THREE.Vector3(6,0,0) })); | |
grp.add(createMakeshiftArmor({ position:new THREE.Vector3(9,0,0) })); | |
grp.add(createBuggyFrame({ position:new THREE.Vector3(12,0,0) })); | |
return grp; | |
} | |
// βββ Existing loader that places from Python data ββββββββββββββββββββββββ | |
function createAndPlaceObject(objData, isNewObject) { | |
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; | |
case "Cyberpunk City Builder Kit": | |
loadedObject = createCyberpunkKit(); break; | |
case "POLYGON - Fantasy Kingdom Pack": | |
loadedObject = createFantasyKingdomPack(); break; | |
case "HEROIC FANTASY CREATURES FULL PACK VOLΒ 1": | |
loadedObject = createHeroicFantasyCreaturesPack(); break; | |
case "POLYGON - Apocalypse Pack": | |
loadedObject = createApocalypsePack(); break; | |
default: console.warn("Unknown object type in data:", objData.type); break; | |
} | |
if (loadedObject) { | |
// position & rotation from CSV or JSON | |
if (objData.position && objData.position.x !== undefined) { | |
loadedObject.position.set(objData.position.x, objData.position.y, objData.position.z); | |
} else if (objData.pos_x !== undefined) { | |
loadedObject.position.set(objData.pos_x, objData.pos_y, objData.pos_z); | |
} | |
if (objData.rotation) { | |
loadedObject.rotation.set( | |
objData.rotation._x, objData.rotation._y, objData.rotation._z | |
); | |
} else if (objData.rot_x !== undefined) { | |
loadedObject.rotation.set( | |
objData.rot_x, objData.rot_y, objData.rot_z | |
); | |
} | |
scene.add(loadedObject); | |
if (isNewObject) newlyPlacedObjects.push(loadedObject); | |
return loadedObject; | |
} | |
return null; | |
} | |
// βββ Setup, Animate etc. (unchanged) ββββββββββββββββββββββββββββββββββββ | |
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(0,15,20); | |
camera.lookAt(0,0,0); | |
scene.add(camera); | |
setupLighting(); | |
setupInitialGround(); | |
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(); | |
restoreUnsavedState(); | |
document.addEventListener('mousemove', onMouseMove, false); | |
document.addEventListener('click', onDocumentClick, false); | |
window.addEventListener('resize', onWindowResize, false); | |
document.addEventListener('keydown', onKeyDown); | |
document.addEventListener('keyup', onKeyUp); | |
window.teleportPlayer = teleportPlayer; | |
window.getSaveDataAndPosition = getSaveDataAndPosition; | |
window.resetNewlyPlacedObjects = resetNewlyPlacedObjects; | |
console.log("Three.js Initialized. World ready."); | |
animate(); | |
} | |
// βββ Click to place logic βββββββββββββββββββββββββββββββββββββββββββββββ | |
function onDocumentClick(event) { | |
if (selectedObjectType === "None") return; | |
const grounds = Object.values(groundMeshes); | |
if (!grounds.length) return; | |
raycaster.setFromCamera(mouse, camera); | |
const hits = raycaster.intersectObjects(grounds); | |
if (hits.length) { | |
const pt = hits[0].point; | |
let newObj = null; | |
switch (selectedObjectType) { | |
case "Simple House": newObj = createSimpleHouse(); break; | |
case "Tree": newObj = createTree(); break; | |
case "Rock": newObj = createRock(); break; | |
case "Fence Post": newObj = createFencePost(); break; | |
case "Cyberpunk City Builder Kit": | |
newObj = createCyberpunkKit(); break; | |
case "POLYGON - Fantasy Kingdom Pack": | |
newObj = createFantasyKingdomPack(); break; | |
case "HEROIC FANTASY CREATURES FULL PACK VOLΒ 1": | |
newObj = createHeroicFantasyCreaturesPack(); break; | |
case "POLYGON - Apocalypse Pack": | |
newObj = createApocalypsePack(); break; | |
default: return; | |
} | |
if (newObj) { | |
newObj.position.copy(pt); | |
newObj.position.y += 0.01; | |
scene.add(newObj); | |
newlyPlacedObjects.push(newObj); | |
saveUnsavedState(); | |
console.log(`Placed new ${selectedObjectType}. Unsaved total: ${newlyPlacedObjects.length}`); | |
} | |
} | |
} | |
// βββ Rest of your functions: save/restore state, movement, animate, etc. ββ | |
function saveUnsavedState() { /* unchanged */ } | |
function restoreUnsavedState() { /* unchanged */ } | |
function clearUnsavedState() { /* unchanged */ } | |
function teleportPlayer(x,z) { /* unchanged */ } | |
function getSaveDataAndPosition() { /* unchanged */ } | |
function resetNewlyPlacedObjects() { /* unchanged */ } | |
function updatePlayerMovement() { /* unchanged */ } | |
function checkAndExpandGround() { /* unchanged */ } | |
function updateCamera() { /* unchanged */ } | |
function onMouseMove(event) { | |
mouse.x = (event.clientX / window.innerWidth) * 2 - 1; | |
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; | |
} | |
function onKeyDown(e) { keysPressed[e.code] = true; } | |
function onKeyUp(e) { keysPressed[e.code] = false; } | |
function onWindowResize() { | |
camera.aspect = window.innerWidth / window.innerHeight; | |
camera.updateProjectionMatrix(); | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
} | |
function animate() { | |
requestAnimationFrame(animate); | |
updatePlayerMovement(); | |
updateCamera(); | |
renderer.render(scene, camera); | |
} | |
// βββ Kick it off ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
init(); | |
</script> | |
</body> | |
</html> | |