awacke1's picture
Update index.html
27e5fda verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Three.js Infinite World Builder</title>
<style>
body {
margin: 0;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
background-color: #333;
}
canvas {
display: block;
}
#ui-container {
position: absolute;
top: 10px;
left: 10px;
z-index: 10;
background: rgba(0,0,0,0.6);
padding: 10px;
border-radius: 8px;
display: flex;
flex-direction: column;
gap: 10px;
color: white;
max-width: 250px;
}
.ui-row {
display: flex;
justify-content: space-between;
align-items: center;
gap: 10px;
}
.ui-row label {
white-space: nowrap;
}
button, select, input[type="number"] {
background-color: #555;
color: white;
border: 1px solid #777;
padding: 8px;
border-radius: 5px;
cursor: pointer;
width: 100%;
box-sizing: border-box;
}
button:hover {
background-color: #666;
}
select, input[type="number"] {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23ffffff%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.4-5.4-13z%22%2F%3E%3C%2Fsvg%3E');
background-repeat: no-repeat;
background-position: right 10px top 50%;
background-size: .65em auto;
padding-right: 2em;
}
</style>
</head>
<body>
<!-- UI Elements for User Interaction -->
<div id="ui-container">
<div class="ui-row">
<button id="newWorldButton">New World</button>
</div>
<div class="ui-row">
<button id="loadWorldButton">Load World</button>
<input type="file" id="loadWorldInput" accept=".json" style="display: none;">
</div>
<div class="ui-row">
<button id="saveWorldButton">Save World</button>
</div>
<hr style="border-color: #555; width:100%;">
<div class="ui-row">
<label for="objectSelect">Object:</label>
<select id="objectSelect"></select>
</div>
<div class="ui-row">
<label for="scaleInput">Scale:</label>
<input type="number" id="scaleInput" value="1.0" step="0.1" min="0.1">
</div>
<div class="ui-row">
<label for="rotationInput">Rotation (Y):</label>
<input type="number" id="rotationInput" value="0" step="15" min="0" max="345">
</div>
</div>
<!-- Import map for Three.js -->
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/"
}
}
</script>
<!-- Main Application Logic -->
<script type="module">
import * as THREE from 'three';
// --- Core Scene Variables ---
let scene, camera, renderer, playerMesh;
let raycaster, mouse;
const keysPressed = {};
const playerSpeed = 0.15;
// --- World State Management ---
let worldObjects = []; // This is the single source of truth for all placed objects
const groundMeshes = {}; // Store ground mesh references by grid key
// --- World Configuration ---
const PLOT_WIDTH = 50.0;
const PLOT_DEPTH = 50.0;
// --- Materials ---
const groundMaterial = new THREE.MeshStandardMaterial({
color: 0x55aa55, roughness: 0.9, metalness: 0.1, side: THREE.DoubleSide
});
// --- Object Creation Map ---
const objectFactory = {};
// --- UI Elements ---
const newWorldButton = document.getElementById('newWorldButton');
const saveWorldButton = document.getElementById('saveWorldButton');
const loadWorldButton = document.getElementById('loadWorldButton');
const loadWorldInput = document.getElementById('loadWorldInput');
const objectSelect = document.getElementById('objectSelect');
const scaleInput = document.getElementById('scaleInput');
const rotationInput = document.getElementById('rotationInput');
/**
* Initializes the entire application.
*/
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();
setupPlayer();
populateObjectFactory();
populateObjectSelector();
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);
addEventListeners();
resetWorld(); // Start with a fresh world
console.log("Three.js Initialized. World ready.");
animate();
}
/**
* Sets up lights for the scene.
*/
function setupLighting() {
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2);
directionalLight.position.set(50, 150, 100);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = 4096;
directionalLight.shadow.mapSize.height = 4096;
directionalLight.shadow.camera.near = 0.5;
directionalLight.shadow.camera.far = 500;
directionalLight.shadow.camera.left = -150;
directionalLight.shadow.camera.right = 150;
directionalLight.shadow.camera.top = 150;
directionalLight.shadow.camera.bottom = -150;
directionalLight.shadow.bias = -0.001;
scene.add(directionalLight);
}
/**
* Creates the player representation in the scene.
*/
function setupPlayer() {
const playerGeo = new THREE.CapsuleGeometry(0.4, 0.8, 4, 8);
const playerMat = new THREE.MeshStandardMaterial({ color: 0x0000ff, roughness: 0.6 });
playerMesh = new THREE.Mesh(playerGeo, playerMat);
playerMesh.castShadow = true;
playerMesh.receiveShadow = true;
scene.add(playerMesh);
}
/**
* Clears the world and resets the state to a default.
*/
function resetWorld() {
console.log("Creating a new world.");
// Clear existing objects
worldObjects.forEach(objData => {
const object = scene.getObjectByProperty('uuid', objData.uuid);
if (object) {
scene.remove(object);
}
});
worldObjects = [];
// Clear ground planes
for (const key in groundMeshes) {
scene.remove(groundMeshes[key]);
delete groundMeshes[key];
}
// Create the initial ground plane at (0,0)
createGroundPlane(0, 0);
// Reset player position
playerMesh.position.set(PLOT_WIDTH / 2, 0.4 + 0.8 / 2, PLOT_DEPTH / 2);
updateCamera(true); // Force camera snap
}
/**
* Populates the world based on data from a loaded file.
* @param {object} worldData - The parsed JSON data from a world file.
*/
function populateWorld(worldData) {
console.log("Loading world from file...");
resetWorld(); // Ensure the scene is clean before loading
if (worldData.objects && Array.isArray(worldData.objects)) {
worldData.objects.forEach(objData => {
// Create and place the object in the scene
const newObject = createObject(objData.type);
if (newObject) {
newObject.position.copy(objData.position);
newObject.rotation.set(objData.rotation._x, objData.rotation._y, objData.rotation._z, objData.rotation._order);
newObject.scale.copy(objData.scale);
newObject.userData.obj_id = objData.obj_id; // Preserve original ID
scene.add(newObject);
// Add its data to our source of truth, including its new scene UUID
const newObjData = { ...objData, uuid: newObject.uuid };
worldObjects.push(newObjData);
}
});
}
// Generate all necessary ground planes for the loaded objects
generateGroundForLoadedWorld();
// Set player position if it exists in the save file
if (worldData.playerPosition) {
playerMesh.position.copy(worldData.playerPosition);
}
updateCamera(true); // Snap camera to new position
console.log(`Loaded ${worldObjects.length} objects.`);
}
/**
* Creates a ground plane at a specific grid coordinate.
* @param {number} gridX - The x-coordinate in the grid.
* @param {number} gridZ - The z-coordinate in the grid.
*/
function createGroundPlane(gridX, gridZ) {
const gridKey = `${gridX}_${gridZ}`;
if (groundMeshes[gridKey]) return; // Don't create if it already exists
console.log(`Creating ground at ${gridX}, ${gridZ}`);
const groundGeometry = new THREE.PlaneGeometry(PLOT_WIDTH, PLOT_DEPTH);
const groundMesh = new THREE.Mesh(groundGeometry, groundMaterial);
groundMesh.rotation.x = -Math.PI / 2;
groundMesh.position.y = -0.05;
groundMesh.position.x = gridX * PLOT_WIDTH + PLOT_WIDTH / 2.0;
groundMesh.position.z = gridZ * PLOT_DEPTH + PLOT_DEPTH / 2.0;
groundMesh.receiveShadow = true;
groundMesh.userData.gridKey = gridKey;
scene.add(groundMesh);
groundMeshes[gridKey] = groundMesh;
}
/**
* After loading a world, this ensures all necessary ground tiles are created.
*/
function generateGroundForLoadedWorld() {
const requiredGrids = new Set();
worldObjects.forEach(objData => {
const gridX = Math.floor(objData.position.x / PLOT_WIDTH);
const gridZ = Math.floor(objData.position.z / PLOT_DEPTH);
requiredGrids.add(`${gridX}_${gridZ}`);
});
// Also add grid for player
const playerGridX = Math.floor(playerMesh.position.x / PLOT_WIDTH);
const playerGridZ = Math.floor(playerMesh.position.z / PLOT_DEPTH);
requiredGrids.add(`${playerGridX}_${playerGridZ}`);
requiredGrids.forEach(key => {
const [gridX, gridZ] = key.split('_').map(Number);
createGroundPlane(gridX, gridZ);
});
}
/**
* Creates a generic base for any object, assigning a type and unique ID.
* @param {string} type - The type name of the object.
* @returns {object} - An object with userData containing type and a new UUID.
*/
function createObjectBase(type) {
return { userData: { type: type, obj_id: THREE.MathUtils.generateUUID() } };
}
/**
* Central function to create a 3D object based on its type string.
* @param {string} type - The type of object to create.
* @returns {THREE.Object3D|null} The created Three.js object or null if type is unknown.
*/
function createObject(type) {
if (objectFactory[type]) {
return objectFactory[type]();
}
console.warn("Unknown object type:", type);
return null;
}
// --- All Object Creation Functions ---
// (Abridged for brevity, the full list is included below this script block)
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 = 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+1/2;
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});
const 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;
}
// --- File and State Management ---
/**
* Gathers all world data and triggers a download of the world file.
*/
function saveWorldToFile() {
console.log("Saving world to file...");
const dataToSave = {
meta: {
version: "1.0",
plotWidth: PLOT_WIDTH,
plotDepth: PLOT_DEPTH,
savedAt: new Date().toISOString()
},
playerPosition: playerMesh.position.clone(),
objects: worldObjects.map(objData => {
// We only need to store the data, not the live scene object reference
const { uuid, ...rest } = objData;
return rest;
})
};
const jsonString = JSON.stringify(dataToSave, null, 2);
const blob = new Blob([jsonString], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `world_${Date.now()}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
console.log(`World saved with ${dataToSave.objects.length} objects.`);
}
/**
* Handles the file selection for loading a world.
* @param {Event} event - The file input change event.
*/
function handleWorldFileLoad(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
try {
const worldData = JSON.parse(e.target.result);
populateWorld(worldData);
} catch (error) {
console.error("Error parsing world file:", error);
alert("Failed to load world file. It might be corrupted.");
}
};
reader.readAsText(file);
}
// --- Event Handlers ---
function addEventListeners() {
document.addEventListener('mousemove', onMouseMove, false);
document.addEventListener('click', onDocumentClick, false);
window.addEventListener('resize', onWindowResize, false);
document.addEventListener('keydown', onKeyDown);
document.addEventListener('keyup', onKeyUp);
newWorldButton.addEventListener('click', () => {
if (confirm('Are you sure you want to start a new world? Any unsaved changes will be lost.')) {
resetWorld();
}
});
saveWorldButton.addEventListener('click', saveWorldToFile);
loadWorldButton.addEventListener('click', () => loadWorldInput.click());
loadWorldInput.addEventListener('change', handleWorldFileLoad);
}
function onMouseMove(event) {
// Adjust for UI offset if mouse is over it
const uiRect = document.getElementById('ui-container').getBoundingClientRect();
if (event.clientX < uiRect.right && event.clientY < uiRect.bottom) {
mouse.x = -99; // Prevent placement when mouse is over UI
mouse.y = -99;
return;
}
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
}
function onDocumentClick(event) {
if (mouse.x === -99) return; // Don't place if mouse was over UI
const selectedObjectType = objectSelect.value;
if (selectedObjectType === "None") return;
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;
const newObject = createObject(selectedObjectType);
if (newObject) {
newObject.position.copy(intersectPoint);
newObject.position.y += 0.01; // Prevent z-fighting
const scale = parseFloat(scaleInput.value) || 1.0;
const rotationY = THREE.MathUtils.degToRad(parseFloat(rotationInput.value) || 0);
newObject.scale.setScalar(scale);
newObject.rotation.y = rotationY;
scene.add(newObject);
// Add the new object's data to our main array
const newObjectData = {
obj_id: newObject.userData.obj_id,
uuid: newObject.uuid, // Keep track of the live object in the scene
type: newObject.userData.type,
position: newObject.position.clone(),
rotation: { _x: newObject.rotation.x, _y: newObject.rotation.y, _z: newObject.rotation.z, _order: newObject.rotation.order },
scale: newObject.scale.clone()
};
worldObjects.push(newObjectData);
console.log(`Placed new ${selectedObjectType}. Total objects: ${worldObjects.length}`);
}
}
}
function onKeyDown(event) { keysPressed[event.code] = true; }
function onKeyUp(event) { keysPressed[event.code] = false; }
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
// --- Game Loop and Updates ---
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);
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));
worldMove.add(right.multiplyScalar(-moveDirection.x));
worldMove.normalize().multiplyScalar(playerSpeed);
playerMesh.position.add(worldMove);
playerMesh.position.y = Math.max(playerMesh.position.y, 0.4 + 0.8/2);
checkAndExpandGround();
}
}
function checkAndExpandGround() {
if (!playerMesh) return;
const currentGridX = Math.floor(playerMesh.position.x / PLOT_WIDTH);
const currentGridZ = Math.floor(playerMesh.position.z / PLOT_DEPTH);
// Check a 3x3 area around the player
for (let dx = -1; dx <= 1; dx++) {
for (let dz = -1; dz <= 1; dz++) {
createGroundPlane(currentGridX + dx, currentGridZ + dz);
}
}
}
function updateCamera(forceSnap = false) {
if (!playerMesh) return;
const offset = new THREE.Vector3(0, 15, 20);
const targetPosition = playerMesh.position.clone().add(offset);
if (forceSnap) {
camera.position.copy(targetPosition);
} else {
camera.position.lerp(targetPosition, 0.08);
}
camera.lookAt(playerMesh.position);
}
function animate() {
requestAnimationFrame(animate);
updatePlayerMovement();
updateCamera();
renderer.render(scene, camera);
}
// --- Full Object Factory ---
function populateObjectFactory() {
Object.assign(objectFactory, {
"Simple House": createSimpleHouse, "Cyberpunk Wall Panel": createCyberpunkWallPanel, "Modular Hab Block": createModularHabBlock, "MegaCorp Skyscraper": createMegaCorpSkyscraper,
"Castle Wall Section": createCastleWallSection, "Wooden Door": createWoodenDoor, "House Roof Section": createHouseRoofSection, "Concrete Bunker Wall": createConcreteBunkerWall,
"Damaged House Facade": createDamagedHouseFacade, "Tree": createTree, "Rock": createRock, "Pine Tree": createPineTree, "Boulder": createBoulder, "Alien Plant": createAlienPlant,
"Floating Rock Platform": createFloatingRockPlatform, "Rubble Pile": createRubblePile, "Fence Post": createFencePost, "Rooftop AC Unit": createRooftopACUnit,
"Holographic Window Display": createHolographicWindowDisplay, "Jersey Barrier": createJerseyBarrier, "Oil Drum": createOilDrum, "Canned Food": createCannedFood,
"Treasure Chest": createTreasureChest, "Wall Torch": createWallTorch, "Bone Pile": createBonePile, "King Figure": createKingFigure, "Soldier Figure": createSoldierFigure,
"Mage Figure": createMageFigure, "Zombie Figure": createZombieFigure, "Survivor Figure": createSurvivorFigure, "Dwarf Miner Figure": createDwarfMinerFigure,
"Undead Knight Figure": createUndeadKnightFigure, "Hero Figure": createHeroFigure, "Wooden Cart": createWoodenCart, "Ballista": createBallista, "Siege Tower": createSiegeTower,
"Buggy Frame": createBuggyFrame, "Motorbike": createMotorbike, "Hover Bike": createHoverBike, "APC": createAPC, "Sand Boat": createSandBoat, "Makeshift Machete": createMakeshiftMachete,
"Pistol Body": createPistolBody, "Scope Attachment": createScopeAttachment, "Laser Pistol": createLaserPistol, "Energy Sword": createEnergySword, "Dwarven Axe": createDwarvenAxe,
"Magic Staff": createMagicStaff, "Candle Flame": createCandleFlame, "Dust Cloud": createDustCloud, "Blood Splat Decal": createBloodSplatDecal,
"Burning Barrel Fire": createBurningBarrelFire, "Warp Tunnel Effect": createWarpTunnelEffect, "Laser Beam": createLaserBeam, "Gold Sparkle": createGoldSparkle, "Steam Vent": createSteamVent
});
}
function populateObjectSelector() {
const options = ["None", ...Object.keys(objectFactory)];
options.forEach(name => {
const option = document.createElement('option');
option.value = name;
option.textContent = name;
objectSelect.appendChild(option);
});
}
// --- All the create... functions from the original file go here ---
function createFencePost(){const b=createObjectBase("Fence Post"),a=new THREE.MeshStandardMaterial({color:14596231,roughness:.9}),c=new THREE.Mesh(new THREE.BoxGeometry(.2,1.5,.2),a);return Object.assign(c,b),c.position.y=.75,c.castShadow=!0,c.receiveShadow=!0,c}
function createCyberpunkWallPanel(){const b=createObjectBase("Cyberpunk Wall Panel"),a=new THREE.MeshStandardMaterial({color:47083,roughness:.5,metalness:.8}),c=new THREE.Mesh(new THREE.BoxGeometry(2,3,.2),a);return Object.assign(c,b),c.position.y=1.5,c.castShadow=!0,c.receiveShadow=!0,c}
function createModularHabBlock(){const b=createObjectBase("Modular Hab Block"),a=new THREE.Group;Object.assign(a,b);const c=new THREE.MeshStandardMaterial({color:6710886,roughness:.7}),d=new THREE.Mesh(new THREE.BoxGeometry(3,2,3),c);return d.position.y=1,d.castShadow=!0,d.receiveShadow=!0,a.add(d),a}
function createMegaCorpSkyscraper(){const b=createObjectBase("MegaCorp Skyscraper"),a=new THREE.Group;Object.assign(a,b);const c=new THREE.MeshStandardMaterial({color:3355545,roughness:.4,metalness:.9}),d=new THREE.Mesh(new THREE.BoxGeometry(4,10,4),c);return d.position.y=5,d.castShadow=!0,d.receiveShadow=!0,a.add(d),a}
function createCastleWallSection(){const b=createObjectBase("Castle Wall Section"),a=new THREE.MeshStandardMaterial({color:8421504,roughness:.8}),c=new THREE.Mesh(new THREE.BoxGeometry(5,3,1),a);return Object.assign(c,b),c.position.y=1.5,c.castShadow=!0,c.receiveShadow=!0,c}
function createWoodenDoor(){const b=createObjectBase("Wooden Door"),a=new THREE.MeshStandardMaterial({color:9127187,roughness:.9}),c=new THREE.Mesh(new THREE.BoxGeometry(1,2,.2),a);return Object.assign(c,b),c.position.y=1,c.castShadow=!0,c.receiveShadow=!0,c}
function createHouseRoofSection(){const b=createObjectBase("House Roof Section"),a=new THREE.MeshStandardMaterial({color:8388608,roughness:.7}),c=new THREE.Mesh(new THREE.ConeGeometry(2,1,4),a);return Object.assign(c,b),c.position.y=1,c.rotation.y=Math.PI/4,c.castShadow=!0,c.receiveShadow=!0,c}
function createConcreteBunkerWall(){const b=createObjectBase("Concrete Bunker Wall"),a=new THREE.MeshStandardMaterial({color:5592405,roughness:.9}),c=new THREE.Mesh(new THREE.BoxGeometry(4,2,1),a);return Object.assign(c,b),c.position.y=1,c.castShadow=!0,c.receiveShadow=!0,c}
function createDamagedHouseFacade(){const b=createObjectBase("Damaged House Facade"),a=new THREE.MeshStandardMaterial({color:11184810,roughness:.9}),c=new THREE.Mesh(new THREE.BoxGeometry(3,2,.3),a);return Object.assign(c,b),c.position.y=1,c.castShadow=!0,c.receiveShadow=!0,c}
function createPineTree(){const c=createObjectBase("Pine Tree"),a=new THREE.Group;Object.assign(a,c);const d=new THREE.MeshStandardMaterial({color:9127187,roughness:.9}),b=new THREE.MeshStandardMaterial({color:25600,roughness:.8}),e=new THREE.Mesh(new THREE.CylinderGeometry(.2,.3,1.5,8),d);e.position.y=.75,e.castShadow=!0,e.receiveShadow=!0,a.add(e);const f=new THREE.Mesh(new THREE.ConeGeometry(1,2,8),b);return f.position.y=2,f.castShadow=!0,f.receiveShadow=!0,a.add(f),a}
function createBoulder(){const b=createObjectBase("Boulder"),a=new THREE.MeshStandardMaterial({color:6710886,roughness:.8}),c=new THREE.Mesh(new THREE.IcosahedronGeometry(1,0),a);return Object.assign(c,b),c.position.y=.5,c.rotation.set(Math.random()*Math.PI,Math.random()*Math.PI,0),c.castShadow=!0,c.receiveShadow=!0,c}
function createAlienPlant(){const c=createObjectBase("Alien Plant"),a=new THREE.Group;Object.assign(a,c);const b=new THREE.MeshStandardMaterial({color:65280,roughness:.7}),d=new THREE.Mesh(new THREE.CylinderGeometry(.1,.1,1,8),b);d.position.y=.5,d.castShadow=!0,d.receiveShadow=!0,a.add(d);const e=new THREE.Mesh(new THREE.SphereGeometry(.3,8,8),b);return e.position.y=1,e.castShadow=!0,e.receiveShadow=!0,a.add(e),a}
function createFloatingRockPlatform(){const b=createObjectBase("Floating Rock Platform"),a=new THREE.MeshStandardMaterial({color:5592405,roughness:.8}),c=new THREE.Mesh(new THREE.BoxGeometry(3,.5,3),a);return Object.assign(c,b),c.position.y=2,c.castShadow=!0,c.receiveShadow=!0,c}
function createRubblePile(){const b=createObjectBase("Rubble Pile"),a=new THREE.Group;Object.assign(a,b);const c=new THREE.MeshStandardMaterial({color:7829367,roughness:.9});for(let d=0;d<5;d++){const e=new THREE.Mesh(new THREE.IcosahedronGeometry(.3,0),c);e.position.set(Math.random()*.5-.25,Math.random()*.3,Math.random()*.5-.25),e.castShadow=!0,e.receiveShadow=!0,a.add(e)}return a}
function createRooftopACUnit(){const b=createObjectBase("Rooftop AC Unit"),a=new THREE.MeshStandardMaterial({color:6710886,roughness:.7,metalness:.5}),c=new THREE.Mesh(new THREE.BoxGeometry(1,.8,1),a);return Object.assign(c,b),c.position.y=.4,c.castShadow=!0,c.receiveShadow=!0,c}
function createHolographicWindowDisplay(){const b=createObjectBase("Holographic Window Display"),a=new THREE.MeshStandardMaterial({color:47083,roughness:.5,transparent:!0,opacity:.7}),c=new THREE.Mesh(new THREE.PlaneGeometry(1,1),a);return Object.assign(c,b),c.position.y=1,c.castShadow=!1,c.receiveShadow=!1,c}
function createJerseyBarrier(){const b=createObjectBase("Jersey Barrier"),a=new THREE.MeshStandardMaterial({color:8947848,roughness:.9}),c=new THREE.Mesh(new THREE.BoxGeometry(2,.8,.5),a);return Object.assign(c,b),c.position.y=.4,c.castShadow=!0,c.receiveShadow=!0,c}
function createOilDrum(){const b=createObjectBase("Oil Drum"),a=new THREE.MeshStandardMaterial({color:5592405,roughness:.7,metalness:.3}),c=new THREE.Mesh(new THREE.CylinderGeometry(.4,.4,1,12),a);return Object.assign(c,b),c.position.y=.5,c.castShadow=!0,c.receiveShadow=!0,c}
function createCannedFood(){const b=createObjectBase("Canned Food"),a=new THREE.MeshStandardMaterial({color:11184810,roughness:.7,metalness:.5}),c=new THREE.Mesh(new THREE.CylinderGeometry(.1,.1,.2,12),a);return Object.assign(c,b),c.position.y=.1,c.castShadow=!0,c.receiveShadow=!0,c}
function createTreasureChest(){const b=createObjectBase("Treasure Chest"),a=new THREE.MeshStandardMaterial({color:9127187,roughness:.8}),c=new THREE.Mesh(new THREE.BoxGeometry(1,.6,.8),a);return Object.assign(c,b),c.position.y=.3,c.castShadow=!0,c.receiveShadow=!0,c}
function createWallTorch(){const c=createObjectBase("Wall Torch"),a=new THREE.Group;Object.assign(a,c);const d=new THREE.MeshStandardMaterial({color:14596231,roughness:.9}),b=new THREE.MeshStandardMaterial({color:16729344,roughness:.5}),e=new THREE.Mesh(new THREE.BoxGeometry(.1,.5,.1),d);e.position.y=.25,e.castShadow=!0,e.receiveShadow=!0,a.add(e);const f=new THREE.Mesh(new THREE.SphereGeometry(.1,8,8),b);return f.position.y=.5,f.castShadow=!0,f.receiveShadow=!1,a.add(f),a}
function createBonePile(){const b=createObjectBase("Bone Pile"),a=new THREE.Group;Object.assign(a,b);const c=new THREE.MeshStandardMaterial({color:14540253,roughness:.9});for(let d=0;d<3;d++){const e=new THREE.Mesh(new THREE.CylinderGeometry(.05,.05,.3,8),c);e.position.set(Math.random()*.2-.1,Math.random()*.1,Math.random()*.2-.1),e.rotation.set(Math.random()*Math.PI,Math.random()*Math.PI,Math.random()*Math.PI),e.castShadow=!0,e.receiveShadow=!0,a.add(e)}return a}
function createKingFigure(){const b=createObjectBase("King Figure"),a=new THREE.MeshStandardMaterial({color:16766720,roughness:.6}),c=new THREE.Mesh(new THREE.CapsuleGeometry(.2,.6,4,8),a);return Object.assign(c,b),c.position.y=.4,c.castShadow=!0,c.receiveShadow=!0,c}
function createSoldierFigure(){const b=createObjectBase("Soldier Figure"),a=new THREE.MeshStandardMaterial({color:2263842,roughness:.7}),c=new THREE.Mesh(new THREE.CapsuleGeometry(.2,.6,4,8),a);return Object.assign(c,b),c.position.y=.4,c.castShadow=!0,c.receiveShadow=!0,c}
function createMageFigure(){const b=createObjectBase("Mage Figure"),a=new THREE.MeshStandardMaterial({color:8388736,roughness:.7}),c=new THREE.Mesh(new THREE.CapsuleGeometry(.2,.6,4,8),a);return Object.assign(c,b),c.position.y=.4,c.castShadow=!0,c.receiveShadow=!0,c}
function createZombieFigure(){const b=createObjectBase("Zombie Figure"),a=new THREE.MeshStandardMaterial({color:9498256,roughness:.8}),c=new THREE.Mesh(new THREE.CapsuleGeometry(.2,.6,4,8),a);return Object.assign(c,b),c.position.y=.4,c.castShadow=!0,c.receiveShadow=!0,c}
function createSurvivorFigure(){const b=createObjectBase("Survivor Figure"),a=new THREE.MeshStandardMaterial({color:4620980,roughness:.7}),c=new THREE.Mesh(new THREE.CapsuleGeometry(.2,.6,4,8),a);return Object.assign(c,b),c.position.y=.4,c.castShadow=!0,c.receiveShadow=!0,c}
function createDwarfMinerFigure(){const b=createObjectBase("Dwarf Miner Figure"),a=new THREE.MeshStandardMaterial({color:9127187,roughness:.7}),c=new THREE.Mesh(new THREE.CapsuleGeometry(.2,.5,4,8),a);return Object.assign(c,b),c.position.y=.35,c.castShadow=!0,c.receiveShadow=!0,c}
function createUndeadKnightFigure(){const b=createObjectBase("Undead Knight Figure"),a=new THREE.MeshStandardMaterial({color:5592405,roughness:.8}),c=new THREE.Mesh(new THREE.CapsuleGeometry(.2,.6,4,8),a);return Object.assign(c,b),c.position.y=.4,c.castShadow=!0,c.receiveShadow=!0,c}
function createHeroFigure(){const b=createObjectBase("Hero Figure"),a=new THREE.MeshStandardMaterial({color:16711680,roughness:.6}),c=new THREE.Mesh(new THREE.CapsuleGeometry(.2,.6,4,8),a);return Object.assign(c,b),c.position.y=.4,c.castShadow=!0,c.receiveShadow=!0,c}
function createWoodenCart(){const b=createObjectBase("Wooden Cart"),a=new THREE.MeshStandardMaterial({color:9127187,roughness:.8}),c=new THREE.Mesh(new THREE.BoxGeometry(2,.8,1),a);return Object.assign(c,b),c.position.y=.4,c.castShadow=!0,c.receiveShadow=!0,c}
function createBallista(){const b=createObjectBase("Ballista"),a=new THREE.MeshStandardMaterial({color:14596231,roughness:.8}),c=new THREE.Mesh(new THREE.BoxGeometry(1.5,.8,.5),a);return Object.assign(c,b),c.position.y=.4,c.castShadow=!0,c.receiveShadow=!0,c}
function createSiegeTower(){const b=createObjectBase("Siege Tower"),a=new THREE.MeshStandardMaterial({color:9127187,roughness:.8}),c=new THREE.Mesh(new THREE.BoxGeometry(2,4,2),a);return Object.assign(c,b),c.position.y=2,c.castShadow=!0,c.receiveShadow=!0,c}
function createBuggyFrame(){const b=createObjectBase("Buggy Frame"),a=new THREE.MeshStandardMaterial({color:6710886,roughness:.7,metalness:.5}),c=new THREE.Mesh(new THREE.BoxGeometry(2,.5,1),a);return Object.assign(c,b),c.position.y=.25,c.castShadow=!0,c.receiveShadow=!0,c}
function createMotorbike(){const b=createObjectBase("Motorbike"),a=new THREE.MeshStandardMaterial({color:3355443,roughness:.7,metalness:.5}),c=new THREE.Mesh(new THREE.BoxGeometry(1.5,.6,.4),a);return Object.assign(c,b),c.position.y=.3,c.castShadow=!0,c.receiveShadow=!0,c}
function createHoverBike(){const b=createObjectBase("Hover Bike"),a=new THREE.MeshStandardMaterial({color:47083,roughness:.5,metalness:.8}),c=new THREE.Mesh(new THREE.BoxGeometry(1.5,.4,.4),a);return Object.assign(c,b),c.position.y=.5,c.castShadow=!0,c.receiveShadow=!0,c}
function createAPC(){const b=createObjectBase("APC"),a=new THREE.MeshStandardMaterial({color:3099951,roughness:.7,metalness:.5}),c=new THREE.Mesh(new THREE.BoxGeometry(3,1.5,1.5),a);return Object.assign(c,b),c.position.y=.75,c.castShadow=!0,c.receiveShadow=!0,c}
function createSandBoat(){const b=createObjectBase("Sand Boat"),a=new THREE.MeshStandardMaterial({color:14596231,roughness:.8}),c=new THREE.Mesh(new THREE.BoxGeometry(2,.5,1),a);return Object.assign(c,b),c.position.y=.25,c.castShadow=!0,c.receiveShadow=!0,c}
function createMakeshiftMachete(){const b=createObjectBase("Makeshift Machete"),a=new THREE.MeshStandardMaterial({color:7829367,roughness:.7,metalness:.5}),c=new THREE.Mesh(new THREE.BoxGeometry(.8,.05,.2),a);return Object.assign(c,b),c.position.y=.1,c.castShadow=!0,c.receiveShadow=!0,c}
function createPistolBody(){const b=createObjectBase("Pistol Body"),a=new THREE.MeshStandardMaterial({color:3355443,roughness:.7,metalness:.5}),c=new THREE.Mesh(new THREE.BoxGeometry(.5,.2,.3),a);return Object.assign(c,b),c.position.y=.1,c.castShadow=!0,c.receiveShadow=!0,c}
function createScopeAttachment(){const b=createObjectBase("Scope Attachment"),a=new THREE.MeshStandardMaterial({color:5592405,roughness:.7,metalness:.5}),c=new THREE.Mesh(new THREE.CylinderGeometry(.05,.05,.3,8),a);return Object.assign(c,b),c.position.y=.15,c.rotation.z=Math.PI/2,c.castShadow=!0,c.receiveShadow=!0,c}
function createLaserPistol(){const b=createObjectBase("Laser Pistol"),a=new THREE.MeshStandardMaterial({color:47083,roughness:.5,metalness:.8}),c=new THREE.Mesh(new THREE.BoxGeometry(.5,.2,.3),a);return Object.assign(c,b),c.position.y=.1,c.castShadow=!0,c.receiveShadow=!0,c}
function createEnergySword(){const b=createObjectBase("Energy Sword"),a=new THREE.MeshStandardMaterial({color:65280,roughness:.5,transparent:!0,opacity:.8}),c=new THREE.Mesh(new THREE.BoxGeometry(.8,.05,.2),a);return Object.assign(c,b),c.position.y=.1,c.castShadow=!0,c.receiveShadow=!0,c}
function createDwarvenAxe(){const b=createObjectBase("Dwarven Axe"),a=new THREE.MeshStandardMaterial({color:5592405,roughness:.7,metalness:.5}),c=new THREE.Mesh(new THREE.BoxGeometry(.5,.3,.1),a);return Object.assign(c,b),c.position.y=.15,c.castShadow=!0,c.receiveShadow=!0,c}
function createMagicStaff(){const c=createObjectBase("Magic Staff"),a=new THREE.Group;Object.assign(a,c);const d=new THREE.MeshStandardMaterial({color:9127187,roughness:.8}),b=new THREE.MeshStandardMaterial({color:16711935,roughness:.5}),e=new THREE.Mesh(new THREE.CylinderGeometry(.05,.05,1.5,8),d);e.position.y=.75,e.castShadow=!0,e.receiveShadow=!0,a.add(e);const f=new THREE.Mesh(new THREE.SphereGeometry(.1,8,8),b);return f.position.y=1.5,f.castShadow=!0,f.receiveShadow=!0,a.add(f),a}
function createCandleFlame(){const b=createObjectBase("Candle Flame"),a=new THREE.MeshStandardMaterial({color:16729344,roughness:.5,transparent:!0,opacity:.8}),c=new THREE.Mesh(new THREE.SphereGeometry(.1,8,8),a);return Object.assign(c,b),c.position.y=.1,c.castShadow=!0,c.receiveShadow=!1,c}
function createDustCloud(){const b=createObjectBase("Dust Cloud"),a=new THREE.MeshStandardMaterial({color:11184810,roughness:.9,transparent:!0,opacity:.5}),c=new THREE.Mesh(new THREE.SphereGeometry(.5,8,8),a);return Object.assign(c,b),c.position.y=.25,c.castShadow=!1,c.receiveShadow=!1,c}
function createBloodSplatDecal(){const b=createObjectBase("Blood Splat Decal"),a=new THREE.MeshStandardMaterial({color:9109504,roughness:.9,transparent:!0,opacity:.8}),c=new THREE.Mesh(new THREE.PlaneGeometry(.5,.5),a);return Object.assign(c,b),c.position.y=.01,c.rotation.x=-Math.PI/2,c.castShadow=!1,c.receiveShadow=!0,c}
function createBurningBarrelFire(){const c=createObjectBase("Burning Barrel Fire"),a=new THREE.Group;Object.assign(a,c);const d=new THREE.MeshStandardMaterial({color:5592405,roughness:.7,metalness:.5}),b=new THREE.MeshStandardMaterial({color:16729344,roughness:.5,transparent:!0,opacity:.8}),e=new THREE.Mesh(new THREE.CylinderGeometry(.4,.4,1,12),d);e.position.y=.5,e.castShadow=!0,e.receiveShadow=!0,a.add(e);const f=new THREE.Mesh(new THREE.SphereGeometry(.3,8,8),b);return f.position.y=1,f.castShadow=!0,f.receiveShadow=!1,a.add(f),a}
function createWarpTunnelEffect(){const b=createObjectBase("Warp Tunnel Effect"),a=new THREE.MeshStandardMaterial({color:47083,roughness:.5,transparent:!0,opacity:.7}),c=new THREE.Mesh(new THREE.CylinderGeometry(.5,.5,2,12),a);return Object.assign(c,b),c.position.y=1,c.castShadow=!1,c.receiveShadow=!1,c}
function createLaserBeam(){const b=createObjectBase("Laser Beam"),a=new THREE.MeshStandardMaterial({color:16711680,roughness:.5,transparent:!0,opacity:.8}),c=new THREE.Mesh(new THREE.CylinderGeometry(.05,.05,1,8),a);return Object.assign(c,b),c.position.y=.5,c.rotation.z=Math.PI/2,c.castShadow=!1,c.receiveShadow=!1,c}
function createGoldSparkle(){const b=createObjectBase("Gold Sparkle"),a=new THREE.MeshStandardMaterial({color:16766720,roughness:.5,transparent:!0,opacity:.8}),c=new THREE.Mesh(new THREE.SphereGeometry(.1,8,8),a);return Object.assign(c,b),c.position.y=.1,c.castShadow=!1,c.receiveShadow=!1,c}
function createSteamVent(){const b=createObjectBase("Steam Vent"),a=new THREE.MeshStandardMaterial({color:11184810,roughness:.9,transparent:!0,opacity:.5}),c=new THREE.Mesh(new THREE.CylinderGeometry(.1,.1,.5,8),a);return Object.assign(c,b),c.position.y=.25,c.castShadow=!1,c.receiveShadow=!1,c}
// --- Start the application ---
init();
</script>
</body>
</html>