Skip to content

Instantly share code, notes, and snippets.

@shricodev
Created June 14, 2025 07:30
Show Gist options
  • Select an option

  • Save shricodev/a8644daf171409d0c09b670dd99af122 to your computer and use it in GitHub Desktop.

Select an option

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
<!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>
<!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