Spaces:
Running
Running
<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> |