CitySimulator / index.html
awacke1's picture
Update index.html
2f8df45 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Interactive AI Traffic Simulator</title>
<style>
body {
margin: 0;
overflow: hidden;
font-family: Arial, sans-serif;
background: #000;
}
#ui {
position: absolute;
top: 10px;
left: 10px;
color: white;
background-color: rgba(0,0,0,0.9);
padding: 15px;
border-radius: 8px;
z-index: 100;
font-size: 14px;
min-width: 200px;
}
#controls {
position: absolute;
top: 10px;
right: 10px;
color: white;
background-color: rgba(0,0,0,0.9);
padding: 15px;
border-radius: 8px;
z-index: 100;
}
#drivingControls {
position: absolute;
bottom: 10px;
left: 50%;
transform: translateX(-50%);
color: white;
background-color: rgba(0,0,0,0.9);
padding: 15px;
border-radius: 8px;
z-index: 100;
text-align: center;
}
#cameraControls {
position: absolute;
bottom: 10px;
right: 10px;
color: white;
background-color: rgba(0,0,0,0.9);
padding: 15px;
border-radius: 8px;
z-index: 100;
}
button {
background-color: #4CAF50;
border: none;
color: white;
padding: 8px 16px;
margin: 5px;
cursor: pointer;
border-radius: 4px;
font-size: 12px;
}
button:hover {
background-color: #45a049;
}
button.active {
background-color: #ff6b6b;
}
#stats {
position: absolute;
bottom: 10px;
left: 10px;
color: white;
background-color: rgba(0,0,0,0.9);
padding: 15px;
border-radius: 8px;
z-index: 100;
font-size: 12px;
min-width: 200px;
}
#minimap {
position: absolute;
top: 10px;
right: 250px;
width: 200px;
height: 200px;
background-color: rgba(0,0,0,0.8);
border: 2px solid white;
border-radius: 8px;
z-index: 100;
}
.highlight { color: #ffcc00; font-weight: bold; }
.success { color: #00ff00; font-weight: bold; }
.player { color: #ff00ff; font-weight: bold; }
.crosshair {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 20px;
height: 20px;
border: 2px solid white;
border-radius: 50%;
z-index: 200;
opacity: 0.7;
display: none;
}
#speedometer {
position: absolute;
bottom: 100px;
right: 10px;
color: white;
background-color: rgba(0,0,0,0.9);
padding: 15px;
border-radius: 8px;
z-index: 100;
text-align: center;
display: none;
}
.speed-gauge {
width: 80px;
height: 80px;
border: 3px solid #4CAF50;
border-radius: 50%;
position: relative;
margin: 0 auto;
}
.speed-needle {
position: absolute;
top: 50%;
left: 50%;
width: 2px;
height: 35px;
background: #ff6b6b;
transform-origin: bottom center;
transform: translate(-50%, -100%) rotate(0deg);
}
</style>
</head>
<body>
<div id="ui">
<div class="highlight">Interactive AI Traffic Simulator</div>
<div>Mode: <span id="currentMode">AI Observer</span></div>
<div>Population: <span id="population">50</span></div>
<div>Speed: <span id="avgSpeed">0</span> km/h</div>
<div>Traffic Flow: <span id="trafficFlow">Normal</span></div>
<div>Active Routes: <span id="activeRoutes">0</span></div>
</div>
<div id="controls">
<button id="pauseBtn">Pause</button>
<button id="resetBtn">Reset</button>
<button id="speedBtn">Speed: 1x</button>
<button id="trafficBtn">Traffic: Normal</button>
<button id="weatherBtn">Weather: Clear</button>
</div>
<div id="drivingControls" style="display: none;">
<div class="highlight">Driving Controls</div>
<div>WASD - Drive | Mouse - Look | Space - Brake</div>
<div>Shift - Boost | Tab - Change Car | Esc - Exit</div>
</div>
<div id="cameraControls">
<div class="highlight">Camera Controls</div>
<button id="firstPersonBtn">First Person</button>
<button id="thirdPersonBtn">Third Person</button>
<button id="overviewBtn">Overview</button>
<button id="freeCamera">Free Cam</button>
<button id="nextCarBtn">Next Car</button>
</div>
<div id="stats">
<div><span class="highlight">Current Statistics:</span></div>
<div>Camera: <span id="cameraMode">Overview</span></div>
<div>Target: <span id="currentTarget">None</span></div>
<div>Distance: <span id="targetDistance">0</span>m</div>
<div>Destination: <span id="currentDestination">None</span></div>
<div>Flock Size: <span id="flockSize">0</span></div>
</div>
<div id="minimap"></div>
<div id="speedometer">
<div class="speed-gauge">
<div class="speed-needle" id="speedNeedle"></div>
</div>
<div>Speed: <span id="currentSpeed">0</span> km/h</div>
</div>
<div class="crosshair" id="crosshair"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
// Global variables
let scene, camera, renderer, clock;
let world = {
roads: [],
intersections: [],
buildings: [],
roadNetwork: new Map(),
trafficLights: []
};
// Camera and interaction
let cameraMode = 'overview'; // firstPerson, thirdPerson, overview, free
let currentTarget = null;
let isPlayerDriving = false;
let mouseControls = { x: 0, y: 0, sensitivity: 0.002 };
let keys = {};
// Traffic simulation
let population = [];
let playerCar = null;
let populationSize = 50;
let paused = false;
let speedMultiplier = 1;
let trafficDensity = 'normal';
let weatherCondition = 'clear';
// Enhanced AI parameters
const NEIGHBOR_RADIUS = 25;
const ROAD_WIDTH = 8;
const BUILDING_VISIT_DISTANCE = 15;
// Simple Neural Network for AI Cars
class SimpleNeuralNetwork {
constructor() {
this.weights = {
input: Array(16).fill().map(() => Array(8).fill().map(() => (Math.random() - 0.5) * 2)),
hidden: Array(8).fill().map(() => Array(4).fill().map(() => (Math.random() - 0.5) * 2)),
output: Array(4).fill().map(() => (Math.random() - 0.5) * 2)
};
}
activate(inputs) {
// Simple forward pass
let hidden = new Array(8).fill(0);
for (let i = 0; i < 8; i++) {
for (let j = 0; j < inputs.length && j < 16; j++) {
hidden[i] += inputs[j] * this.weights.input[j][i];
}
hidden[i] = Math.tanh(hidden[i]);
}
let outputs = new Array(4).fill(0);
for (let i = 0; i < 4; i++) {
for (let j = 0; j < hidden.length; j++) {
outputs[i] += hidden[j] * this.weights.hidden[j][i];
}
outputs[i] = this.weights.output[i];
outputs[i] = Math.tanh(outputs[i]);
}
return outputs;
}
mutate(rate = 0.1) {
// Simple mutation
Object.keys(this.weights).forEach(layer => {
if (Array.isArray(this.weights[layer][0])) {
this.weights[layer].forEach(neuron => {
neuron.forEach((weight, i) => {
if (Math.random() < rate) {
neuron[i] += (Math.random() - 0.5) * 0.5;
}
});
});
} else {
this.weights[layer].forEach((weight, i) => {
if (Math.random() < rate) {
this.weights[layer][i] += (Math.random() - 0.5) * 0.5;
}
});
}
});
}
}
// Enhanced AI Car with realistic navigation
class RealisticAICar {
constructor(x = 0, z = 0, isPlayer = false) {
this.isPlayer = isPlayer;
this.brain = isPlayer ? null : new SimpleNeuralNetwork();
this.mesh = this.createCarMesh();
this.mesh.position.set(x, 1, z);
// Movement properties
this.velocity = new THREE.Vector3(0, 0, 0);
this.acceleration = new THREE.Vector3();
this.maxSpeed = isPlayer ? 35 : 25;
this.currentSpeed = 0;
this.steering = 0;
// Navigation properties
this.destination = null;
this.currentPath = [];
this.pathIndex = 0;
this.isAtDestination = false;
this.lastDestinationTime = 0;
// Flocking properties
this.neighbors = [];
this.flockingForce = new THREE.Vector3();
// State tracking
this.sensors = Array(8).fill(0);
this.onRoad = false;
this.atIntersection = false;
this.waitingAtLight = false;
this.initializeDestination();
}
createCarMesh() {
const group = new THREE.Group();
// Car body
const bodyGeometry = new THREE.BoxGeometry(1.8, 1, 4);
const bodyMaterial = new THREE.MeshLambertMaterial({
color: this.isPlayer ? 0xff00ff : new THREE.Color().setHSL(Math.random(), 0.8, 0.6)
});
const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
body.position.y = 0.5;
body.castShadow = true;
group.add(body);
// Roof
const roofGeometry = new THREE.BoxGeometry(1.6, 0.8, 2.5);
const roof = new THREE.Mesh(roofGeometry, bodyMaterial);
roof.position.set(0, 1.4, -0.3);
group.add(roof);
// Wheels
const wheelGeometry = new THREE.CylinderGeometry(0.4, 0.4, 0.3, 8);
const wheelMaterial = new THREE.MeshLambertMaterial({ color: 0x333333 });
this.wheels = [];
const wheelPositions = [
[-1, 0, 1.5], [1, 0, 1.5],
[-1, 0, -1.5], [1, 0, -1.5]
];
wheelPositions.forEach((pos, i) => {
const wheel = new THREE.Mesh(wheelGeometry, wheelMaterial);
wheel.position.set(...pos);
wheel.rotation.z = Math.PI / 2;
this.wheels.push(wheel);
group.add(wheel);
});
// Player indicator
if (this.isPlayer) {
const indicatorGeometry = new THREE.ConeGeometry(0.5, 2, 4);
const indicator = new THREE.Mesh(indicatorGeometry,
new THREE.MeshLambertMaterial({ color: 0xffff00 }));
indicator.position.set(0, 3, 0);
group.add(indicator);
}
return group;
}
initializeDestination() {
if (world.buildings.length > 0) {
this.setRandomDestination();
}
}
setRandomDestination() {
const availableBuildings = world.buildings.filter(b =>
b.mesh.position.distanceTo(this.mesh.position) > 50
);
if (availableBuildings.length > 0) {
this.destination = availableBuildings[Math.floor(Math.random() * availableBuildings.length)];
this.calculatePath();
this.isAtDestination = false;
}
}
calculatePath() {
if (!this.destination) return;
// Simple pathfinding - find nearest road to destination
const start = this.mesh.position.clone();
const end = this.destination.mesh.position.clone();
// For now, create a simple path with waypoints
this.currentPath = this.generateWaypoints(start, end);
this.pathIndex = 0;
}
generateWaypoints(start, end) {
const waypoints = [];
const roadSpacing = 80;
// Navigate to road grid
const startRoadX = Math.round(start.x / roadSpacing) * roadSpacing;
const startRoadZ = Math.round(start.z / roadSpacing) * roadSpacing;
const endRoadX = Math.round(end.x / roadSpacing) * roadSpacing;
const endRoadZ = Math.round(end.z / roadSpacing) * roadSpacing;
waypoints.push(new THREE.Vector3(startRoadX, 1, start.z));
waypoints.push(new THREE.Vector3(startRoadX, 1, startRoadZ));
// Navigate along roads
if (startRoadX !== endRoadX) {
waypoints.push(new THREE.Vector3(endRoadX, 1, startRoadZ));
}
waypoints.push(new THREE.Vector3(endRoadX, 1, endRoadZ));
waypoints.push(end.clone().add(new THREE.Vector3(0, 1, 0)));
return waypoints;
}
updateSensors() {
const maxDistance = 15;
const raycaster = new THREE.Raycaster();
// 8-direction sensor array
for (let i = 0; i < 8; i++) {
const angle = (i * Math.PI * 2) / 8;
const direction = new THREE.Vector3(
Math.sin(angle), 0, Math.cos(angle)
);
direction.applyQuaternion(this.mesh.quaternion);
raycaster.set(this.mesh.position, direction);
const intersects = raycaster.intersectObjects(this.getObstacles(), true);
if (intersects.length > 0 && intersects[0].distance <= maxDistance) {
this.sensors[i] = 1 - (intersects[0].distance / maxDistance);
} else {
this.sensors[i] = 0;
}
}
this.updateRoadStatus();
this.updateFlocking();
}
updateRoadStatus() {
const pos = this.mesh.position;
const roadSpacing = 80;
const roadHalfWidth = ROAD_WIDTH / 2;
const nearestHorizontalRoad = Math.round(pos.z / roadSpacing) * roadSpacing;
const nearestVerticalRoad = Math.round(pos.x / roadSpacing) * roadSpacing;
const distToHorizontalRoad = Math.abs(pos.z - nearestHorizontalRoad);
const distToVerticalRoad = Math.abs(pos.x - nearestVerticalRoad);
this.onRoad = distToHorizontalRoad <= roadHalfWidth || distToVerticalRoad <= roadHalfWidth;
// Check if at intersection
this.atIntersection = distToHorizontalRoad <= roadHalfWidth && distToVerticalRoad <= roadHalfWidth;
}
updateFlocking() {
this.neighbors = [];
let separation = new THREE.Vector3();
let alignment = new THREE.Vector3();
let cohesion = new THREE.Vector3();
let neighborCount = 0;
population.forEach(other => {
if (other !== this) {
const distance = this.mesh.position.distanceTo(other.mesh.position);
if (distance < NEIGHBOR_RADIUS && distance > 0) {
this.neighbors.push(other);
cohesion.add(other.mesh.position);
alignment.add(other.velocity);
neighborCount++;
if (distance < 8) {
const diff = this.mesh.position.clone().sub(other.mesh.position);
diff.normalize().divideScalar(distance);
separation.add(diff);
}
}
}
});
if (neighborCount > 0) {
cohesion.divideScalar(neighborCount).sub(this.mesh.position).normalize();
alignment.divideScalar(neighborCount).normalize();
}
this.flockingForce = separation.multiplyScalar(1.5)
.add(alignment.multiplyScalar(1.0))
.add(cohesion.multiplyScalar(0.5));
}
update(deltaTime) {
this.updateSensors();
if (this.isPlayer) {
this.updatePlayerControls(deltaTime);
} else {
this.updateAI(deltaTime);
}
this.updateMovement(deltaTime);
this.updateDestination();
this.keepInBounds();
}
updatePlayerControls(deltaTime) {
const forwardForce = keys['KeyW'] ? 1 : 0;
const backwardForce = keys['KeyS'] ? -0.5 : 0;
const leftTurn = keys['KeyA'] ? -1 : 0;
const rightTurn = keys['KeyD'] ? 1 : 0;
const brake = keys['Space'] ? 1 : 0;
const boost = keys['ShiftLeft'] ? 1.5 : 1;
this.applyMovement(forwardForce + backwardForce, leftTurn + rightTurn, brake, boost, deltaTime);
}
updateAI(deltaTime) {
if (!this.brain) return;
const inputs = [
...this.sensors,
this.onRoad ? 1 : 0,
this.atIntersection ? 1 : 0,
this.currentSpeed / this.maxSpeed,
this.getPathDirection(),
this.flockingForce.x,
this.flockingForce.z,
this.neighbors.length / 10,
this.getDestinationDirection()
];
const outputs = this.brain.activate(inputs);
const [forward, turn, brake, formation] = outputs;
this.applyMovement(forward, turn, brake, 1, deltaTime);
}
getPathDirection() {
if (this.currentPath.length === 0 || this.pathIndex >= this.currentPath.length) {
return 0;
}
const target = this.currentPath[this.pathIndex];
const direction = target.clone().sub(this.mesh.position).normalize();
const forward = new THREE.Vector3(0, 0, 1).applyQuaternion(this.mesh.quaternion);
return direction.dot(forward);
}
getDestinationDirection() {
if (!this.destination) return 0;
const direction = this.destination.mesh.position.clone().sub(this.mesh.position).normalize();
const forward = new THREE.Vector3(0, 0, 1).applyQuaternion(this.mesh.quaternion);
return direction.dot(forward);
}
applyMovement(forwardForce, turnForce, brakeForce, boostMultiplier, deltaTime) {
// Steering
this.steering = turnForce * 0.03;
this.mesh.rotation.y += this.steering * this.currentSpeed * deltaTime;
// Acceleration
const maxAcceleration = 15 * boostMultiplier;
if (forwardForce > 0) {
this.currentSpeed += maxAcceleration * forwardForce * deltaTime;
} else if (forwardForce < 0) {
this.currentSpeed += maxAcceleration * forwardForce * deltaTime;
}
// Braking
if (brakeForce > 0) {
this.currentSpeed *= Math.pow(0.1, brakeForce * deltaTime);
}
// Natural deceleration
this.currentSpeed *= Math.pow(0.98, deltaTime * 60);
// Speed limits
this.currentSpeed = Math.max(-this.maxSpeed * 0.5,
Math.min(this.maxSpeed * boostMultiplier, this.currentSpeed));
// Apply velocity
const forward = new THREE.Vector3(0, 0, 1);
forward.applyQuaternion(this.mesh.quaternion);
this.velocity = forward.multiplyScalar(this.currentSpeed);
// Apply flocking forces for AI cars
if (!this.isPlayer && this.flockingForce.length() > 0) {
this.velocity.add(this.flockingForce.multiplyScalar(5 * deltaTime));
}
// Update position
this.mesh.position.add(this.velocity.clone().multiplyScalar(deltaTime));
// Wheel rotation
this.wheels.forEach(wheel => {
wheel.rotation.x += this.currentSpeed * deltaTime * 0.2;
});
}
updateMovement(deltaTime) {
// Road following behavior for AI
if (!this.isPlayer && this.onRoad) {
const roadBonus = this.getRoadFollowingForce();
this.mesh.position.add(roadBonus.multiplyScalar(deltaTime * 2));
}
}
getRoadFollowingForce() {
const pos = this.mesh.position;
const roadSpacing = 80;
const roadHalfWidth = ROAD_WIDTH / 2;
const nearestHorizontalRoad = Math.round(pos.z / roadSpacing) * roadSpacing;
const nearestVerticalRoad = Math.round(pos.x / roadSpacing) * roadSpacing;
const distToHorizontalRoad = pos.z - nearestHorizontalRoad;
const distToVerticalRoad = pos.x - nearestVerticalRoad;
const force = new THREE.Vector3();
if (Math.abs(distToHorizontalRoad) <= roadHalfWidth) {
force.z = -distToHorizontalRoad * 0.5;
}
if (Math.abs(distToVerticalRoad) <= roadHalfWidth) {
force.x = -distToVerticalRoad * 0.5;
}
return force;
}
updateDestination() {
if (!this.destination) return;
const distanceToDestination = this.mesh.position.distanceTo(this.destination.mesh.position);
if (distanceToDestination < BUILDING_VISIT_DISTANCE) {
if (!this.isAtDestination) {
this.isAtDestination = true;
this.lastDestinationTime = Date.now();
}
// Stay at destination for a while, then pick new one
if (Date.now() - this.lastDestinationTime > 3000) {
this.setRandomDestination();
}
}
// Update path following
if (this.currentPath.length > 0 && this.pathIndex < this.currentPath.length) {
const waypoint = this.currentPath[this.pathIndex];
const distanceToWaypoint = this.mesh.position.distanceTo(waypoint);
if (distanceToWaypoint < 10) {
this.pathIndex++;
}
}
}
getObstacles() {
let obstacles = [];
population.forEach(car => {
if (car !== this) {
obstacles.push(car.mesh);
}
});
world.buildings.forEach(building => {
obstacles.push(building.mesh);
});
return obstacles;
}
keepInBounds() {
const bounds = 300;
if (Math.abs(this.mesh.position.x) > bounds ||
Math.abs(this.mesh.position.z) > bounds) {
if (Math.abs(this.mesh.position.x) > bounds) {
this.mesh.position.x = Math.sign(this.mesh.position.x) * bounds;
this.velocity.x *= -0.5;
}
if (Math.abs(this.mesh.position.z) > bounds) {
this.mesh.position.z = Math.sign(this.mesh.position.z) * bounds;
this.velocity.z *= -0.5;
}
}
}
}
function init() {
scene = new THREE.Scene();
scene.background = new THREE.Color(0x87CEEB);
scene.fog = new THREE.Fog(0x87CEEB, 200, 800);
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 80, 80);
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);
// Lighting
const ambientLight = new THREE.AmbientLight(0x404040, 0.6);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(50, 100, 50);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = 2048;
directionalLight.shadow.mapSize.height = 2048;
scene.add(directionalLight);
createWorld();
createPopulation();
clock = new THREE.Clock();
setupEventListeners();
setupKeyboardControls();
setupMouseControls();
animate();
}
function createWorld() {
// Ground
const groundGeometry = new THREE.PlaneGeometry(800, 800);
const groundMaterial = new THREE.MeshLambertMaterial({ color: 0x228B22 });
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2;
ground.receiveShadow = true;
scene.add(ground);
createRoadNetwork();
createBuildings();
createTrafficLights();
}
function createRoadNetwork() {
const roadMaterial = new THREE.MeshLambertMaterial({ color: 0x444444 });
const roadSpacing = 80;
// Create road grid
for (let i = -240; i <= 240; i += roadSpacing) {
// Horizontal roads
const hRoadGeometry = new THREE.PlaneGeometry(480, ROAD_WIDTH);
const hRoad = new THREE.Mesh(hRoadGeometry, roadMaterial);
hRoad.rotation.x = -Math.PI / 2;
hRoad.position.set(0, 0.1, i);
scene.add(hRoad);
// Vertical roads
const vRoadGeometry = new THREE.PlaneGeometry(ROAD_WIDTH, 480);
const vRoad = new THREE.Mesh(vRoadGeometry, roadMaterial);
vRoad.rotation.x = -Math.PI / 2;
vRoad.position.set(i, 0.1, 0);
scene.add(vRoad);
// Road markings
createRoadMarkings(i);
}
}
function createRoadMarkings(position) {
const markingMaterial = new THREE.MeshLambertMaterial({ color: 0xffffff });
// Center line markings
for (let j = -200; j <= 200; j += 20) {
const markingGeometry = new THREE.PlaneGeometry(1, 8);
const marking = new THREE.Mesh(markingGeometry, markingMaterial);
marking.rotation.x = -Math.PI / 2;
marking.position.set(j, 0.11, position);
scene.add(marking);
const vMarking = new THREE.Mesh(markingGeometry.clone(), markingMaterial);
vMarking.rotation.x = -Math.PI / 2;
vMarking.rotation.z = Math.PI / 2;
vMarking.position.set(position, 0.11, j);
scene.add(vMarking);
}
}
function createBuildings() {
world.buildings = [];
const buildingMaterial = new THREE.MeshLambertMaterial({ color: 0x666666 });
// Create buildings in grid pattern with some randomness
for (let x = -200; x <= 200; x += 80) {
for (let z = -200; z <= 200; z += 80) {
if (Math.random() > 0.3) { // Don't place building everywhere
const offsetX = x + (Math.random() - 0.5) * 40;
const offsetZ = z + (Math.random() - 0.5) * 40;
const width = 15 + Math.random() * 20;
const height = 10 + Math.random() * 30;
const depth = 15 + Math.random() * 20;
const buildingGeometry = new THREE.BoxGeometry(width, height, depth);
const building = new THREE.Mesh(buildingGeometry, buildingMaterial);
building.position.set(offsetX, height / 2, offsetZ);
building.castShadow = true;
scene.add(building);
world.buildings.push({ mesh: building });
}
}
}
}
function createTrafficLights() {
world.trafficLights = [];
const roadSpacing = 80;
// Place traffic lights at major intersections
for (let x = -160; x <= 160; x += roadSpacing) {
for (let z = -160; z <= 160; z += roadSpacing) {
if (Math.random() > 0.7) {
const light = createTrafficLight(x, z);
world.trafficLights.push(light);
}
}
}
}
function createTrafficLight(x, z) {
const poleGeometry = new THREE.CylinderGeometry(0.2, 0.2, 8);
const poleMaterial = new THREE.MeshLambertMaterial({ color: 0x333333 });
const pole = new THREE.Mesh(poleGeometry, poleMaterial);
pole.position.set(x + 6, 4, z + 6);
scene.add(pole);
const lightBoxGeometry = new THREE.BoxGeometry(1, 3, 1);
const lightBoxMaterial = new THREE.MeshLambertMaterial({ color: 0x222222 });
const lightBox = new THREE.Mesh(lightBoxGeometry, lightBoxMaterial);
lightBox.position.set(x + 6, 7, z + 6);
scene.add(lightBox);
// Light states
const states = ['red', 'yellow', 'green'];
const currentState = states[Math.floor(Math.random() * states.length)];
return {
pole: pole,
lightBox: lightBox,
position: new THREE.Vector3(x, 0, z),
state: currentState,
lastChange: Date.now()
};
}
function createPopulation() {
population = [];
// Create AI cars
for (let i = 0; i < populationSize - 1; i++) {
const angle = (i / populationSize) * Math.PI * 2;
const radius = 30 + Math.random() * 50;
const x = Math.cos(angle) * radius;
const z = Math.sin(angle) * radius;
const car = new RealisticAICar(x, z, false);
population.push(car);
scene.add(car.mesh);
}
// Create player car
playerCar = new RealisticAICar(0, 0, true);
population.push(playerCar);
scene.add(playerCar.mesh);
currentTarget = playerCar;
}
function animate() {
requestAnimationFrame(animate);
if (!paused) {
const deltaTime = Math.min(clock.getDelta() * speedMultiplier, 0.1);
updatePopulation(deltaTime);
updateTrafficLights();
updateCamera();
updateUI();
}
renderer.render(scene, camera);
}
function updatePopulation(deltaTime) {
population.forEach(car => car.update(deltaTime));
}
function updateTrafficLights() {
world.trafficLights.forEach(light => {
if (Date.now() - light.lastChange > 5000) {
const states = ['red', 'yellow', 'green'];
const currentIndex = states.indexOf(light.state);
light.state = states[(currentIndex + 1) % states.length];
light.lastChange = Date.now();
// Update light color
const colors = { red: 0xff0000, yellow: 0xffff00, green: 0x00ff00 };
light.lightBox.material.color.setHex(colors[light.state]);
}
});
}
function updateCamera() {
if (!currentTarget) return;
const targetPos = currentTarget.mesh.position;
const targetRot = currentTarget.mesh.rotation;
switch (cameraMode) {
case 'firstPerson':
// First person view
camera.position.copy(targetPos);
camera.position.y += 2;
camera.position.add(new THREE.Vector3(0, 0, 2).applyQuaternion(currentTarget.mesh.quaternion));
const lookDirection = new THREE.Vector3(0, 0, -1).applyQuaternion(currentTarget.mesh.quaternion);
lookDirection.add(new THREE.Vector3(mouseControls.x, mouseControls.y, 0));
camera.lookAt(targetPos.clone().add(lookDirection.multiplyScalar(10)));
break;
case 'thirdPerson':
// Third person view
const behindOffset = new THREE.Vector3(0, 8, 15).applyQuaternion(currentTarget.mesh.quaternion);
camera.position.lerp(targetPos.clone().sub(behindOffset), 0.1);
camera.lookAt(targetPos);
break;
case 'overview':
// Overview of the area
camera.position.lerp(new THREE.Vector3(0, 150, 150), 0.02);
camera.lookAt(targetPos);
break;
case 'free':
// Free camera movement
updateFreeCamera();
break;
}
}
function updateFreeCamera() {
const moveSpeed = 2;
if (keys['KeyW']) camera.position.add(new THREE.Vector3(0, 0, -moveSpeed));
if (keys['KeyS']) camera.position.add(new THREE.Vector3(0, 0, moveSpeed));
if (keys['KeyA']) camera.position.add(new THREE.Vector3(-moveSpeed, 0, 0));
if (keys['KeyD']) camera.position.add(new THREE.Vector3(moveSpeed, 0, 0));
if (keys['KeyQ']) camera.position.y += moveSpeed;
if (keys['KeyE']) camera.position.y -= moveSpeed;
}
function updateUI() {
if (currentTarget) {
document.getElementById('currentMode').textContent = currentTarget.isPlayer ? 'Player Driving' : 'AI Observer';
document.getElementById('currentTarget').textContent = currentTarget.isPlayer ? 'Player' : 'AI Car';
document.getElementById('currentSpeed').textContent = Math.round(Math.abs(currentTarget.currentSpeed * 3.6));
document.getElementById('flockSize').textContent = currentTarget.neighbors.length;
if (currentTarget.destination) {
const distance = currentTarget.mesh.position.distanceTo(currentTarget.destination.mesh.position);
document.getElementById('targetDistance').textContent = Math.round(distance);
document.getElementById('currentDestination').textContent = 'Building';
} else {
document.getElementById('targetDistance').textContent = '0';
document.getElementById('currentDestination').textContent = 'None';
}
}
document.getElementById('population').textContent = population.length;
const avgSpeed = population.reduce((sum, car) => sum + Math.abs(car.currentSpeed), 0) / population.length;
document.getElementById('avgSpeed').textContent = Math.round(avgSpeed * 3.6);
const activeRoutes = population.filter(car => car.currentPath.length > 0).length;
document.getElementById('activeRoutes').textContent = activeRoutes;
document.getElementById('cameraMode').textContent = cameraMode;
// Update speedometer
if (currentTarget && currentTarget.isPlayer) {
const speedPercent = Math.abs(currentTarget.currentSpeed) / currentTarget.maxSpeed;
const needleRotation = -90 + (speedPercent * 180);
document.getElementById('speedNeedle').style.transform =
`translate(-50%, -100%) rotate(${needleRotation}deg)`;
}
}
function setupEventListeners() {
// UI controls
document.getElementById('pauseBtn').addEventListener('click', togglePause);
document.getElementById('resetBtn').addEventListener('click', resetSimulation);
document.getElementById('speedBtn').addEventListener('click', toggleSpeed);
document.getElementById('trafficBtn').addEventListener('click', toggleTraffic);
document.getElementById('weatherBtn').addEventListener('click', toggleWeather);
// Camera controls
document.getElementById('firstPersonBtn').addEventListener('click', () => setCameraMode('firstPerson'));
document.getElementById('thirdPersonBtn').addEventListener('click', () => setCameraMode('thirdPerson'));
document.getElementById('overviewBtn').addEventListener('click', () => setCameraMode('overview'));
document.getElementById('freeCamera').addEventListener('click', () => setCameraMode('free'));
document.getElementById('nextCarBtn').addEventListener('click', switchToNextCar);
window.addEventListener('resize', onWindowResize);
}
function setupKeyboardControls() {
document.addEventListener('keydown', (event) => {
keys[event.code] = true;
if (event.code === 'Tab') {
event.preventDefault();
switchToNextCar();
}
if (event.code === 'Escape') {
setCameraMode('overview');
}
});
document.addEventListener('keyup', (event) => {
keys[event.code] = false;
});
}
function setupMouseControls() {
let isMouseLocked = false;
document.addEventListener('click', () => {
if (cameraMode === 'firstPerson' || cameraMode === 'free') {
document.body.requestPointerLock();
}
});
document.addEventListener('pointerlockchange', () => {
isMouseLocked = document.pointerLockElement === document.body;
});
document.addEventListener('mousemove', (event) => {
if (isMouseLocked) {
mouseControls.x += event.movementX * mouseControls.sensitivity;
mouseControls.y -= event.movementY * mouseControls.sensitivity;
mouseControls.x = Math.max(-Math.PI/3, Math.min(Math.PI/3, mouseControls.x));
mouseControls.y = Math.max(-Math.PI/6, Math.min(Math.PI/6, mouseControls.y));
}
});
}
function setCameraMode(mode) {
cameraMode = mode;
// Update UI
document.querySelectorAll('#cameraControls button').forEach(btn => {
btn.classList.remove('active');
});
const buttonMap = {
'firstPerson': 'firstPersonBtn',
'thirdPerson': 'thirdPersonBtn',
'overview': 'overviewBtn',
'free': 'freeCamera'
};
if (buttonMap[mode]) {
document.getElementById(buttonMap[mode]).classList.add('active');
}
// Show/hide UI elements
const showDriving = mode === 'firstPerson' && currentTarget && currentTarget.isPlayer;
document.getElementById('drivingControls').style.display = showDriving ? 'block' : 'none';
document.getElementById('speedometer').style.display = showDriving ? 'block' : 'none';
document.getElementById('crosshair').style.display = mode === 'firstPerson' ? 'block' : 'none';
mouseControls.x = 0;
mouseControls.y = 0;
}
function switchToNextCar() {
if (population.length === 0) return;
const currentIndex = population.indexOf(currentTarget);
const nextIndex = (currentIndex + 1) % population.length;
currentTarget = population[nextIndex];
// If switching to player car, enable driving controls
if (currentTarget.isPlayer && cameraMode !== 'overview') {
setCameraMode('firstPerson');
}
}
function togglePause() {
paused = !paused;
document.getElementById('pauseBtn').textContent = paused ? 'Resume' : 'Pause';
}
function resetSimulation() {
population.forEach(car => {
if (car.mesh.parent) scene.remove(car.mesh);
});
createPopulation();
currentTarget = playerCar;
}
function toggleSpeed() {
speedMultiplier = speedMultiplier === 1 ? 2 : speedMultiplier === 2 ? 5 : 1;
document.getElementById('speedBtn').textContent = `Speed: ${speedMultiplier}x`;
}
function toggleTraffic() {
const levels = ['light', 'normal', 'heavy'];
const currentIndex = levels.indexOf(trafficDensity);
trafficDensity = levels[(currentIndex + 1) % levels.length];
document.getElementById('trafficBtn').textContent = `Traffic: ${trafficDensity}`;
document.getElementById('trafficFlow').textContent = trafficDensity;
}
function toggleWeather() {
const conditions = ['clear', 'rain', 'fog'];
const currentIndex = conditions.indexOf(weatherCondition);
weatherCondition = conditions[(currentIndex + 1) % conditions.length];
document.getElementById('weatherBtn').textContent = `Weather: ${weatherCondition}`;
// Update visual effects
if (weatherCondition === 'fog') {
scene.fog.near = 50;
scene.fog.far = 200;
} else {
scene.fog.near = 200;
scene.fog.far = 800;
}
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
init();
</script>
</body>
</html>