-
-
Save shricodev/a8644daf171409d0c09b670dd99af122 to your computer and use it in GitHub Desktop.
3D Town and Fire truck Simulation (Developed by Claude Opus 4 AI Model) - Blog Demo
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!doctype html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>3D City Builder & Driver</title> | |
| <style> | |
| body { | |
| margin: 0; | |
| padding: 0; | |
| font-family: Arial, sans-serif; | |
| overflow: hidden; | |
| } | |
| #canvas-container { | |
| position: relative; | |
| width: 100vw; | |
| height: 100vh; | |
| } | |
| #controls { | |
| position: absolute; | |
| top: 10px; | |
| left: 10px; | |
| background: rgba(0, 0, 0, 0.8); | |
| color: white; | |
| padding: 15px; | |
| border-radius: 10px; | |
| z-index: 100; | |
| } | |
| #controls button { | |
| display: block; | |
| margin: 5px 0; | |
| padding: 8px 15px; | |
| background: #4caf50; | |
| color: white; | |
| border: none; | |
| border-radius: 5px; | |
| cursor: pointer; | |
| width: 100%; | |
| } | |
| #controls button:hover { | |
| background: #45a049; | |
| } | |
| #controls button.active { | |
| background: #ff5722; | |
| } | |
| .building-btn { | |
| background: #2196f3 !important; | |
| } | |
| .building-btn:hover { | |
| background: #1976d2 !important; | |
| } | |
| #info { | |
| position: absolute; | |
| bottom: 10px; | |
| left: 10px; | |
| background: rgba(0, 0, 0, 0.8); | |
| color: white; | |
| padding: 10px; | |
| border-radius: 5px; | |
| font-size: 14px; | |
| } | |
| #mode-indicator { | |
| position: absolute; | |
| top: 10px; | |
| right: 10px; | |
| background: rgba(0, 0, 0, 0.8); | |
| color: white; | |
| padding: 10px 20px; | |
| border-radius: 5px; | |
| font-weight: bold; | |
| } | |
| #preview-indicator { | |
| position: absolute; | |
| top: 50px; | |
| right: 10px; | |
| background: rgba(0, 255, 0, 0.8); | |
| color: white; | |
| padding: 5px 10px; | |
| border-radius: 5px; | |
| font-size: 12px; | |
| display: none; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="canvas-container"> | |
| <div id="controls"> | |
| <h3>City Builder</h3> | |
| <button id="mode-toggle">Toggle Mode: BUILD</button> | |
| <hr /> | |
| <div id="build-controls"> | |
| <h4>Buildings:</h4> | |
| <button class="building-btn active" data-type="small"> | |
| Small House | |
| </button> | |
| <button class="building-btn" data-type="medium"> | |
| Medium Building | |
| </button> | |
| <button class="building-btn" data-type="tall">Skyscraper</button> | |
| <button class="building-btn" data-type="wide">Shopping Center</button> | |
| <button class="building-btn" data-type="factory">Factory</button> | |
| <hr /> | |
| <h4>Camera Controls:</h4> | |
| <button id="rotate-left">← Rotate Left</button> | |
| <button id="rotate-right">→ Rotate Right</button> | |
| <button id="zoom-in">+ Zoom In</button> | |
| <button id="zoom-out">- Zoom Out</button> | |
| </div> | |
| <hr /> | |
| <button id="clear-all">Clear All Buildings</button> | |
| </div> | |
| <div id="info"> | |
| <div><b>BUILD mode:</b> Click on green areas to place buildings</div> | |
| <div>Use camera control buttons or Q/E to rotate, +/- to zoom</div> | |
| <div><b>DRIVE mode:</b> WASD/Arrows to drive</div> | |
| </div> | |
| <div id="mode-indicator">MODE: BUILD</div> | |
| <div id="preview-indicator">Click to place building</div> | |
| </div> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> | |
| <script> | |
| // Global variables | |
| let scene, camera, renderer; | |
| let mode = "BUILD"; // BUILD or DRIVE | |
| let selectedBuildingType = "small"; | |
| let buildings = []; | |
| let trafficCars = []; | |
| let playerCar; | |
| let clock = new THREE.Clock(); | |
| let previewBuilding = null; | |
| let validPlacement = false; | |
| // Camera controls | |
| let cameraAngle = 0; | |
| let cameraDistance = 50; | |
| let cameraHeight = 30; | |
| // Player car controls | |
| let keys = {}; | |
| let mouseX = 0; | |
| let mouseY = 0; | |
| // Raycaster for building placement | |
| const raycaster = new THREE.Raycaster(); | |
| const mouse = new THREE.Vector2(); | |
| // Building configurations | |
| const buildingConfigs = { | |
| small: { width: 4, height: 3, depth: 4, color: 0x8b4513 }, | |
| medium: { width: 6, height: 6, depth: 6, color: 0x696969 }, | |
| tall: { width: 8, height: 20, depth: 8, color: 0x4682b4 }, | |
| wide: { width: 15, height: 4, depth: 8, color: 0xdc143c }, | |
| factory: { width: 12, height: 6, depth: 12, color: 0x708090 }, | |
| }; | |
| // Initialize scene | |
| function init() { | |
| // Scene setup | |
| scene = new THREE.Scene(); | |
| scene.background = new THREE.Color(0x87ceeb); | |
| scene.fog = new THREE.Fog(0x87ceeb, 50, 200); | |
| // Camera setup | |
| camera = new THREE.PerspectiveCamera( | |
| 75, | |
| window.innerWidth / window.innerHeight, | |
| 0.1, | |
| 1000, | |
| ); | |
| updateBuildCamera(); | |
| // Renderer setup | |
| renderer = new THREE.WebGLRenderer({ antialias: true }); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| renderer.shadowMap.enabled = true; | |
| renderer.shadowMap.type = THREE.PCFSoftShadowMap; | |
| document | |
| .getElementById("canvas-container") | |
| .appendChild(renderer.domElement); | |
| // Lights | |
| const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); | |
| scene.add(ambientLight); | |
| const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); | |
| directionalLight.position.set(50, 100, 50); | |
| directionalLight.castShadow = true; | |
| directionalLight.shadow.camera.left = -100; | |
| directionalLight.shadow.camera.right = 100; | |
| directionalLight.shadow.camera.top = 100; | |
| directionalLight.shadow.camera.bottom = -100; | |
| directionalLight.shadow.mapSize.width = 2048; | |
| directionalLight.shadow.mapSize.height = 2048; | |
| scene.add(directionalLight); | |
| // Create ground | |
| createGround(); | |
| // Create roads | |
| createRoads(); | |
| // Create player car | |
| createPlayerCar(); | |
| // Create initial traffic | |
| createTraffic(); | |
| // Create preview building | |
| createPreviewBuilding(); | |
| // Event listeners | |
| setupEventListeners(); | |
| // Start animation | |
| animate(); | |
| } | |
| function createGround() { | |
| // Main ground | |
| const groundGeometry = new THREE.PlaneGeometry(200, 200); | |
| const groundMaterial = new THREE.MeshStandardMaterial({ | |
| color: 0x3a5f3a, | |
| roughness: 0.8, | |
| }); | |
| const ground = new THREE.Mesh(groundGeometry, groundMaterial); | |
| ground.rotation.x = -Math.PI / 2; | |
| ground.receiveShadow = true; | |
| ground.name = "ground"; | |
| scene.add(ground); | |
| } | |
| function createRoads() { | |
| const roadMaterial = new THREE.MeshStandardMaterial({ | |
| color: 0x333333, | |
| roughness: 0.7, | |
| }); | |
| // Main roads | |
| const roadGeometry = new THREE.PlaneGeometry(10, 200); | |
| // North-South road | |
| const road1 = new THREE.Mesh(roadGeometry, roadMaterial); | |
| road1.rotation.x = -Math.PI / 2; | |
| road1.position.y = 0.01; | |
| road1.receiveShadow = true; | |
| road1.name = "road"; | |
| scene.add(road1); | |
| // East-West road | |
| const road2 = new THREE.Mesh(roadGeometry, roadMaterial); | |
| road2.rotation.x = -Math.PI / 2; | |
| road2.rotation.z = Math.PI / 2; | |
| road2.position.y = 0.01; | |
| road2.receiveShadow = true; | |
| road2.name = "road"; | |
| scene.add(road2); | |
| // Road lines | |
| const lineMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00 }); | |
| const lineGeometry = new THREE.PlaneGeometry(0.3, 4); | |
| for (let i = -100; i <= 100; i += 8) { | |
| if (Math.abs(i) > 5) { | |
| const line1 = new THREE.Mesh(lineGeometry, lineMaterial); | |
| line1.rotation.x = -Math.PI / 2; | |
| line1.position.set(0, 0.02, i); | |
| scene.add(line1); | |
| const line2 = new THREE.Mesh(lineGeometry, lineMaterial); | |
| line2.rotation.x = -Math.PI / 2; | |
| line2.rotation.z = Math.PI / 2; | |
| line2.position.set(i, 0.02, 0); | |
| scene.add(line2); | |
| } | |
| } | |
| } | |
| function createPlayerCar() { | |
| const carGroup = new THREE.Group(); | |
| // Car body | |
| const bodyGeometry = new THREE.BoxGeometry(2, 1, 4); | |
| const bodyMaterial = new THREE.MeshStandardMaterial({ | |
| color: 0xff0000, | |
| }); | |
| const carBody = new THREE.Mesh(bodyGeometry, bodyMaterial); | |
| carBody.position.y = 0.5; | |
| carBody.castShadow = true; | |
| carGroup.add(carBody); | |
| // Car roof | |
| const roofGeometry = new THREE.BoxGeometry(1.8, 0.8, 2); | |
| const roofMaterial = new THREE.MeshStandardMaterial({ | |
| color: 0xcc0000, | |
| }); | |
| const carRoof = new THREE.Mesh(roofGeometry, roofMaterial); | |
| carRoof.position.y = 1.2; | |
| carRoof.castShadow = true; | |
| carGroup.add(carRoof); | |
| // Wheels | |
| const wheelGeometry = new THREE.CylinderGeometry(0.3, 0.3, 0.2, 16); | |
| const wheelMaterial = new THREE.MeshStandardMaterial({ | |
| color: 0x000000, | |
| }); | |
| const wheelPositions = [ | |
| { x: -0.8, z: 1.5 }, | |
| { x: 0.8, z: 1.5 }, | |
| { x: -0.8, z: -1.5 }, | |
| { x: 0.8, z: -1.5 }, | |
| ]; | |
| wheelPositions.forEach((pos) => { | |
| const wheel = new THREE.Mesh(wheelGeometry, wheelMaterial); | |
| wheel.rotation.z = Math.PI / 2; | |
| wheel.position.set(pos.x, 0.3, pos.z); | |
| wheel.castShadow = true; | |
| carGroup.add(wheel); | |
| }); | |
| carGroup.position.set(0, 0, -20); | |
| carGroup.visible = false; | |
| scene.add(carGroup); | |
| playerCar = { | |
| mesh: carGroup, | |
| velocity: new THREE.Vector3(), | |
| speed: 0, | |
| maxSpeed: 30, | |
| acceleration: 20, | |
| deceleration: 30, | |
| turnSpeed: 2, | |
| }; | |
| } | |
| function createTraffic() { | |
| const carColors = [0x0000ff, 0x00ff00, 0xffff00, 0xff00ff, 0x00ffff]; | |
| for (let i = 0; i < 10; i++) { | |
| const car = createTrafficCar(carColors[i % carColors.length]); | |
| // Random position on roads | |
| if (Math.random() > 0.5) { | |
| // On vertical road | |
| car.position.set( | |
| Math.random() > 0.5 ? 3 : -3, | |
| 0, | |
| (Math.random() - 0.5) * 180, | |
| ); | |
| car.userData.direction = Math.random() > 0.5 ? "north" : "south"; | |
| car.rotation.y = car.userData.direction === "north" ? 0 : Math.PI; | |
| } else { | |
| // On horizontal road | |
| car.position.set( | |
| (Math.random() - 0.5) * 180, | |
| 0, | |
| Math.random() > 0.5 ? 3 : -3, | |
| ); | |
| car.userData.direction = Math.random() > 0.5 ? "east" : "west"; | |
| car.rotation.y = | |
| car.userData.direction === "east" ? -Math.PI / 2 : Math.PI / 2; | |
| } | |
| car.userData.speed = 5 + Math.random() * 10; | |
| trafficCars.push(car); | |
| scene.add(car); | |
| } | |
| } | |
| function createTrafficCar(color) { | |
| const carGroup = new THREE.Group(); | |
| // Car body | |
| const bodyGeometry = new THREE.BoxGeometry(1.8, 0.9, 3.6); | |
| const bodyMaterial = new THREE.MeshStandardMaterial({ color: color }); | |
| const carBody = new THREE.Mesh(bodyGeometry, bodyMaterial); | |
| carBody.position.y = 0.45; | |
| carBody.castShadow = true; | |
| carGroup.add(carBody); | |
| // Car roof | |
| const roofGeometry = new THREE.BoxGeometry(1.6, 0.7, 1.8); | |
| const roofMaterial = new THREE.MeshStandardMaterial({ color: color }); | |
| const carRoof = new THREE.Mesh(roofGeometry, roofMaterial); | |
| carRoof.position.y = 1.1; | |
| carRoof.castShadow = true; | |
| carGroup.add(carRoof); | |
| return carGroup; | |
| } | |
| function createPreviewBuilding() { | |
| const config = buildingConfigs[selectedBuildingType]; | |
| const geometry = new THREE.BoxGeometry( | |
| config.width, | |
| config.height, | |
| config.depth, | |
| ); | |
| const material = new THREE.MeshStandardMaterial({ | |
| color: config.color, | |
| transparent: true, | |
| opacity: 0.5, | |
| }); | |
| previewBuilding = new THREE.Mesh(geometry, material); | |
| previewBuilding.position.y = config.height / 2; | |
| previewBuilding.visible = false; | |
| scene.add(previewBuilding); | |
| } | |
| function updatePreviewBuilding() { | |
| if (!previewBuilding || mode !== "BUILD") { | |
| if (previewBuilding) previewBuilding.visible = false; | |
| return; | |
| } | |
| const config = buildingConfigs[selectedBuildingType]; | |
| // Update geometry if building type changed | |
| previewBuilding.geometry.dispose(); | |
| previewBuilding.geometry = new THREE.BoxGeometry( | |
| config.width, | |
| config.height, | |
| config.depth, | |
| ); | |
| previewBuilding.position.y = config.height / 2; | |
| // Update material color | |
| const isValid = validPlacement ? 0x00ff00 : 0xff0000; | |
| previewBuilding.material.color.setHex(isValid); | |
| previewBuilding.material.opacity = 0.5; | |
| } | |
| function createBuilding(type, position) { | |
| const config = buildingConfigs[type]; | |
| const geometry = new THREE.BoxGeometry( | |
| config.width, | |
| config.height, | |
| config.depth, | |
| ); | |
| const material = new THREE.MeshStandardMaterial({ | |
| color: config.color, | |
| }); | |
| const building = new THREE.Mesh(geometry, material); | |
| building.position.copy(position); | |
| building.position.y = config.height / 2; | |
| building.castShadow = true; | |
| building.receiveShadow = true; | |
| building.userData.type = type; | |
| // Add windows for tall buildings | |
| if (type === "tall" || type === "medium") { | |
| addWindows(building, config); | |
| } | |
| buildings.push(building); | |
| scene.add(building); | |
| } | |
| function addWindows(building, config) { | |
| const windowGeometry = new THREE.PlaneGeometry(0.8, 1.2); | |
| const windowMaterial = new THREE.MeshBasicMaterial({ | |
| color: 0x87ceeb, | |
| emissive: 0x87ceeb, | |
| emissiveIntensity: 0.2, | |
| }); | |
| const windowSpacing = 2; | |
| const windowsPerFloor = Math.floor(config.width / windowSpacing); | |
| const floors = Math.floor(config.height / 3); | |
| for (let floor = 1; floor < floors; floor++) { | |
| for (let i = 0; i < windowsPerFloor; i++) { | |
| // Front windows | |
| const windowFront = new THREE.Mesh(windowGeometry, windowMaterial); | |
| windowFront.position.set( | |
| (i - windowsPerFloor / 2 + 0.5) * windowSpacing, | |
| floor * 3 - config.height / 2, | |
| config.depth / 2 + 0.01, | |
| ); | |
| building.add(windowFront); | |
| // Back windows | |
| const windowBack = new THREE.Mesh(windowGeometry, windowMaterial); | |
| windowBack.position.set( | |
| (i - windowsPerFloor / 2 + 0.5) * windowSpacing, | |
| floor * 3 - config.height / 2, | |
| -config.depth / 2 - 0.01, | |
| ); | |
| windowBack.rotation.y = Math.PI; | |
| building.add(windowBack); | |
| } | |
| } | |
| } | |
| function updateBuildCamera() { | |
| camera.position.x = Math.sin(cameraAngle) * cameraDistance; | |
| camera.position.y = cameraHeight; | |
| camera.position.z = Math.cos(cameraAngle) * cameraDistance; | |
| camera.lookAt(0, 0, 0); | |
| } | |
| function updateDriveCamera() { | |
| if (playerCar && playerCar.mesh) { | |
| // Third person camera following the car | |
| const carPos = playerCar.mesh.position; | |
| const carRotation = playerCar.mesh.rotation.y; | |
| const cameraOffset = new THREE.Vector3( | |
| Math.sin(carRotation) * 10, | |
| 5, | |
| Math.cos(carRotation) * 10, | |
| ); | |
| camera.position.x = carPos.x + cameraOffset.x; | |
| camera.position.y = carPos.y + cameraOffset.y; | |
| camera.position.z = carPos.z + cameraOffset.z; | |
| camera.lookAt(carPos); | |
| } | |
| } | |
| function handleMouseMove(event) { | |
| // Update mouse coordinates | |
| mouse.x = (event.clientX / window.innerWidth) * 2 - 1; | |
| mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; | |
| if (mode === "BUILD") { | |
| // Update raycaster | |
| raycaster.setFromCamera(mouse, camera); | |
| // Find ground intersection | |
| const ground = scene.getObjectByName("ground"); | |
| const intersects = raycaster.intersectObject(ground); | |
| if (intersects.length > 0) { | |
| const point = intersects[0].point; | |
| // Check if position is valid (not on road) | |
| validPlacement = Math.abs(point.x) > 8 || Math.abs(point.z) > 8; | |
| // Update preview building position | |
| if (previewBuilding) { | |
| previewBuilding.position.x = point.x; | |
| previewBuilding.position.z = point.z; | |
| previewBuilding.visible = true; | |
| updatePreviewBuilding(); | |
| // Update preview indicator | |
| const indicator = document.getElementById("preview-indicator"); | |
| indicator.style.display = "block"; | |
| indicator.style.backgroundColor = validPlacement | |
| ? "rgba(0, 255, 0, 0.8)" | |
| : "rgba(255, 0, 0, 0.8)"; | |
| indicator.textContent = validPlacement | |
| ? "Click to place building" | |
| : "Cannot place on road"; | |
| } | |
| } | |
| } | |
| } | |
| function handleClick(event) { | |
| if (mode !== "BUILD") return; | |
| // Update mouse coordinates | |
| mouse.x = (event.clientX / window.innerWidth) * 2 - 1; | |
| mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; | |
| // Update raycaster | |
| raycaster.setFromCamera(mouse, camera); | |
| // Find ground intersection | |
| const ground = scene.getObjectByName("ground"); | |
| const intersects = raycaster.intersectObject(ground); | |
| if (intersects.length > 0) { | |
| const position = intersects[0].point; | |
| // Check if position is not on road | |
| if (Math.abs(position.x) > 8 || Math.abs(position.z) > 8) { | |
| createBuilding(selectedBuildingType, position); | |
| } | |
| } | |
| } | |
| function handleKeyDown(event) { | |
| keys[event.key.toLowerCase()] = true; | |
| // Camera controls in build mode | |
| if (mode === "BUILD") { | |
| switch (event.key.toLowerCase()) { | |
| case "q": | |
| cameraAngle -= 0.1; | |
| updateBuildCamera(); | |
| break; | |
| case "e": | |
| cameraAngle += 0.1; | |
| updateBuildCamera(); | |
| break; | |
| case "+": | |
| case "=": | |
| cameraDistance = Math.max(20, cameraDistance - 5); | |
| updateBuildCamera(); | |
| break; | |
| case "-": | |
| case "_": | |
| cameraDistance = Math.min(100, cameraDistance + 5); | |
| updateBuildCamera(); | |
| break; | |
| } | |
| } | |
| } | |
| function handleKeyUp(event) { | |
| keys[event.key.toLowerCase()] = false; | |
| } | |
| function updatePlayerCar(deltaTime) { | |
| if (mode !== "DRIVE" || !playerCar.mesh.visible) return; | |
| const car = playerCar; | |
| // Acceleration/Deceleration | |
| if (keys["w"] || keys["arrowup"]) { | |
| car.speed = Math.min( | |
| car.speed + car.acceleration * deltaTime, | |
| car.maxSpeed, | |
| ); | |
| } else if (keys["s"] || keys["arrowdown"]) { | |
| car.speed = Math.max( | |
| car.speed - car.acceleration * deltaTime, | |
| -car.maxSpeed / 2, | |
| ); | |
| } else { | |
| // Friction | |
| if (car.speed > 0) { | |
| car.speed = Math.max(car.speed - car.deceleration * deltaTime, 0); | |
| } else { | |
| car.speed = Math.min(car.speed + car.deceleration * deltaTime, 0); | |
| } | |
| } | |
| // Steering | |
| if (car.speed !== 0) { | |
| if (keys["a"] || keys["arrowleft"]) { | |
| car.mesh.rotation.y += | |
| car.turnSpeed * deltaTime * (car.speed > 0 ? 1 : -1); | |
| } | |
| if (keys["d"] || keys["arrowright"]) { | |
| car.mesh.rotation.y -= | |
| car.turnSpeed * deltaTime * (car.speed > 0 ? 1 : -1); | |
| } | |
| } | |
| // Update position | |
| const direction = new THREE.Vector3( | |
| -Math.sin(car.mesh.rotation.y), | |
| 0, | |
| -Math.cos(car.mesh.rotation.y), | |
| ); | |
| car.mesh.position.add(direction.multiplyScalar(car.speed * deltaTime)); | |
| // Keep car on ground | |
| car.mesh.position.y = 0; | |
| // Boundary checking | |
| const boundary = 95; | |
| car.mesh.position.x = Math.max( | |
| -boundary, | |
| Math.min(boundary, car.mesh.position.x), | |
| ); | |
| car.mesh.position.z = Math.max( | |
| -boundary, | |
| Math.min(boundary, car.mesh.position.z), | |
| ); | |
| } | |
| function updateTraffic(deltaTime) { | |
| trafficCars.forEach((car) => { | |
| const direction = car.userData.direction; | |
| const speed = car.userData.speed * deltaTime; | |
| switch (direction) { | |
| case "north": | |
| car.position.z -= speed; | |
| if (car.position.z < -95) car.position.z = 95; | |
| break; | |
| case "south": | |
| car.position.z += speed; | |
| if (car.position.z > 95) car.position.z = -95; | |
| break; | |
| case "east": | |
| car.position.x += speed; | |
| if (car.position.x > 95) car.position.x = -95; | |
| break; | |
| case "west": | |
| car.position.x -= speed; | |
| if (car.position.x < -95) car.position.x = 95; | |
| break; | |
| } | |
| }); | |
| } | |
| function toggleMode() { | |
| mode = mode === "BUILD" ? "DRIVE" : "BUILD"; | |
| document.getElementById("mode-toggle").textContent = | |
| `Toggle Mode: ${mode}`; | |
| document.getElementById("mode-indicator").textContent = `MODE: ${mode}`; | |
| if (mode === "DRIVE") { | |
| playerCar.mesh.visible = true; | |
| updateDriveCamera(); | |
| document.getElementById("build-controls").style.display = "none"; | |
| document.getElementById("preview-indicator").style.display = "none"; | |
| if (previewBuilding) previewBuilding.visible = false; | |
| } else { | |
| playerCar.mesh.visible = false; | |
| updateBuildCamera(); | |
| document.getElementById("build-controls").style.display = "block"; | |
| } | |
| } | |
| function clearAllBuildings() { | |
| buildings.forEach((building) => { | |
| scene.remove(building); | |
| }); | |
| buildings = []; | |
| } | |
| function setupEventListeners() { | |
| // Mode toggle | |
| document | |
| .getElementById("mode-toggle") | |
| .addEventListener("click", toggleMode); | |
| // Building selection | |
| document.querySelectorAll(".building-btn").forEach((btn) => { | |
| btn.addEventListener("click", (e) => { | |
| selectedBuildingType = e.target.dataset.type; | |
| document | |
| .querySelectorAll(".building-btn") | |
| .forEach((b) => b.classList.remove("active")); | |
| e.target.classList.add("active"); | |
| updatePreviewBuilding(); | |
| }); | |
| }); | |
| // Camera control buttons | |
| document.getElementById("rotate-left").addEventListener("click", () => { | |
| cameraAngle -= 0.2; | |
| updateBuildCamera(); | |
| }); | |
| document | |
| .getElementById("rotate-right") | |
| .addEventListener("click", () => { | |
| cameraAngle += 0.2; | |
| updateBuildCamera(); | |
| }); | |
| document.getElementById("zoom-in").addEventListener("click", () => { | |
| cameraDistance = Math.max(20, cameraDistance - 5); | |
| updateBuildCamera(); | |
| }); | |
| document.getElementById("zoom-out").addEventListener("click", () => { | |
| cameraDistance = Math.min(100, cameraDistance + 5); | |
| updateBuildCamera(); | |
| }); | |
| // Clear buildings | |
| document | |
| .getElementById("clear-all") | |
| .addEventListener("click", clearAllBuildings); | |
| // Mouse and keyboard controls | |
| renderer.domElement.addEventListener("mousemove", handleMouseMove); | |
| renderer.domElement.addEventListener("click", handleClick); | |
| document.addEventListener("keydown", handleKeyDown); | |
| document.addEventListener("keyup", handleKeyUp); | |
| // Window resize | |
| window.addEventListener("resize", () => { | |
| camera.aspect = window.innerWidth / window.innerHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| }); | |
| } | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| const deltaTime = clock.getDelta(); | |
| // Update based on mode | |
| if (mode === "DRIVE") { | |
| updatePlayerCar(deltaTime); | |
| updateDriveCamera(); | |
| } else { | |
| updateBuildCamera(); | |
| } | |
| // Always update traffic | |
| updateTraffic(deltaTime); | |
| renderer.render(scene, camera); | |
| } | |
| // Initialize the application | |
| init(); | |
| </script> | |
| </body> | |
| </html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!doctype html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>3D City Builder & Fire Rescue</title> | |
| <style> | |
| body { | |
| margin: 0; | |
| padding: 0; | |
| font-family: Arial, sans-serif; | |
| overflow: hidden; | |
| } | |
| #canvas-container { | |
| position: relative; | |
| width: 100vw; | |
| height: 100vh; | |
| } | |
| #controls { | |
| position: absolute; | |
| top: 10px; | |
| left: 10px; | |
| background: rgba(0, 0, 0, 0.8); | |
| color: white; | |
| padding: 15px; | |
| border-radius: 10px; | |
| z-index: 100; | |
| max-height: 80vh; | |
| overflow-y: auto; | |
| } | |
| #controls button { | |
| display: block; | |
| margin: 5px 0; | |
| padding: 8px 15px; | |
| background: #4caf50; | |
| color: white; | |
| border: none; | |
| border-radius: 5px; | |
| cursor: pointer; | |
| width: 100%; | |
| } | |
| #controls button:hover { | |
| background: #45a049; | |
| } | |
| #controls button.active { | |
| background: #ff5722; | |
| } | |
| .building-btn { | |
| background: #2196f3 !important; | |
| } | |
| .building-btn:hover { | |
| background: #1976d2 !important; | |
| } | |
| #vehicle-btn { | |
| background: #ff5722 !important; | |
| } | |
| #vehicle-btn:hover { | |
| background: #d84315 !important; | |
| } | |
| #info { | |
| position: absolute; | |
| bottom: 10px; | |
| left: 10px; | |
| background: rgba(0, 0, 0, 0.8); | |
| color: white; | |
| padding: 10px; | |
| border-radius: 5px; | |
| font-size: 14px; | |
| } | |
| #mode-indicator { | |
| position: absolute; | |
| top: 10px; | |
| right: 10px; | |
| background: rgba(0, 0, 0, 0.8); | |
| color: white; | |
| padding: 10px 20px; | |
| border-radius: 5px; | |
| font-weight: bold; | |
| } | |
| #preview-indicator { | |
| position: absolute; | |
| top: 50px; | |
| right: 10px; | |
| background: rgba(0, 255, 0, 0.8); | |
| color: white; | |
| padding: 5px 10px; | |
| border-radius: 5px; | |
| font-size: 12px; | |
| display: none; | |
| } | |
| #fire-alert { | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| background: rgba(255, 0, 0, 0.9); | |
| color: white; | |
| padding: 20px; | |
| border-radius: 10px; | |
| font-size: 24px; | |
| font-weight: bold; | |
| display: none; | |
| animation: pulse 1s infinite; | |
| text-align: center; | |
| z-index: 200; | |
| } | |
| @keyframes pulse { | |
| 0% { | |
| transform: translate(-50%, -50%) scale(1); | |
| } | |
| 50% { | |
| transform: translate(-50%, -50%) scale(1.1); | |
| } | |
| 100% { | |
| transform: translate(-50%, -50%) scale(1); | |
| } | |
| } | |
| #stats { | |
| position: absolute; | |
| top: 10px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| background: rgba(0, 0, 0, 0.8); | |
| color: white; | |
| padding: 10px 20px; | |
| border-radius: 5px; | |
| display: flex; | |
| gap: 20px; | |
| } | |
| .stat { | |
| text-align: center; | |
| } | |
| .stat-value { | |
| font-size: 24px; | |
| font-weight: bold; | |
| } | |
| .stat-label { | |
| font-size: 12px; | |
| } | |
| #water-meter { | |
| position: absolute; | |
| bottom: 10px; | |
| right: 10px; | |
| background: rgba(0, 0, 0, 0.8); | |
| color: white; | |
| padding: 10px; | |
| border-radius: 5px; | |
| width: 200px; | |
| } | |
| #water-bar { | |
| width: 100%; | |
| height: 20px; | |
| background: #333; | |
| border-radius: 10px; | |
| overflow: hidden; | |
| margin-top: 5px; | |
| } | |
| #water-fill { | |
| height: 100%; | |
| background: #2196f3; | |
| width: 100%; | |
| transition: width 0.3s; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="canvas-container"> | |
| <div id="controls"> | |
| <h3>City Builder & Fire Rescue</h3> | |
| <button id="mode-toggle">Toggle Mode: BUILD</button> | |
| <hr /> | |
| <h4>Vehicles:</h4> | |
| <button id="vehicle-select">Vehicle: Regular Car</button> | |
| <hr /> | |
| <div id="build-controls"> | |
| <h4>Buildings:</h4> | |
| <button class="building-btn active" data-type="small"> | |
| Small House | |
| </button> | |
| <button class="building-btn" data-type="medium"> | |
| Office Building | |
| </button> | |
| <button class="building-btn" data-type="tall">Skyscraper</button> | |
| <button class="building-btn" data-type="wide">Shopping Mall</button> | |
| <button class="building-btn" data-type="factory">Factory</button> | |
| <hr /> | |
| <h4>Camera Controls:</h4> | |
| <button id="rotate-left">← Rotate Left</button> | |
| <button id="rotate-right">→ Rotate Right</button> | |
| <button id="zoom-in">+ Zoom In</button> | |
| <button id="zoom-out">- Zoom Out</button> | |
| </div> | |
| <hr /> | |
| <button id="clear-all">Clear All Buildings</button> | |
| </div> | |
| <div id="info"> | |
| <div><b>BUILD mode:</b> Click to place buildings</div> | |
| <div><b>DRIVE mode:</b> WASD/Arrows to drive</div> | |
| <div><b>Firetruck:</b> SPACE to spray water</div> | |
| </div> | |
| <div id="mode-indicator">MODE: BUILD</div> | |
| <div id="preview-indicator">Click to place building</div> | |
| <div id="fire-alert"> | |
| 🔥 FIRE EMERGENCY! 🔥<br /> | |
| <span id="fire-location"></span> | |
| </div> | |
| <div id="stats"> | |
| <div class="stat"> | |
| <div class="stat-value" id="fires-active">0</div> | |
| <div class="stat-label">Active Fires</div> | |
| </div> | |
| <div class="stat"> | |
| <div class="stat-value" id="fires-extinguished">0</div> | |
| <div class="stat-label">Your Score</div> | |
| </div> | |
| <div class="stat"> | |
| <div class="stat-value" id="helicopter-score">0</div> | |
| <div class="stat-label">Helicopter Score</div> | |
| </div> | |
| </div> | |
| <div id="water-meter" style="display: none"> | |
| <div>Water Tank</div> | |
| <div id="water-bar"><div id="water-fill"></div></div> | |
| </div> | |
| </div> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> | |
| <script> | |
| // Global variables | |
| let scene, camera, renderer; | |
| let mode = "BUILD"; // BUILD or DRIVE | |
| let selectedBuildingType = "small"; | |
| let selectedVehicle = "car"; // car or firetruck | |
| let buildings = []; | |
| let trafficCars = []; | |
| let playerCar, firetruck, currentVehicle; | |
| let helicopter; | |
| let clock = new THREE.Clock(); | |
| let previewBuilding = null; | |
| let validPlacement = false; | |
| // Fire system | |
| let fireSystem = { | |
| fires: [], | |
| particles: [], | |
| nextFireTime: 10, | |
| playerScore: 0, | |
| helicopterScore: 0, | |
| waterLevel: 100, | |
| }; | |
| // Camera controls | |
| let cameraAngle = 0; | |
| let cameraDistance = 50; | |
| let cameraHeight = 30; | |
| // Player controls | |
| let keys = {}; | |
| let mouseX = 0; | |
| let mouseY = 0; | |
| // Raycaster for building placement | |
| const raycaster = new THREE.Raycaster(); | |
| const mouse = new THREE.Vector2(); | |
| // Building configurations | |
| const buildingConfigs = { | |
| small: { width: 4, height: 3, depth: 4, color: 0x8b4513, floors: 1 }, | |
| medium: { width: 6, height: 8, depth: 6, color: 0x696969, floors: 4 }, | |
| tall: { width: 8, height: 20, depth: 8, color: 0x4682b4, floors: 10 }, | |
| wide: { width: 15, height: 6, depth: 8, color: 0xdc143c, floors: 2 }, | |
| factory: { | |
| width: 12, | |
| height: 8, | |
| depth: 12, | |
| color: 0x708090, | |
| floors: 2, | |
| }, | |
| }; | |
| // Initialize scene | |
| function init() { | |
| // Scene setup | |
| scene = new THREE.Scene(); | |
| scene.background = new THREE.Color(0x87ceeb); | |
| scene.fog = new THREE.Fog(0x87ceeb, 50, 200); | |
| // Camera setup | |
| camera = new THREE.PerspectiveCamera( | |
| 75, | |
| window.innerWidth / window.innerHeight, | |
| 0.1, | |
| 1000, | |
| ); | |
| updateBuildCamera(); | |
| // Renderer setup | |
| renderer = new THREE.WebGLRenderer({ antialias: true }); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| renderer.shadowMap.enabled = true; | |
| renderer.shadowMap.type = THREE.PCFSoftShadowMap; | |
| document | |
| .getElementById("canvas-container") | |
| .appendChild(renderer.domElement); | |
| // Lighting setup | |
| const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); | |
| scene.add(ambientLight); | |
| const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); | |
| directionalLight.position.set(50, 100, 50); | |
| directionalLight.castShadow = true; | |
| directionalLight.shadow.camera.left = -100; | |
| directionalLight.shadow.camera.right = 100; | |
| directionalLight.shadow.camera.top = 100; | |
| directionalLight.shadow.camera.bottom = -100; | |
| directionalLight.shadow.mapSize.width = 2048; | |
| directionalLight.shadow.mapSize.height = 2048; | |
| scene.add(directionalLight); | |
| // Create world elements | |
| createGround(); | |
| createRoads(); | |
| // Create vehicles | |
| createPlayerCar(); | |
| createFiretruck(); | |
| currentVehicle = playerCar; | |
| // Create helicopter | |
| createHelicopter(); | |
| // Create initial scene | |
| createTraffic(); | |
| createPreviewBuilding(); | |
| createInitialBuildings(); | |
| // Setup event listeners | |
| setupEventListeners(); | |
| // Start animation loop | |
| animate(); | |
| } | |
| function createGround() { | |
| // Main ground with texture-like appearance | |
| const groundGeometry = new THREE.PlaneGeometry(200, 200); | |
| const groundMaterial = new THREE.MeshStandardMaterial({ | |
| color: 0x3a5f3a, | |
| roughness: 0.8, | |
| metalness: 0.1, | |
| }); | |
| const ground = new THREE.Mesh(groundGeometry, groundMaterial); | |
| ground.rotation.x = -Math.PI / 2; | |
| ground.receiveShadow = true; | |
| ground.name = "ground"; | |
| scene.add(ground); | |
| } | |
| function createRoads() { | |
| const roadMaterial = new THREE.MeshStandardMaterial({ | |
| color: 0x333333, | |
| roughness: 0.7, | |
| }); | |
| // Main roads | |
| const roadGeometry = new THREE.PlaneGeometry(10, 200); | |
| // North-South road | |
| const road1 = new THREE.Mesh(roadGeometry, roadMaterial); | |
| road1.rotation.x = -Math.PI / 2; | |
| road1.position.y = 0.01; | |
| road1.receiveShadow = true; | |
| road1.name = "road"; | |
| scene.add(road1); | |
| // East-West road | |
| const road2 = new THREE.Mesh(roadGeometry, roadMaterial); | |
| road2.rotation.x = -Math.PI / 2; | |
| road2.rotation.z = Math.PI / 2; | |
| road2.position.y = 0.01; | |
| road2.receiveShadow = true; | |
| road2.name = "road"; | |
| scene.add(road2); | |
| // Road lines | |
| const lineMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00 }); | |
| const lineGeometry = new THREE.PlaneGeometry(0.3, 4); | |
| for (let i = -100; i <= 100; i += 8) { | |
| if (Math.abs(i) > 5) { | |
| // Vertical road lines | |
| const line1 = new THREE.Mesh(lineGeometry, lineMaterial); | |
| line1.rotation.x = -Math.PI / 2; | |
| line1.position.set(0, 0.02, i); | |
| scene.add(line1); | |
| // Horizontal road lines | |
| const line2 = new THREE.Mesh(lineGeometry, lineMaterial); | |
| line2.rotation.x = -Math.PI / 2; | |
| line2.rotation.z = Math.PI / 2; | |
| line2.position.set(i, 0.02, 0); | |
| scene.add(line2); | |
| } | |
| } | |
| } | |
| function createPlayerCar() { | |
| const carGroup = new THREE.Group(); | |
| // Car body | |
| const bodyGeometry = new THREE.BoxGeometry(2, 1, 4); | |
| const bodyMaterial = new THREE.MeshStandardMaterial({ | |
| color: 0xff0000, | |
| }); | |
| const carBody = new THREE.Mesh(bodyGeometry, bodyMaterial); | |
| carBody.position.y = 0.5; | |
| carBody.castShadow = true; | |
| carGroup.add(carBody); | |
| // Car roof | |
| const roofGeometry = new THREE.BoxGeometry(1.8, 0.8, 2); | |
| const roofMaterial = new THREE.MeshStandardMaterial({ | |
| color: 0xcc0000, | |
| }); | |
| const carRoof = new THREE.Mesh(roofGeometry, roofMaterial); | |
| carRoof.position.y = 1.2; | |
| carRoof.castShadow = true; | |
| carGroup.add(carRoof); | |
| // Wheels | |
| const wheelGeometry = new THREE.CylinderGeometry(0.3, 0.3, 0.2, 16); | |
| const wheelMaterial = new THREE.MeshStandardMaterial({ | |
| color: 0x000000, | |
| }); | |
| const wheelPositions = [ | |
| { x: -0.8, z: 1.5 }, | |
| { x: 0.8, z: 1.5 }, | |
| { x: -0.8, z: -1.5 }, | |
| { x: 0.8, z: -1.5 }, | |
| ]; | |
| wheelPositions.forEach((pos) => { | |
| const wheel = new THREE.Mesh(wheelGeometry, wheelMaterial); | |
| wheel.rotation.z = Math.PI / 2; | |
| wheel.position.set(pos.x, 0.3, pos.z); | |
| wheel.castShadow = true; | |
| carGroup.add(wheel); | |
| }); | |
| carGroup.position.set(0, 0, -20); | |
| carGroup.visible = false; | |
| scene.add(carGroup); | |
| playerCar = { | |
| mesh: carGroup, | |
| velocity: new THREE.Vector3(), | |
| speed: 0, | |
| maxSpeed: 30, | |
| acceleration: 20, | |
| deceleration: 30, | |
| turnSpeed: 2, | |
| }; | |
| } | |
| function createFiretruck() { | |
| const truckGroup = new THREE.Group(); | |
| // Truck body | |
| const bodyGeometry = new THREE.BoxGeometry(2.5, 1.5, 6); | |
| const bodyMaterial = new THREE.MeshStandardMaterial({ | |
| color: 0xff0000, | |
| }); | |
| const truckBody = new THREE.Mesh(bodyGeometry, bodyMaterial); | |
| truckBody.position.y = 0.75; | |
| truckBody.castShadow = true; | |
| truckGroup.add(truckBody); | |
| // Cab | |
| const cabGeometry = new THREE.BoxGeometry(2.3, 1.2, 2); | |
| const cabMaterial = new THREE.MeshStandardMaterial({ color: 0xcc0000 }); | |
| const cab = new THREE.Mesh(cabGeometry, cabMaterial); | |
| cab.position.set(0, 1.6, -1.5); | |
| cab.castShadow = true; | |
| truckGroup.add(cab); | |
| // Water tank | |
| const tankGeometry = new THREE.CylinderGeometry(0.8, 0.8, 3, 16); | |
| const tankMaterial = new THREE.MeshStandardMaterial({ | |
| color: 0xffffff, | |
| metalness: 0.7, | |
| roughness: 0.3, | |
| }); | |
| const tank = new THREE.Mesh(tankGeometry, tankMaterial); | |
| tank.rotation.z = Math.PI / 2; | |
| tank.position.set(0, 1.5, 1); | |
| tank.castShadow = true; | |
| truckGroup.add(tank); | |
| // Ladder | |
| const ladderGeometry = new THREE.BoxGeometry(0.5, 0.2, 4); | |
| const ladderMaterial = new THREE.MeshStandardMaterial({ | |
| color: 0xc0c0c0, | |
| metalness: 0.8, | |
| }); | |
| const ladder = new THREE.Mesh(ladderGeometry, ladderMaterial); | |
| ladder.position.set(0, 2, 0); | |
| ladder.castShadow = true; | |
| truckGroup.add(ladder); | |
| // Emergency lights | |
| const lightGeometry = new THREE.BoxGeometry(0.3, 0.3, 0.3); | |
| const lightMaterial = new THREE.MeshBasicMaterial({ color: 0x0000ff }); | |
| const light1 = new THREE.Mesh(lightGeometry, lightMaterial); | |
| light1.position.set(-0.8, 2.3, -1.5); | |
| truckGroup.add(light1); | |
| const light2 = new THREE.Mesh(lightGeometry, lightMaterial); | |
| light2.position.set(0.8, 2.3, -1.5); | |
| truckGroup.add(light2); | |
| // Water spray nozzle | |
| const nozzleGeometry = new THREE.ConeGeometry(0.3, 1, 8); | |
| const nozzleMaterial = new THREE.MeshStandardMaterial({ | |
| color: 0x808080, | |
| }); | |
| const nozzle = new THREE.Mesh(nozzleGeometry, nozzleMaterial); | |
| nozzle.rotation.z = -Math.PI / 2; | |
| nozzle.position.set(0, 1.5, -3.5); | |
| truckGroup.add(nozzle); | |
| // Wheels | |
| const wheelGeometry = new THREE.CylinderGeometry(0.4, 0.4, 0.3, 16); | |
| const wheelMaterial = new THREE.MeshStandardMaterial({ | |
| color: 0x000000, | |
| }); | |
| const wheelPositions = [ | |
| { x: -1, z: 2 }, | |
| { x: 1, z: 2 }, | |
| { x: -1, z: -2 }, | |
| { x: 1, z: -2 }, | |
| ]; | |
| wheelPositions.forEach((pos) => { | |
| const wheel = new THREE.Mesh(wheelGeometry, wheelMaterial); | |
| wheel.rotation.z = Math.PI / 2; | |
| wheel.position.set(pos.x, 0.4, pos.z); | |
| wheel.castShadow = true; | |
| truckGroup.add(wheel); | |
| }); | |
| truckGroup.position.set(10, 0, -20); | |
| truckGroup.visible = false; | |
| scene.add(truckGroup); | |
| firetruck = { | |
| mesh: truckGroup, | |
| velocity: new THREE.Vector3(), | |
| speed: 0, | |
| maxSpeed: 25, | |
| acceleration: 15, | |
| deceleration: 25, | |
| turnSpeed: 1.8, | |
| waterSpray: null, | |
| }; | |
| } | |
| function createHelicopter() { | |
| const heliGroup = new THREE.Group(); | |
| // Body | |
| const bodyGeometry = new THREE.SphereGeometry(2, 8, 6); | |
| const bodyMaterial = new THREE.MeshStandardMaterial({ | |
| color: 0x0000ff, | |
| metalness: 0.6, | |
| roughness: 0.4, | |
| }); | |
| const body = new THREE.Mesh(bodyGeometry, bodyMaterial); | |
| body.scale.set(1, 0.7, 1.5); | |
| body.castShadow = true; | |
| heliGroup.add(body); | |
| // Tail | |
| const tailGeometry = new THREE.CylinderGeometry(0.5, 1, 4, 8); | |
| const tail = new THREE.Mesh(tailGeometry, bodyMaterial); | |
| tail.rotation.z = Math.PI / 2; | |
| tail.position.set(0, 0.5, 3); | |
| tail.castShadow = true; | |
| heliGroup.add(tail); | |
| // Main rotor | |
| const rotorGeometry = new THREE.BoxGeometry(10, 0.1, 0.5); | |
| const rotorMaterial = new THREE.MeshStandardMaterial({ | |
| color: 0x333333, | |
| }); | |
| const mainRotor = new THREE.Mesh(rotorGeometry, rotorMaterial); | |
| mainRotor.position.y = 1.5; | |
| heliGroup.add(mainRotor); | |
| // Tail rotor | |
| const tailRotorGeometry = new THREE.BoxGeometry(0.1, 2, 0.3); | |
| const tailRotor = new THREE.Mesh(tailRotorGeometry, rotorMaterial); | |
| tailRotor.position.set(0, 0.5, 5); | |
| heliGroup.add(tailRotor); | |
| // Landing skids | |
| const skidGeometry = new THREE.CylinderGeometry(0.1, 0.1, 4, 8); | |
| const skidMaterial = new THREE.MeshStandardMaterial({ | |
| color: 0x666666, | |
| }); | |
| const skid1 = new THREE.Mesh(skidGeometry, skidMaterial); | |
| skid1.rotation.z = Math.PI / 2; | |
| skid1.position.set(-1, -1.5, 0); | |
| heliGroup.add(skid1); | |
| const skid2 = new THREE.Mesh(skidGeometry, skidMaterial); | |
| skid2.rotation.z = Math.PI / 2; | |
| skid2.position.set(1, -1.5, 0); | |
| heliGroup.add(skid2); | |
| heliGroup.position.set(50, 30, 50); | |
| scene.add(heliGroup); | |
| helicopter = { | |
| mesh: heliGroup, | |
| mainRotor: mainRotor, | |
| tailRotor: tailRotor, | |
| target: null, | |
| speed: 20, | |
| waterDropping: false, | |
| }; | |
| } | |
| function createTraffic() { | |
| const carColors = [0x0000ff, 0x00ff00, 0xffff00, 0xff00ff, 0x00ffff]; | |
| for (let i = 0; i < 10; i++) { | |
| const car = createTrafficCar(carColors[i % carColors.length]); | |
| // Random position on roads | |
| if (Math.random() > 0.5) { | |
| // On vertical road | |
| car.position.set( | |
| Math.random() > 0.5 ? 3 : -3, | |
| 0, | |
| (Math.random() - 0.5) * 180, | |
| ); | |
| car.userData.direction = Math.random() > 0.5 ? "north" : "south"; | |
| car.rotation.y = car.userData.direction === "north" ? 0 : Math.PI; | |
| } else { | |
| // On horizontal road | |
| car.position.set( | |
| (Math.random() - 0.5) * 180, | |
| 0, | |
| Math.random() > 0.5 ? 3 : -3, | |
| ); | |
| car.userData.direction = Math.random() > 0.5 ? "east" : "west"; | |
| car.rotation.y = | |
| car.userData.direction === "east" ? -Math.PI / 2 : Math.PI / 2; | |
| } | |
| car.userData.speed = 5 + Math.random() * 10; | |
| trafficCars.push(car); | |
| scene.add(car); | |
| } | |
| } | |
| function createTrafficCar(color) { | |
| const carGroup = new THREE.Group(); | |
| // Car body | |
| const bodyGeometry = new THREE.BoxGeometry(1.8, 0.9, 3.6); | |
| const bodyMaterial = new THREE.MeshStandardMaterial({ color: color }); | |
| const carBody = new THREE.Mesh(bodyGeometry, bodyMaterial); | |
| carBody.position.y = 0.45; | |
| carBody.castShadow = true; | |
| carGroup.add(carBody); | |
| // Car roof | |
| const roofGeometry = new THREE.BoxGeometry(1.6, 0.7, 1.8); | |
| const roofMaterial = new THREE.MeshStandardMaterial({ color: color }); | |
| const carRoof = new THREE.Mesh(roofGeometry, roofMaterial); | |
| carRoof.position.y = 1.1; | |
| carRoof.castShadow = true; | |
| carGroup.add(carRoof); | |
| return carGroup; | |
| } | |
| function createPreviewBuilding() { | |
| const config = buildingConfigs[selectedBuildingType]; | |
| const geometry = new THREE.BoxGeometry( | |
| config.width, | |
| config.height, | |
| config.depth, | |
| ); | |
| const material = new THREE.MeshStandardMaterial({ | |
| color: config.color, | |
| transparent: true, | |
| opacity: 0.5, | |
| }); | |
| previewBuilding = new THREE.Mesh(geometry, material); | |
| previewBuilding.position.y = config.height / 2; | |
| previewBuilding.visible = false; | |
| scene.add(previewBuilding); | |
| } | |
| function updatePreviewBuilding() { | |
| if (!previewBuilding || mode !== "BUILD") { | |
| if (previewBuilding) previewBuilding.visible = false; | |
| return; | |
| } | |
| const config = buildingConfigs[selectedBuildingType]; | |
| // Update geometry if building type changed | |
| previewBuilding.geometry.dispose(); | |
| previewBuilding.geometry = new THREE.BoxGeometry( | |
| config.width, | |
| config.height, | |
| config.depth, | |
| ); | |
| previewBuilding.position.y = config.height / 2; | |
| // Update material color | |
| const isValid = validPlacement ? 0x00ff00 : 0xff0000; | |
| previewBuilding.material.color.setHex(isValid); | |
| previewBuilding.material.opacity = 0.5; | |
| } | |
| function createBuilding(type, position) { | |
| const config = buildingConfigs[type]; | |
| const buildingGroup = new THREE.Group(); | |
| // Main building structure | |
| const geometry = new THREE.BoxGeometry( | |
| config.width, | |
| config.height, | |
| config.depth, | |
| ); | |
| const material = new THREE.MeshStandardMaterial({ | |
| color: config.color, | |
| roughness: 0.7, | |
| metalness: 0.2, | |
| }); | |
| const building = new THREE.Mesh(geometry, material); | |
| building.position.y = config.height / 2; | |
| building.castShadow = true; | |
| building.receiveShadow = true; | |
| buildingGroup.add(building); | |
| // Add architectural details | |
| addBuildingDetails(buildingGroup, config, type); | |
| buildingGroup.position.copy(position); | |
| buildingGroup.userData = { | |
| type: type, | |
| onFire: false, | |
| fireStrength: 0, | |
| originalY: position.y, | |
| }; | |
| buildings.push(buildingGroup); | |
| scene.add(buildingGroup); | |
| } | |
| function addBuildingDetails(buildingGroup, config, type) { | |
| // Add windows | |
| if (type !== "factory") { | |
| addWindows(buildingGroup, config); | |
| } | |
| // Add roof details | |
| const roofGeometry = new THREE.BoxGeometry( | |
| config.width * 0.9, | |
| 0.5, | |
| config.depth * 0.9, | |
| ); | |
| const roofMaterial = new THREE.MeshStandardMaterial({ | |
| color: 0x444444, | |
| roughness: 0.8, | |
| }); | |
| const roof = new THREE.Mesh(roofGeometry, roofMaterial); | |
| roof.position.y = config.height + 0.25; | |
| roof.castShadow = true; | |
| buildingGroup.add(roof); | |
| // Add entrance | |
| const entranceGeometry = new THREE.BoxGeometry( | |
| config.width * 0.3, | |
| 2, | |
| 0.1, | |
| ); | |
| const entranceMaterial = new THREE.MeshStandardMaterial({ | |
| color: 0x654321, | |
| roughness: 0.6, | |
| }); | |
| const entrance = new THREE.Mesh(entranceGeometry, entranceMaterial); | |
| entrance.position.set(0, 1, config.depth / 2 + 0.05); | |
| buildingGroup.add(entrance); | |
| } | |
| function addWindows(buildingGroup, config) { | |
| const windowGeometry = new THREE.PlaneGeometry(0.8, 1.2); | |
| const windowMaterial = new THREE.MeshStandardMaterial({ | |
| color: 0x87ceeb, | |
| metalness: 0.6, | |
| roughness: 0.1, | |
| emissive: 0x111111, | |
| emissiveIntensity: 0.2, | |
| }); | |
| const windowSpacing = 2; | |
| const windowsPerFloor = Math.floor(config.width / windowSpacing); | |
| const floors = config.floors; | |
| for (let floor = 0; floor < floors; floor++) { | |
| for (let i = 0; i < windowsPerFloor; i++) { | |
| const xPos = (i - windowsPerFloor / 2 + 0.5) * windowSpacing; | |
| const yPos = floor * 2 + 1.5; | |
| // Front windows | |
| const windowFront = new THREE.Mesh(windowGeometry, windowMaterial); | |
| windowFront.position.set(xPos, yPos, config.depth / 2 + 0.01); | |
| buildingGroup.add(windowFront); | |
| // Back windows | |
| const windowBack = new THREE.Mesh(windowGeometry, windowMaterial); | |
| windowBack.position.set(xPos, yPos, -config.depth / 2 - 0.01); | |
| windowBack.rotation.y = Math.PI; | |
| buildingGroup.add(windowBack); | |
| // Side windows | |
| if (i < Math.floor(config.depth / windowSpacing)) { | |
| const windowLeft = new THREE.Mesh(windowGeometry, windowMaterial); | |
| windowLeft.position.set(-config.width / 2 - 0.01, yPos, xPos); | |
| windowLeft.rotation.y = Math.PI / 2; | |
| buildingGroup.add(windowLeft); | |
| const windowRight = new THREE.Mesh( | |
| windowGeometry, | |
| windowMaterial, | |
| ); | |
| windowRight.position.set(config.width / 2 + 0.01, yPos, xPos); | |
| windowRight.rotation.y = -Math.PI / 2; | |
| buildingGroup.add(windowRight); | |
| } | |
| } | |
| } | |
| } | |
| function createInitialBuildings() { | |
| // Create some initial buildings to make the city more interesting | |
| const positions = [ | |
| { x: 20, z: 20, type: "small" }, | |
| { x: -20, z: 20, type: "medium" }, | |
| { x: 20, z: -20, type: "tall" }, | |
| { x: -20, z: -20, type: "wide" }, | |
| { x: 40, z: 0, type: "factory" }, | |
| { x: -40, z: 0, type: "tall" }, | |
| { x: 0, z: 40, type: "medium" }, | |
| { x: 0, z: -40, type: "small" }, | |
| ]; | |
| positions.forEach((pos) => { | |
| createBuilding(pos.type, new THREE.Vector3(pos.x, 0, pos.z)); | |
| }); | |
| } | |
| function createFire(building) { | |
| if (building.userData.onFire) return; | |
| building.userData.onFire = true; | |
| building.userData.fireStrength = 100; | |
| const fire = { | |
| building: building, | |
| particles: [], | |
| }; | |
| // Create fire particles | |
| const particleCount = 50; | |
| const particleGeometry = new THREE.SphereGeometry(0.3, 4, 4); | |
| for (let i = 0; i < particleCount; i++) { | |
| const particleMaterial = new THREE.MeshBasicMaterial({ | |
| color: new THREE.Color(1, Math.random() * 0.5, 0), | |
| transparent: true, | |
| opacity: 0.8, | |
| }); | |
| const particle = new THREE.Mesh(particleGeometry, particleMaterial); | |
| particle.position.copy(building.position); | |
| particle.position.y += | |
| building.children[0].geometry.parameters.height / 2; | |
| particle.userData = { | |
| velocity: new THREE.Vector3( | |
| (Math.random() - 0.5) * 0.5, | |
| Math.random() * 2 + 1, | |
| (Math.random() - 0.5) * 0.5, | |
| ), | |
| life: Math.random() * 2 + 1, | |
| }; | |
| scene.add(particle); | |
| fire.particles.push(particle); | |
| } | |
| fireSystem.fires.push(fire); | |
| updateFireAlert(); | |
| // Add point light for fire glow | |
| const fireLight = new THREE.PointLight(0xff4500, 2, 20); | |
| fireLight.position.copy(building.position); | |
| fireLight.position.y += | |
| building.children[0].geometry.parameters.height / 2; | |
| building.userData.fireLight = fireLight; | |
| scene.add(fireLight); | |
| } | |
| function updateFires(deltaTime) { | |
| fireSystem.fires.forEach((fire, index) => { | |
| if (!fire.building.userData.onFire) { | |
| // Remove fire | |
| fire.particles.forEach((particle) => { | |
| scene.remove(particle); | |
| }); | |
| if (fire.building.userData.fireLight) { | |
| scene.remove(fire.building.userData.fireLight); | |
| fire.building.userData.fireLight = null; | |
| } | |
| fireSystem.fires.splice(index, 1); | |
| return; | |
| } | |
| // Update particles | |
| fire.particles.forEach((particle, pIndex) => { | |
| particle.userData.life -= deltaTime; | |
| if (particle.userData.life <= 0) { | |
| // Reset particle | |
| particle.position.copy(fire.building.position); | |
| particle.position.y += | |
| fire.building.children[0].geometry.parameters.height / 2; | |
| particle.userData.life = Math.random() * 2 + 1; | |
| particle.userData.velocity = new THREE.Vector3( | |
| (Math.random() - 0.5) * 0.5, | |
| Math.random() * 2 + 1, | |
| (Math.random() - 0.5) * 0.5, | |
| ); | |
| } else { | |
| // Update particle position | |
| particle.position.add( | |
| particle.userData.velocity.clone().multiplyScalar(deltaTime), | |
| ); | |
| particle.material.opacity = particle.userData.life / 3; | |
| } | |
| }); | |
| // Flicker fire light | |
| if (fire.building.userData.fireLight) { | |
| fire.building.userData.fireLight.intensity = | |
| 1.5 + Math.random() * 0.5; | |
| } | |
| }); | |
| } | |
| function startRandomFire() { | |
| if (buildings.length === 0) return; | |
| const availableBuildings = buildings.filter((b) => !b.userData.onFire); | |
| if (availableBuildings.length === 0) return; | |
| const randomBuilding = | |
| availableBuildings[ | |
| Math.floor(Math.random() * availableBuildings.length) | |
| ]; | |
| createFire(randomBuilding); | |
| } | |
| function extinguishFire(building, extinguisher) { | |
| if (!building.userData.onFire) return; | |
| building.userData.fireStrength -= 30; | |
| if (building.userData.fireStrength <= 0) { | |
| building.userData.onFire = false; | |
| building.userData.fireStrength = 0; | |
| if (extinguisher === "player") { | |
| fireSystem.playerScore++; | |
| } else { | |
| fireSystem.helicopterScore++; | |
| } | |
| updateFireAlert(); | |
| updateStats(); | |
| } | |
| } | |
| // --- CHANGE START --- | |
| // Modified to accept who is extinguishing the fire (player or helicopter) | |
| function createWaterSpray( | |
| position, | |
| direction, | |
| extinguisherType = "player", | |
| ) { | |
| const sprayGeometry = new THREE.ConeGeometry(2, 10, 8); | |
| const sprayMaterial = new THREE.MeshBasicMaterial({ | |
| color: 0x4444ff, | |
| transparent: true, | |
| opacity: 0.3, | |
| }); | |
| const spray = new THREE.Mesh(sprayGeometry, sprayMaterial); | |
| spray.position.copy(position); | |
| spray.lookAt(position.clone().add(direction)); | |
| spray.rotateX(Math.PI / 2); // Corrected rotation for lookAt | |
| scene.add(spray); | |
| // Check for fire collision | |
| buildings.forEach((building) => { | |
| if (building.userData.onFire) { | |
| const distance = position.distanceTo(building.position); | |
| // Increased range for helicopter spray to be effective from above | |
| const effectiveRange = extinguisherType === "helicopter" ? 25 : 15; | |
| if (distance < effectiveRange) { | |
| extinguishFire(building, extinguisherType); | |
| } | |
| } | |
| }); | |
| // Remove spray after a short time | |
| setTimeout(() => { | |
| scene.remove(spray); | |
| }, 200); | |
| } | |
| // --- CHANGE END --- | |
| // --- CHANGE START --- | |
| // Update helicopter behavior and movement to spray water | |
| function updateHelicopter(deltaTime) { | |
| // Rotate rotors for visual effect | |
| helicopter.mainRotor.rotation.y += deltaTime * 10; | |
| helicopter.tailRotor.rotation.z += deltaTime * 15; | |
| // Find nearest fire to target | |
| let nearestFire = null; | |
| let minDistance = Infinity; | |
| fireSystem.fires.forEach((fire) => { | |
| const distance = helicopter.mesh.position.distanceTo( | |
| fire.building.position, | |
| ); | |
| if (distance < minDistance) { | |
| minDistance = distance; | |
| nearestFire = fire; | |
| } | |
| }); | |
| if (nearestFire) { | |
| // Move towards fire | |
| const targetPos = nearestFire.building.position.clone(); | |
| targetPos.y = 30; // Maintain flying height | |
| const direction = targetPos | |
| .clone() | |
| .sub(helicopter.mesh.position) | |
| .normalize(); | |
| helicopter.mesh.position.add( | |
| direction.multiplyScalar(helicopter.speed * deltaTime), | |
| ); | |
| // Look at target | |
| helicopter.mesh.lookAt(targetPos); | |
| // Spray water if close enough | |
| if (minDistance < 15 && !helicopter.waterDropping) { | |
| helicopter.waterDropping = true; | |
| const sprayPosition = helicopter.mesh.position.clone(); | |
| const sprayDirection = nearestFire.building.position | |
| .clone() | |
| .sub(sprayPosition) | |
| .normalize(); | |
| createWaterSpray(sprayPosition, sprayDirection, "helicopter"); | |
| // Cooldown to prevent constant spraying | |
| setTimeout(() => { | |
| helicopter.waterDropping = false; | |
| }, 1000); // Wait 1 second before spraying again | |
| } | |
| } else { | |
| // Patrol mode when no fires | |
| const time = Date.now() * 0.0005; | |
| helicopter.mesh.position.x = Math.sin(time) * 50; | |
| helicopter.mesh.position.z = Math.cos(time) * 50; | |
| helicopter.mesh.position.y = 25; | |
| } | |
| } | |
| // --- CHANGE END --- | |
| // Update fire alert UI display | |
| function updateFireAlert() { | |
| const fireAlert = document.getElementById("fire-alert"); | |
| const activeFiresCount = fireSystem.fires.length; | |
| document.getElementById("fires-active").textContent = activeFiresCount; | |
| if ( | |
| activeFiresCount > 0 && | |
| (!fireAlert.style.display || fireAlert.style.display === "none") | |
| ) { | |
| fireAlert.style.display = "block"; | |
| const fire = fireSystem.fires[0]; | |
| document.getElementById("fire-location").textContent = | |
| `Location: ${Math.round(fire.building.position.x)}, ${Math.round(fire.building.position.z)}`; | |
| // Hide alert after 3 seconds | |
| setTimeout(() => { | |
| fireAlert.style.display = "none"; | |
| }, 3000); | |
| } | |
| } | |
| // Update statistics display | |
| function updateStats() { | |
| document.getElementById("fires-extinguished").textContent = | |
| fireSystem.playerScore; | |
| document.getElementById("helicopter-score").textContent = | |
| fireSystem.helicopterScore; | |
| } | |
| // Update water level meter for firetruck | |
| function updateWaterLevel(deltaTime) { | |
| if (selectedVehicle === "firetruck") { | |
| const waterMeter = document.getElementById("water-meter"); | |
| waterMeter.style.display = "block"; | |
| if (keys[" "] && fireSystem.waterLevel > 0) { | |
| // Decrease water when spraying | |
| fireSystem.waterLevel -= 10 * deltaTime; | |
| fireSystem.waterLevel = Math.max(0, fireSystem.waterLevel); | |
| } else if (!keys[" "] && fireSystem.waterLevel < 100) { | |
| // Refill water when not spraying | |
| fireSystem.waterLevel += 5 * deltaTime; | |
| fireSystem.waterLevel = Math.min(100, fireSystem.waterLevel); | |
| } | |
| // Update water meter visual | |
| document.getElementById("water-fill").style.width = | |
| fireSystem.waterLevel + "%"; | |
| } else { | |
| // Hide water meter for regular car | |
| document.getElementById("water-meter").style.display = "none"; | |
| } | |
| } | |
| // Update camera position for build mode | |
| function updateBuildCamera() { | |
| camera.position.x = Math.sin(cameraAngle) * cameraDistance; | |
| camera.position.y = cameraHeight; | |
| camera.position.z = Math.cos(cameraAngle) * cameraDistance; | |
| camera.lookAt(0, 0, 0); | |
| } | |
| // Update camera position for drive mode | |
| function updateDriveCamera() { | |
| if (currentVehicle && currentVehicle.mesh) { | |
| // Third person camera following the vehicle | |
| const vehiclePos = currentVehicle.mesh.position; | |
| const vehicleRotation = currentVehicle.mesh.rotation.y; | |
| const cameraOffset = new THREE.Vector3( | |
| Math.sin(vehicleRotation) * 10, | |
| 5, | |
| Math.cos(vehicleRotation) * 10, | |
| ); | |
| camera.position.x = vehiclePos.x + cameraOffset.x; | |
| camera.position.y = vehiclePos.y + cameraOffset.y; | |
| camera.position.z = vehiclePos.z + cameraOffset.z; | |
| camera.lookAt(vehiclePos); | |
| } | |
| } | |
| // Handle mouse movement events | |
| function handleMouseMove(event) { | |
| // Update mouse coordinates for raycasting | |
| mouse.x = (event.clientX / window.innerWidth) * 2 - 1; | |
| mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; | |
| if (mode === "BUILD") { | |
| // Update raycaster | |
| raycaster.setFromCamera(mouse, camera); | |
| // Find ground intersection | |
| const ground = scene.getObjectByName("ground"); | |
| const intersects = raycaster.intersectObject(ground); | |
| if (intersects.length > 0) { | |
| const point = intersects[0].point; | |
| // Check if position is valid (not on road) | |
| validPlacement = Math.abs(point.x) > 8 || Math.abs(point.z) > 8; | |
| // Update preview building position | |
| if (previewBuilding) { | |
| previewBuilding.position.x = point.x; | |
| previewBuilding.position.z = point.z; | |
| previewBuilding.visible = true; | |
| updatePreviewBuilding(); | |
| // Update preview indicator | |
| const indicator = document.getElementById("preview-indicator"); | |
| indicator.style.display = "block"; | |
| indicator.style.backgroundColor = validPlacement | |
| ? "rgba(0, 255, 0, 0.8)" | |
| : "rgba(255, 0, 0, 0.8)"; | |
| indicator.textContent = validPlacement | |
| ? "Click to place building" | |
| : "Cannot place on road"; | |
| } | |
| } | |
| } | |
| } | |
| // Handle mouse click events for building placement | |
| function handleClick(event) { | |
| if (mode !== "BUILD") return; | |
| // Update mouse coordinates | |
| mouse.x = (event.clientX / window.innerWidth) * 2 - 1; | |
| mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; | |
| // Update raycaster | |
| raycaster.setFromCamera(mouse, camera); | |
| // Find ground intersection | |
| const ground = scene.getObjectByName("ground"); | |
| const intersects = raycaster.intersectObject(ground); | |
| if (intersects.length > 0) { | |
| const position = intersects[0].point; | |
| // Check if position is not on road | |
| if (Math.abs(position.x) > 8 || Math.abs(position.z) > 8) { | |
| createBuilding(selectedBuildingType, position); | |
| } | |
| } | |
| } | |
| // --- CHANGE START --- | |
| // Handle key press events | |
| function handleKeyDown(event) { | |
| // This is the crucial fix. It prevents the spacebar from "clicking" a focused button, | |
| // which was causing the mode to toggle instead of spraying water. | |
| if (event.key === " ") { | |
| event.preventDefault(); | |
| } | |
| keys[event.key.toLowerCase()] = true; | |
| // Camera controls in build mode | |
| if (mode === "BUILD") { | |
| switch (event.key.toLowerCase()) { | |
| case "q": | |
| cameraAngle -= 0.1; | |
| updateBuildCamera(); | |
| break; | |
| case "e": | |
| cameraAngle += 0.1; | |
| updateBuildCamera(); | |
| break; | |
| case "+": | |
| case "=": | |
| cameraDistance = Math.max(20, cameraDistance - 5); | |
| updateBuildCamera(); | |
| break; | |
| case "-": | |
| case "_": | |
| cameraDistance = Math.min(100, cameraDistance + 5); | |
| updateBuildCamera(); | |
| break; | |
| } | |
| } | |
| } | |
| // --- CHANGE END --- | |
| // Handle key release events | |
| function handleKeyUp(event) { | |
| keys[event.key.toLowerCase()] = false; | |
| } | |
| // Update player vehicle movement and controls | |
| function updatePlayerVehicle(deltaTime) { | |
| if (mode !== "DRIVE" || !currentVehicle.mesh.visible) return; | |
| const vehicle = currentVehicle; | |
| // Acceleration/Deceleration | |
| if (keys["w"] || keys["arrowup"]) { | |
| vehicle.speed = Math.min( | |
| vehicle.speed + vehicle.acceleration * deltaTime, | |
| vehicle.maxSpeed, | |
| ); | |
| } else if (keys["s"] || keys["arrowdown"]) { | |
| vehicle.speed = Math.max( | |
| vehicle.speed - vehicle.acceleration * deltaTime, | |
| -vehicle.maxSpeed / 2, | |
| ); | |
| } else { | |
| // Apply friction when no input | |
| if (vehicle.speed > 0) { | |
| vehicle.speed = Math.max( | |
| vehicle.speed - vehicle.deceleration * deltaTime, | |
| 0, | |
| ); | |
| } else { | |
| vehicle.speed = Math.min( | |
| vehicle.speed + vehicle.deceleration * deltaTime, | |
| 0, | |
| ); | |
| } | |
| } | |
| // Steering (only when moving) | |
| if (vehicle.speed !== 0) { | |
| if (keys["a"] || keys["arrowleft"]) { | |
| vehicle.mesh.rotation.y += | |
| vehicle.turnSpeed * deltaTime * (vehicle.speed > 0 ? 1 : -1); | |
| } | |
| if (keys["d"] || keys["arrowright"]) { | |
| vehicle.mesh.rotation.y -= | |
| vehicle.turnSpeed * deltaTime * (vehicle.speed > 0 ? 1 : -1); | |
| } | |
| } | |
| // Update position based on rotation and speed | |
| const direction = new THREE.Vector3( | |
| -Math.sin(vehicle.mesh.rotation.y), | |
| 0, | |
| -Math.cos(vehicle.mesh.rotation.y), | |
| ); | |
| vehicle.mesh.position.add( | |
| direction.multiplyScalar(vehicle.speed * deltaTime), | |
| ); | |
| // Keep vehicle on ground | |
| vehicle.mesh.position.y = 0; | |
| // Boundary checking - keep vehicle within world bounds | |
| const boundary = 95; | |
| vehicle.mesh.position.x = Math.max( | |
| -boundary, | |
| Math.min(boundary, vehicle.mesh.position.x), | |
| ); | |
| vehicle.mesh.position.z = Math.max( | |
| -boundary, | |
| Math.min(boundary, vehicle.mesh.position.z), | |
| ); | |
| // --- CHANGE START --- | |
| // Water spray for firetruck | |
| if ( | |
| selectedVehicle === "firetruck" && | |
| keys[" "] && | |
| fireSystem.waterLevel > 0 | |
| ) { | |
| const sprayPosition = vehicle.mesh.position.clone(); | |
| sprayPosition.y += 1.5; // Spray from above vehicle | |
| const sprayDirection = direction.clone(); | |
| sprayDirection.y = 0.2; // Slight upward angle | |
| // The updated createWaterSpray function will handle this correctly as the "player" | |
| createWaterSpray(sprayPosition, sprayDirection); | |
| } | |
| // --- CHANGE END --- | |
| } | |
| // Update traffic car movement | |
| function updateTraffic(deltaTime) { | |
| trafficCars.forEach((car) => { | |
| const direction = car.userData.direction; | |
| const speed = car.userData.speed * deltaTime; | |
| switch (direction) { | |
| case "north": | |
| car.position.z -= speed; | |
| if (car.position.z < -95) car.position.z = 95; | |
| break; | |
| case "south": | |
| car.position.z += speed; | |
| if (car.position.z > 95) car.position.z = -95; | |
| break; | |
| case "east": | |
| car.position.x += speed; | |
| if (car.position.x > 95) car.position.x = -95; | |
| break; | |
| case "west": | |
| car.position.x -= speed; | |
| if (car.position.x < -95) car.position.x = 95; | |
| break; | |
| } | |
| }); | |
| } | |
| // Toggle between BUILD and DRIVE modes | |
| function toggleMode() { | |
| mode = mode === "BUILD" ? "DRIVE" : "BUILD"; | |
| // Update UI elements | |
| document.getElementById("mode-toggle").textContent = | |
| `Toggle Mode: ${mode}`; | |
| document.getElementById("mode-indicator").textContent = `MODE: ${mode}`; | |
| if (mode === "DRIVE") { | |
| // Switch to drive mode | |
| currentVehicle.mesh.visible = true; | |
| updateDriveCamera(); | |
| document.getElementById("build-controls").style.display = "none"; | |
| document.getElementById("preview-indicator").style.display = "none"; | |
| if (previewBuilding) previewBuilding.visible = false; | |
| } else { | |
| // Switch to build mode | |
| currentVehicle.mesh.visible = false; | |
| updateBuildCamera(); | |
| document.getElementById("build-controls").style.display = "block"; | |
| } | |
| } | |
| // Toggle between car and firetruck | |
| function toggleVehicle() { | |
| selectedVehicle = selectedVehicle === "car" ? "firetruck" : "car"; | |
| if (mode === "DRIVE") { | |
| // Hide all vehicles first | |
| playerCar.mesh.visible = false; | |
| firetruck.mesh.visible = false; | |
| // Set current vehicle and show it | |
| if (selectedVehicle === "car") { | |
| currentVehicle = playerCar; | |
| } else { | |
| currentVehicle = firetruck; | |
| } | |
| currentVehicle.mesh.visible = true; | |
| } | |
| // Update UI button text | |
| document.getElementById("vehicle-select").textContent = | |
| `Vehicle: ${selectedVehicle === "car" ? "Regular Car" : "Firetruck"}`; | |
| } | |
| // Clear all buildings from the scene | |
| function clearAllBuildings() { | |
| // Remove all buildings from scene | |
| buildings.forEach((building) => { | |
| scene.remove(building); | |
| }); | |
| buildings = []; | |
| // Clear all fires and related objects | |
| fireSystem.fires.forEach((fire) => { | |
| fire.particles.forEach((particle) => { | |
| scene.remove(particle); | |
| }); | |
| if (fire.building.userData.fireLight) { | |
| scene.remove(fire.building.userData.fireLight); | |
| } | |
| }); | |
| fireSystem.fires = []; | |
| } | |
| // Set up all event listeners | |
| function setupEventListeners() { | |
| // Mode toggle button | |
| document | |
| .getElementById("mode-toggle") | |
| .addEventListener("click", toggleMode); | |
| // Vehicle toggle button | |
| document | |
| .getElementById("vehicle-select") | |
| .addEventListener("click", toggleVehicle); | |
| // Building selection buttons | |
| document.querySelectorAll(".building-btn").forEach((btn) => { | |
| btn.addEventListener("click", (e) => { | |
| selectedBuildingType = e.target.dataset.type; | |
| // Update active button styling | |
| document | |
| .querySelectorAll(".building-btn") | |
| .forEach((b) => b.classList.remove("active")); | |
| e.target.classList.add("active"); | |
| updatePreviewBuilding(); | |
| }); | |
| }); | |
| // Camera control buttons | |
| document.getElementById("rotate-left").addEventListener("click", () => { | |
| cameraAngle -= 0.2; | |
| updateBuildCamera(); | |
| }); | |
| document | |
| .getElementById("rotate-right") | |
| .addEventListener("click", () => { | |
| cameraAngle += 0.2; | |
| updateBuildCamera(); | |
| }); | |
| document.getElementById("zoom-in").addEventListener("click", () => { | |
| cameraDistance = Math.max(20, cameraDistance - 5); | |
| updateBuildCamera(); | |
| }); | |
| document.getElementById("zoom-out").addEventListener("click", () => { | |
| cameraDistance = Math.min(100, cameraDistance + 5); | |
| updateBuildCamera(); | |
| }); | |
| // Clear all buildings button | |
| document | |
| .getElementById("clear-all") | |
| .addEventListener("click", clearAllBuildings); | |
| // Mouse and keyboard controls | |
| renderer.domElement.addEventListener("mousemove", handleMouseMove); | |
| renderer.domElement.addEventListener("click", handleClick); | |
| document.addEventListener("keydown", handleKeyDown); | |
| document.addEventListener("keyup", handleKeyUp); | |
| // Window resize handler | |
| window.addEventListener("resize", () => { | |
| camera.aspect = window.innerWidth / window.innerHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| }); | |
| } | |
| // Main animation loop | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| const deltaTime = clock.getDelta(); | |
| const elapsedTime = clock.getElapsedTime(); | |
| // Fire spawning system - start random fires periodically | |
| fireSystem.nextFireTime -= deltaTime; | |
| if (fireSystem.nextFireTime <= 0 && buildings.length > 0) { | |
| startRandomFire(); | |
| fireSystem.nextFireTime = 10 + Math.random() * 20; // Next fire in 10-30 seconds | |
| } | |
| // Update based on current mode | |
| if (mode === "DRIVE") { | |
| updatePlayerVehicle(deltaTime); | |
| updateDriveCamera(); | |
| } else { | |
| updateBuildCamera(); | |
| } | |
| // Always update these systems | |
| updateTraffic(deltaTime); | |
| updateFires(deltaTime); | |
| updateHelicopter(deltaTime); | |
| updateWaterLevel(deltaTime); | |
| updateStats(); | |
| // Render the scene | |
| renderer.render(scene, camera); | |
| } | |
| // Initialize the application | |
| init(); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment