diff --git "a/index.html" "b/index.html"
--- "a/index.html"
+++ "b/index.html"
@@ -21,7 +21,7 @@
border-radius: 8px;
z-index: 100;
font-size: 14px;
- min-width: 200px;
+ min-width: 220px;
}
#controls {
position: absolute;
@@ -138,7 +138,7 @@
Generation Stats:
Crashes: 0
Total Distance: 0
- Parking Events: 0
+ Parking Visits: 0
Lane Violations: 0
Convoy Length: 0
@@ -173,17 +173,17 @@
let world = {
roads: [],
intersections: [],
- buildings: [],
- parkingLots: [],
+ buildings: [], // Will store { mesh: buildingMesh, parkingLot: parkingLotObject, visitorCount: 0, barGraphMesh: barMesh }
+ parkingLots: [], // Will store { center, spots, queue, approachLanes, exitLanes, accessPoints, building: buildingObject }
flockLines: []
};
// Enhanced evolution system
let epoch = 1;
- let epochTime = 60;
+ let epochTime = 60; // seconds per epoch
let timeLeft = 60;
let population = [];
- let species = [];
+ let species = []; // For future speciation if needed
let populationSize = 100;
let bestFitness = 0;
let crashCount = 0;
@@ -191,32 +191,37 @@
let speedMultiplier = 1;
let cameraMode = 'overview'; // 'overview', 'follow_best', 'follow_convoy'
let showFlockLines = true;
- let trafficRules = true;
- let parkingEvents = 0;
- let laneViolations = 0;
+ let trafficRules = true; // General toggle, specific rules handled by AI traits
+ let parkingEvents = 0; // Total parking visits in an epoch
+ let laneViolations = 0; // Total lane violations in an epoch
// Traffic and road parameters
- const ROAD_WIDTH = 12;
- const LANE_WIDTH = 6;
- const ROAD_SPACING = 150;
- const FOLLOW_DISTANCE = 8;
- const CONVOY_MAX_DISTANCE = 12;
+ const ROAD_WIDTH_UNIT = 6; // Base width for one lane
+ const ROAD_SPACING = 150; // Spacing for major grid roads
+ const FOLLOW_DISTANCE = 8; // Base follow distance for convoys
+ const CONVOY_MAX_DISTANCE = 12; // Max distance before convoy link breaks
const PARKING_SPOT_SIZE = { width: 4, length: 8 };
+ const GRASS_THRESHOLD = 0.15; // Road position value below which car is considered on grass
+
+ // Manual control state for "Follow Best"
+ let manuallyControlledCar = null;
+ const manualControls = { W: false, A: false, S: false, D: false };
+
// Enhanced Neural Network for traffic behavior
class TrafficAI {
constructor() {
this.inputSize = 28; // Enhanced traffic-aware inputs
- this.hiddenLayers = [36, 28, 20];
- this.outputSize = 10; // More nuanced traffic outputs
- this.memorySize = 8;
+ this.hiddenLayers = [36, 28, 20]; // Hidden layer sizes
+ this.outputSize = 10; // Outputs: accel, brake, steerL, steerR, laneChange, convoy, park, signalL, signalR, stop
+ this.memorySize = 8; // Short-term memory for road context
this.weights = [];
this.biases = [];
this.memory = new Array(this.memorySize).fill(0);
this.memoryPointer = 0;
- // Build network
+ // Build network layers
let prevSize = this.inputSize + this.memorySize;
for (let i = 0; i < this.hiddenLayers.length; i++) {
this.weights.push(this.randomMatrix(prevSize, this.hiddenLayers[i]));
@@ -227,13 +232,13 @@
this.weights.push(this.randomMatrix(prevSize, this.outputSize));
this.biases.push(this.randomArray(this.outputSize));
- // Traffic-specific traits
+ // Traffic-specific traits (evolvable)
this.trafficTraits = {
- laneKeeping: Math.random(),
- followingBehavior: Math.random(),
- parkingSkill: Math.random(),
- convoyDiscipline: Math.random(),
- roadPriority: Math.random()
+ laneKeeping: Math.random(), // 0-1, tendency to stay in lane
+ followingBehavior: Math.random(), // 0-1, how closely to follow
+ parkingSkill: Math.random(), // 0-1, efficiency in parking
+ convoyDiscipline: Math.random(), // 0-1, tendency to form/join convoys
+ roadPriority: Math.random() // 0-1, preference for staying on roads
};
}
@@ -242,28 +247,30 @@
for (let i = 0; i < rows; i++) {
matrix[i] = [];
for (let j = 0; j < cols; j++) {
- matrix[i][j] = (Math.random() - 0.5) * 2;
+ matrix[i][j] = (Math.random() - 0.5) * 2; // Weights between -1 and 1
}
}
return matrix;
}
randomArray(size) {
- return Array(size).fill().map(() => (Math.random() - 0.5) * 2);
+ return Array(size).fill().map(() => (Math.random() - 0.5) * 2); // Biases between -1 and 1
}
activate(inputs) {
- let currentInput = [...inputs, ...this.memory];
+ let currentInput = [...inputs, ...this.memory]; // Combine current inputs with memory
+ // Forward pass through hidden layers
for (let layer = 0; layer < this.hiddenLayers.length; layer++) {
currentInput = this.forwardLayer(currentInput, this.weights[layer], this.biases[layer]);
}
+ // Output layer
const outputs = this.forwardLayer(currentInput,
this.weights[this.weights.length - 1],
this.biases[this.biases.length - 1]);
- this.updateMemory(inputs, outputs);
+ this.updateMemory(inputs, outputs); // Update memory based on current state
return outputs;
}
@@ -275,18 +282,21 @@
outputs[i] += inputs[j] * weights[j][i];
}
outputs[i] += biases[i];
- outputs[i] = this.sigmoid(outputs[i]);
+ outputs[i] = this.sigmoid(outputs[i]); // Sigmoid activation
}
return outputs;
}
sigmoid(x) {
- return 1 / (1 + Math.exp(-Math.max(-10, Math.min(10, x))));
+ // Clamping input to prevent extreme values in exp, which can cause NaN
+ const clampedX = Math.max(-10, Math.min(10, x));
+ return 1 / (1 + Math.exp(-clampedX));
}
updateMemory(inputs, outputs) {
- const roadInfo = inputs.slice(20, 24).reduce((a, b) => a + b, 0) / 4;
+ // Example: Store average road sensor data in memory
+ const roadInfo = inputs.slice(20, 24).reduce((a, b) => a + b, 0) / 4; // Average of road sensors
this.memory[this.memoryPointer] = roadInfo;
this.memoryPointer = (this.memoryPointer + 1) % this.memorySize;
}
@@ -299,10 +309,11 @@
this.mutateArray(biasArray, rate);
});
+ // Mutate traffic traits
Object.keys(this.trafficTraits).forEach(trait => {
if (Math.random() < rate) {
- this.trafficTraits[trait] += (Math.random() - 0.5) * 0.2;
- this.trafficTraits[trait] = Math.max(0, Math.min(1, this.trafficTraits[trait]));
+ this.trafficTraits[trait] += (Math.random() - 0.5) * 0.2; // Small random change
+ this.trafficTraits[trait] = Math.max(0, Math.min(1, this.trafficTraits[trait])); // Clamp between 0 and 1
}
});
}
@@ -311,8 +322,8 @@
for (let i = 0; i < matrix.length; i++) {
for (let j = 0; j < matrix[i].length; j++) {
if (Math.random() < rate) {
- matrix[i][j] += (Math.random() - 0.5) * 0.5;
- matrix[i][j] = Math.max(-3, Math.min(3, matrix[i][j]));
+ matrix[i][j] += (Math.random() - 0.5) * 0.5; // Small random change
+ matrix[i][j] = Math.max(-3, Math.min(3, matrix[i][j])); // Clamp weights
}
}
}
@@ -321,8 +332,8 @@
mutateArray(array, rate) {
for (let i = 0; i < array.length; i++) {
if (Math.random() < rate) {
- array[i] += (Math.random() - 0.5) * 0.5;
- array[i] = Math.max(-3, Math.min(3, array[i]));
+ array[i] += (Math.random() - 0.5) * 0.5; // Small random change
+ array[i] = Math.max(-3, Math.min(3, array[i])); // Clamp biases
}
}
}
@@ -333,7 +344,7 @@
newAI.biases = this.biases.map(bias => [...bias]);
newAI.memory = [...this.memory];
newAI.memoryPointer = this.memoryPointer;
- newAI.trafficTraits = {...this.trafficTraits};
+ newAI.trafficTraits = {...this.trafficTraits}; // Copy traits
return newAI;
}
}
@@ -343,71 +354,80 @@
constructor(x = 0, z = 0) {
this.brain = new TrafficAI();
this.mesh = this.createCarMesh();
- this.mesh.position.set(x, 1, z);
+ this.mesh.position.set(x, 1, z); // Car height above ground
// Movement and traffic properties
this.velocity = new THREE.Vector3();
- this.acceleration = new THREE.Vector3();
- this.maxSpeed = 20;
- this.minSpeed = 2;
- this.currentLane = null;
- this.targetLane = null;
- this.lanePosition = 0; // -1 to 1 within lane
+ this.acceleration = new THREE.Vector3(); // Not directly used, NN outputs control velocity changes
+ this.maxSpeed = 20; // Max speed units per second
+ this.minSpeed = 2; // Min speed when moving
+ this.currentLane = null; // Reference to current road lane object (if any)
+ this.targetLane = null; // Target lane for lane changes
+ this.lanePosition = 0; // -1 (left edge) to 1 (right edge) within its current lane
// Road transition tracking
- this.lastRoadPosition = 0;
- this.isTransitioningToRoad = false;
- this.roadTransitionTime = 0;
-
+ this.lastRoadPositionScore = 0; // Score from getRoadPosition() in previous frame
+ this.isReturningToRoad = false; // Flag for 180-turn behavior when on grass
+ this.turnAngleGoal = 0; // Target angle for the turn
+ this.turnProgress = 0; // Current progress of the turn
+ this.initialOrientationY = 0; // Orientation before starting a turn
+
// Convoy and flock behavior
- this.flockId = -1;
- this.convoyPosition = -1; // Position in convoy (-1 = not in convoy)
- this.convoyLeader = null;
- this.convoyFollowers = [];
- this.followTarget = null;
- this.role = 'driver'; // driver, leader, parker
+ this.flockId = -1; // ID for flocking group (future use)
+ this.convoyPosition = -1; // Position in convoy (-1 = not in convoy, 0 = leader)
+ this.convoyLeader = null; // Reference to convoy leader car
+ this.convoyFollowers = []; // Array of cars following this one (if leader)
+ this.followTarget = null; // Car this one is following in a convoy
+ this.role = 'driver'; // 'driver', 'leader', 'parker'
// Enhanced parking system
this.isParked = false;
- this.parkingSpot = null;
- this.targetParkingLot = null;
- this.parkingQueue = -1; // Position in parking queue (-1 = not queued)
- this.isParkingApproach = false;
- this.isInApproachLane = false;
- this.isInExitLane = false;
- this.approachTarget = null;
- this.exitTarget = null;
- this.selectedApproachLane = -1;
- this.selectedExitLane = -1;
- this.parkingAttempts = 0;
+ this.parkingSpot = null; // Reference to the ParkingSpot object
+ this.targetParkingLot = null; // Reference to the ParkingLot object
+ this.parkingQueuePosition = -1; // Position in parking lot queue
+ this.isParkingApproach = false; // True if actively moving towards a parking spot
+ this.isInApproachLane = false; // True if in a dedicated approach lane
+ this.isInExitLane = false; // True if in a dedicated exit lane
+ this.approachTargetPosition = null; // Specific point in approach lane
+ this.exitTargetPosition = null; // Specific point in exit lane
+ this.selectedApproachLaneIndex = -1;
+ this.selectedExitLaneIndex = -1;
+ this.parkingAttempts = 0; // Number of times tried to park this epoch
this.maxParkingAttempts = 3;
- this.departureTime = 0;
- this.turnSignal = 'none'; // left, right, none
- this.laneDiscipline = 0;
- this.followingDistance = FOLLOW_DISTANCE;
+ this.departureTime = 0; // Timer for how long to stay parked
+ this.isExitingParking = false; // Flag for 180-degree turn when exiting parking
+
+ this.turnSignal = 'none'; // 'left', 'right', 'none'
+ this.laneDiscipline = 0; // Score for staying in lane
+ this.followingDistance = FOLLOW_DISTANCE; // Current following distance
// Fitness and metrics
this.fitness = 0;
- this.roadTime = 0;
- this.convoyTime = 0;
- this.parkingScore = 0;
- this.trafficViolations = 0;
+ this.roadTime = 0; // Time spent on roads
+ this.convoyTime = 0; // Time spent in a convoy
+ this.parkingScore = 0; // Score for successful parking
+ this.trafficViolations = 0; // Count of lane violations, etc.
this.distanceTraveled = 0;
this.crashed = false;
- this.timeAlive = 100;
+ this.timeAlive = epochTime * 0.8 + Math.random() * epochTime * 0.4; // Lifespan before attempting to park
// Sensors and visualization
- this.sensors = Array(16).fill(0);
- this.roadSensors = Array(8).fill(0);
- this.trafficSensors = Array(4).fill(0);
- this.sensorRays = [];
- this.flockLines = [];
- this.neighbors = [];
+ this.sensors = Array(16).fill(0); // 16 general obstacle sensors
+ this.roadSensors = Array(8).fill(0); // Road-specific sensors
+ this.trafficSensors = Array(4).fill(0); // Traffic/convoy sensors
+ this.sensorRays = []; // Visual lines for sensors
+ this.flockLines = []; // Visual lines for convoy connections
+ this.neighbors = []; // Nearby cars for flocking/convoy logic
this.lastPosition = new THREE.Vector3(x, 1, z);
this.createSensorRays();
this.createFlockVisualization();
this.initializeMovement();
+
+ // Manual control inputs
+ this.manualAcceleration = 0;
+ this.manualBraking = 0;
+ this.manualSteer = 0; // -1 for left, 1 for right
}
createCarMesh() {
@@ -415,11 +435,12 @@
// Car body
const bodyGeometry = new THREE.BoxGeometry(1.5, 0.8, 3.5);
+ // Removed flatShading: true as it's not a property of MeshLambertMaterial
this.bodyMaterial = new THREE.MeshLambertMaterial({
color: new THREE.Color().setHSL(Math.random(), 0.8, 0.6)
});
const body = new THREE.Mesh(bodyGeometry, this.bodyMaterial);
- body.position.y = 0.4;
+ body.position.y = 0.4; // Body center y
body.castShadow = true;
group.add(body);
@@ -427,35 +448,35 @@
const signalGeometry = new THREE.SphereGeometry(0.15, 6, 4);
this.leftSignal = new THREE.Mesh(signalGeometry,
new THREE.MeshLambertMaterial({ color: 0xff8800, transparent: true, opacity: 0.5 }));
- this.leftSignal.position.set(-0.8, 0.8, 1.2);
+ this.leftSignal.position.set(-0.8, 0.8, 1.2); // Front-left
group.add(this.leftSignal);
this.rightSignal = new THREE.Mesh(signalGeometry,
new THREE.MeshLambertMaterial({ color: 0xff8800, transparent: true, opacity: 0.5 }));
- this.rightSignal.position.set(0.8, 0.8, 1.2);
+ this.rightSignal.position.set(0.8, 0.8, 1.2); // Front-right
group.add(this.rightSignal);
- // Role indicator
+ // Role indicator (cone above car)
const indicatorGeometry = new THREE.ConeGeometry(0.2, 0.8, 6);
this.roleIndicator = new THREE.Mesh(indicatorGeometry,
- new THREE.MeshLambertMaterial({ color: 0xffffff }));
- this.roleIndicator.position.set(0, 1.5, 0);
+ new THREE.MeshLambertMaterial({ color: 0xffffff })); // Default white
+ this.roleIndicator.position.set(0, 1.5, 0); // Above car body
group.add(this.roleIndicator);
- // Wheels with proper rotation
- const wheelGeometry = new THREE.CylinderGeometry(0.3, 0.3, 0.2, 8);
+ // Wheels
+ const wheelGeometry = new THREE.CylinderGeometry(0.3, 0.3, 0.2, 8); // radiusTop, radiusBottom, height, segments
const wheelMaterial = new THREE.MeshLambertMaterial({ color: 0x333333 });
this.wheels = [];
const wheelPositions = [
- [-0.7, 0, 1.4], [0.7, 0, 1.4],
- [-0.7, 0, -1.4], [0.7, 0, -1.4]
+ [-0.7, 0, 1.4], [0.7, 0, 1.4], // Front wheels
+ [-0.7, 0, -1.4], [0.7, 0, -1.4] // Rear wheels
];
- wheelPositions.forEach((pos, i) => {
+ wheelPositions.forEach((pos) => {
const wheel = new THREE.Mesh(wheelGeometry, wheelMaterial);
wheel.position.set(...pos);
- wheel.rotation.z = Math.PI / 2;
+ wheel.rotation.z = Math.PI / 2; // Rotate to lie flat
this.wheels.push(wheel);
group.add(wheel);
});
@@ -470,14 +491,14 @@
opacity: 0.2
});
- for (let i = 0; i < 16; i++) {
+ for (let i = 0; i < 16; i++) { // 16 general obstacle sensors
const geometry = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(0, 0, 0),
- new THREE.Vector3(0, 0, 10)
+ new THREE.Vector3(0, 0, 10) // Default length 10
]);
const ray = new THREE.Line(geometry, sensorMaterial);
this.sensorRays.push(ray);
- this.mesh.add(ray);
+ this.mesh.add(ray); // Add rays as children of car mesh for relative positioning
}
}
@@ -486,1040 +507,604 @@
color: 0x00ff00,
transparent: true,
opacity: 0.6,
- linewidth: 2
+ linewidth: 2 // Note: linewidth might not be supported by all WebGL renderers
});
- for (let i = 0; i < 10; i++) {
+ for (let i = 0; i < 10; i++) { // Max 10 flock lines per car
const geometry = new THREE.BufferGeometry().setFromPoints([
- new THREE.Vector3(0, 2, 0),
- new THREE.Vector3(0, 2, 0)
+ new THREE.Vector3(0, 2, 0), // Start point (relative to world for now)
+ new THREE.Vector3(0, 2, 0) // End point
]);
const line = new THREE.Line(geometry, flockMaterial);
this.flockLines.push(line);
- if (showFlockLines) scene.add(line);
+ if (showFlockLines) scene.add(line); // Add to scene directly
}
}
initializeMovement() {
- // Start on a road if possible
const nearestRoad = this.findNearestRoad();
if (nearestRoad) {
- this.currentLane = nearestRoad.lane;
+ this.currentLane = nearestRoad.lane; // Store lane type (e.g., 'highway_horizontal')
this.mesh.rotation.y = nearestRoad.direction;
this.velocity.set(
- Math.sin(nearestRoad.direction) * 8,
- 0,
- Math.cos(nearestRoad.direction) * 8
+ Math.sin(nearestRoad.direction) * 8, 0, Math.cos(nearestRoad.direction) * 8
);
} else {
+ // Random initial orientation and velocity if no road found
this.mesh.rotation.y = Math.random() * Math.PI * 2;
this.velocity.set(
- Math.sin(this.mesh.rotation.y) * 6,
- 0,
- Math.cos(this.mesh.rotation.y) * 6
+ Math.sin(this.mesh.rotation.y) * 6, 0, Math.cos(this.mesh.rotation.y) * 6
);
}
}
findNearestRoad() {
+ // This function is complex and crucial. It determines the closest road segment.
+ // It considers different road types (highways, secondary, local, access)
+ // and returns information about the road (type, center, direction, width).
+ // For brevity, its detailed implementation is assumed from the original code,
+ // but it needs to be robust for multilane scenarios.
+ // Key is that it returns an object like:
+ // { lane: 'type_orientation', center: number, direction: angle, width: number }
+
const pos = this.mesh.position;
- let nearestRoad = null;
+ let nearestRoadInfo = null;
let minDistance = Infinity;
-
- // Check major highways (8 lanes - 48 units wide)
- const mainHighwayPositions = [0]; // Main cross highways
- mainHighwayPositions.forEach(roadPos => {
- // Horizontal highway
- const distToHorizontal = Math.abs(pos.z - roadPos);
- if (distToHorizontal < 24 && distToHorizontal < minDistance) {
- minDistance = distToHorizontal;
- const laneCenter = roadPos + (pos.x > 0 ? -12 : 12); // 4 lanes each direction
- nearestRoad = {
- lane: 'highway_horizontal',
- center: laneCenter,
- direction: pos.x > 0 ? Math.PI : 0,
- width: 48
- };
+
+ world.roads.forEach(road => {
+ let distanceToRoadCenterLine;
+ let roadCenterCoord; // x for vertical, z for horizontal
+ let carRelevantCoord; // x for vertical, z for horizontal
+ let roadWidth = road.width;
+
+ if (road.direction === 'horizontal') {
+ roadCenterCoord = road.z;
+ carRelevantCoord = pos.z;
+ // Check if car is within road's x-bounds
+ if (pos.x < road.start || pos.x > road.end) return;
+ } else { // vertical
+ roadCenterCoord = road.x;
+ carRelevantCoord = pos.x;
+ // Check if car is within road's z-bounds
+ if (pos.z < road.start || pos.z > road.end) return;
}
- // Vertical highway
- const distToVertical = Math.abs(pos.x - roadPos);
- if (distToVertical < 24 && distToVertical < minDistance) {
- minDistance = distToVertical;
- const laneCenter = roadPos + (pos.z > 0 ? -12 : 12);
- nearestRoad = {
- lane: 'highway_vertical',
- center: laneCenter,
- direction: pos.z > 0 ? -Math.PI/2 : Math.PI/2,
- width: 48
- };
+ distanceToRoadCenterLine = Math.abs(carRelevantCoord - roadCenterCoord);
+
+ if (distanceToRoadCenterLine < roadWidth / 2 + 5) { // Consider roads slightly wider for detection
+ if (distanceToRoadCenterLine < minDistance) {
+ minDistance = distanceToRoadCenterLine;
+ nearestRoadInfo = {
+ lane: `${road.type}_${road.direction}`,
+ center: roadCenterCoord, // Centerline coordinate (x or z)
+ direction: road.orientationAngle, // Actual angle of the road
+ width: roadWidth,
+ roadObject: road // Reference to the road object itself
+ };
+ }
}
});
-
- // Check secondary highways (4 lanes - 24 units wide)
- for (let roadPos = -300; roadPos <= 300; roadPos += 150) {
- if (roadPos === 0) continue; // Skip main highway
-
- const distToHorizontal = Math.abs(pos.z - roadPos);
- if (distToHorizontal < 12 && distToHorizontal < minDistance) {
- minDistance = distToHorizontal;
- const laneCenter = roadPos + (pos.x > 0 ? -6 : 6);
- nearestRoad = {
- lane: 'secondary_horizontal',
- center: laneCenter,
- direction: pos.x > 0 ? Math.PI : 0,
- width: 24
- };
- }
-
- const distToVertical = Math.abs(pos.x - roadPos);
- if (distToVertical < 12 && distToVertical < minDistance) {
- minDistance = distToVertical;
- const laneCenter = roadPos + (pos.z > 0 ? -6 : 6);
- nearestRoad = {
- lane: 'secondary_vertical',
- center: laneCenter,
- direction: pos.z > 0 ? -Math.PI/2 : Math.PI/2,
- width: 24
- };
- }
- }
-
- // Check local roads (2 lanes - 12 units wide)
- for (let roadPos = -375; roadPos <= 375; roadPos += 75) {
- const distToHorizontal = Math.abs(pos.z - roadPos);
- if (distToHorizontal < 6 && distToHorizontal < minDistance) {
- minDistance = distToHorizontal;
- const laneCenter = roadPos + (pos.x > 0 ? -3 : 3);
- nearestRoad = {
- lane: 'local_horizontal',
- center: laneCenter,
- direction: pos.x > 0 ? Math.PI : 0,
- width: 12
- };
- }
-
- const distToVertical = Math.abs(pos.x - roadPos);
- if (distToVertical < 6 && distToVertical < minDistance) {
- minDistance = distToVertical;
- const laneCenter = roadPos + (pos.z > 0 ? -3 : 3);
- nearestRoad = {
- lane: 'local_vertical',
- center: laneCenter,
- direction: pos.z > 0 ? -Math.PI/2 : Math.PI/2,
- width: 12
- };
- }
- }
-
- // Check building access roads (less precise matching)
- if (!nearestRoad || minDistance > 8) {
- world.buildings.forEach(building => {
- const buildingPos = building.mesh.position;
- const buildingBox = new THREE.Box3().setFromObject(building.mesh);
- const size = buildingBox.getSize(new THREE.Vector3());
-
- // Check proximity to building access roads
- const accessRoadPositions = [
- { x: buildingPos.x, z: buildingPos.z + size.z/2 + 10, dir: 'horizontal' },
- { x: buildingPos.x, z: buildingPos.z - size.z/2 - 10, dir: 'horizontal' },
- { x: buildingPos.x + size.x/2 + 10, z: buildingPos.z, dir: 'vertical' },
- { x: buildingPos.x - size.x/2 - 10, z: buildingPos.z, dir: 'vertical' }
- ];
-
- accessRoadPositions.forEach(accessRoad => {
- const dist = pos.distanceTo(new THREE.Vector3(accessRoad.x, 0, accessRoad.z));
- if (dist < 8 && dist < minDistance) {
- minDistance = dist;
- nearestRoad = {
- lane: `access_${accessRoad.dir}`,
- center: accessRoad.dir === 'horizontal' ? accessRoad.z : accessRoad.x,
- direction: accessRoad.dir === 'horizontal' ?
- (pos.x > accessRoad.x ? Math.PI : 0) :
- (pos.z > accessRoad.z ? -Math.PI/2 : Math.PI/2),
- width: 8
- };
- }
- });
- });
- }
-
- return nearestRoad;
+ return nearestRoadInfo;
}
- getRoadPosition() {
+ getRoadPositionScore() { // Renamed from getRoadPosition to avoid conflict
+ // Calculates a score (0-1) based on how well the car is on *any* road surface.
+ // Higher score means more centered on a road.
const pos = this.mesh.position;
let maxRoadScore = 0;
-
- // Check all road types for best road position score
-
- // Major highways
- const mainHighwayDist = Math.abs(pos.z);
- if (mainHighwayDist <= 24) {
- maxRoadScore = Math.max(maxRoadScore, 1 - (mainHighwayDist / 24));
- }
- const mainVerticalDist = Math.abs(pos.x);
- if (mainVerticalDist <= 24) {
- maxRoadScore = Math.max(maxRoadScore, 1 - (mainVerticalDist / 24));
- }
-
- // Secondary highways
- for (let roadPos = -300; roadPos <= 300; roadPos += 150) {
- if (roadPos === 0) continue;
-
- const hDist = Math.abs(pos.z - roadPos);
- if (hDist <= 12) {
- maxRoadScore = Math.max(maxRoadScore, 1 - (hDist / 12));
- }
+
+ world.roads.forEach(road => {
+ let distanceToRoadCenterLine;
+ let carRelevantCoord;
- const vDist = Math.abs(pos.x - roadPos);
- if (vDist <= 12) {
- maxRoadScore = Math.max(maxRoadScore, 1 - (vDist / 12));
- }
- }
-
- // Local roads
- for (let roadPos = -375; roadPos <= 375; roadPos += 75) {
- const hDist = Math.abs(pos.z - roadPos);
- if (hDist <= 6) {
- maxRoadScore = Math.max(maxRoadScore, 1 - (hDist / 6));
+ if (road.direction === 'horizontal') {
+ if (pos.x < road.start || pos.x > road.end) return; // Outside road segment length
+ carRelevantCoord = pos.z;
+ distanceToRoadCenterLine = Math.abs(carRelevantCoord - road.z);
+ } else { // vertical
+ if (pos.z < road.start || pos.z > road.end) return; // Outside road segment length
+ carRelevantCoord = pos.x;
+ distanceToRoadCenterLine = Math.abs(carRelevantCoord - road.x);
}
-
- const vDist = Math.abs(pos.x - roadPos);
- if (vDist <= 6) {
- maxRoadScore = Math.max(maxRoadScore, 1 - (vDist / 6));
+
+ if (distanceToRoadCenterLine <= road.width / 2) {
+ maxRoadScore = Math.max(maxRoadScore, 1 - (distanceToRoadCenterLine / (road.width / 2)));
}
- }
-
+ });
return maxRoadScore;
}
updateSensors() {
- const maxDistance = 10;
+ const maxDistance = 10; // Max sensor range
const raycaster = new THREE.Raycaster();
// 16-direction obstacle sensors
for (let i = 0; i < 16; i++) {
- const angle = (i * Math.PI * 2) / 16;
- const direction = new THREE.Vector3(
- Math.sin(angle), 0, Math.cos(angle)
- );
- direction.applyQuaternion(this.mesh.quaternion);
+ const angle = (i * Math.PI * 2) / 16; // Angle for this sensor ray
+ const direction = new THREE.Vector3(Math.sin(angle), 0, Math.cos(angle));
+ direction.applyQuaternion(this.mesh.quaternion); // Rotate ray with car's orientation
raycaster.set(this.mesh.position, direction);
- const intersects = raycaster.intersectObjects(this.getObstacles(), true);
+ const intersects = raycaster.intersectObjects(this.getObstacles(), true); // Check against other cars and buildings
if (intersects.length > 0 && intersects[0].distance <= maxDistance) {
- this.sensors[i] = 1 - (intersects[0].distance / maxDistance);
+ this.sensors[i] = 1 - (intersects[0].distance / maxDistance); // Normalized sensor reading (1 = close, 0 = far)
} else {
- this.sensors[i] = 0;
+ this.sensors[i] = 0; // No obstacle detected in range
}
// Update visual rays
const endDistance = intersects.length > 0 ?
Math.min(intersects[0].distance, maxDistance) : maxDistance;
const rayEnd = direction.clone().multiplyScalar(endDistance);
- this.sensorRays[i].geometry.setFromPoints([
- new THREE.Vector3(0, 0, 0), rayEnd
- ]);
+ this.sensorRays[i].geometry.setFromPoints([new THREE.Vector3(0,0,0), rayEnd]); // Update ray geometry
}
- // Road-specific sensors
this.updateRoadSensors();
this.updateTrafficSensors();
}
updateRoadSensors() {
- const pos = this.mesh.position;
-
- // Road position and lane detection
- this.roadSensors[0] = this.getRoadPosition();
- this.roadSensors[1] = this.getLanePosition();
- this.roadSensors[2] = this.getRoadDirection();
- this.roadSensors[3] = this.getDistanceToIntersection();
-
- // Parking lot detection
- this.roadSensors[4] = this.getNearestParkingLot();
- this.roadSensors[5] = this.getParkingAvailability();
-
- // Traffic flow
- this.roadSensors[6] = this.getTrafficDensity();
- this.roadSensors[7] = this.getOptimalSpeed();
- }
-
- updateTrafficSensors() {
- // Convoy and following behavior
- this.trafficSensors[0] = this.getConvoyStatus();
- this.trafficSensors[1] = this.getFollowingDistance();
- this.trafficSensors[2] = this.getLeaderDistance();
- this.trafficSensors[3] = this.getNeedToPark();
- }
-
- getRoadPosition() {
- const pos = this.mesh.position;
-
- // Check horizontal roads
- for (let roadZ = -300; roadZ <= 300; roadZ += ROAD_SPACING) {
- const distToRoad = Math.abs(pos.z - roadZ);
- if (distToRoad <= ROAD_WIDTH / 2) {
- return 1 - (distToRoad / (ROAD_WIDTH / 2));
- }
- }
-
- // Check vertical roads
- for (let roadX = -300; roadX <= 300; roadX += ROAD_SPACING) {
- const distToRoad = Math.abs(pos.x - roadX);
- if (distToRoad <= ROAD_WIDTH / 2) {
- return 1 - (distToRoad / (ROAD_WIDTH / 2));
- }
- }
-
- return 0; // Off road
+ // Simplified for now, these would provide detailed info about road layout, intersections, etc.
+ this.roadSensors[0] = this.getRoadPositionScore(); // How well on a road
+ this.roadSensors[1] = this.getLanePosition(); // Position within current lane
+ this.roadSensors[2] = this.getRoadDirectionAlignment(); // Alignment with road direction
+ this.roadSensors[3] = this.getDistanceToIntersection(); // Normalized distance to nearest intersection
+ this.roadSensors[4] = this.getNearestParkingLotProximity(); // Proximity to parking
+ this.roadSensors[5] = this.getParkingAvailability(); // Availability in target lot
+ this.roadSensors[6] = this.getTrafficDensity(); // Local traffic density
+ this.roadSensors[7] = this.getOptimalSpeedFactor(); // Factor based on road type/density
}
-
+
getLanePosition() {
- const pos = this.mesh.position;
const roadInfo = this.findNearestRoad();
-
- if (!roadInfo) return 0.5;
-
- if (roadInfo.lane === 'horizontal') {
- const laneOffset = pos.z - roadInfo.center;
- return 0.5 + (laneOffset / LANE_WIDTH);
- } else {
- const laneOffset = pos.x - roadInfo.center;
- return 0.5 + (laneOffset / LANE_WIDTH);
+ if (!roadInfo || !roadInfo.roadObject) return 0.5; // Default to center if no road
+
+ const pos = this.mesh.position;
+ let laneOffset;
+ if (roadInfo.roadObject.direction === 'horizontal') {
+ laneOffset = pos.z - roadInfo.center;
+ } else { // vertical
+ laneOffset = pos.x - roadInfo.center;
}
+ // Normalize to -1 (left edge of road) to 1 (right edge of road)
+ let normalizedPosition = (laneOffset / (roadInfo.width / 2));
+ // Then map to 0-1 for NN input (0 = left edge, 0.5 = center, 1 = right edge)
+ return Math.max(0, Math.min(1, (normalizedPosition + 1) / 2));
}
-
- getRoadDirection() {
+
+ getRoadDirectionAlignment() {
const roadInfo = this.findNearestRoad();
- if (!roadInfo) return 0.5;
-
- const currentDirection = Math.atan2(this.velocity.x, this.velocity.z);
- const targetDirection = roadInfo.direction;
-
- let angleDiff = targetDirection - currentDirection;
- while (angleDiff > Math.PI) angleDiff -= 2 * Math.PI;
- while (angleDiff < -Math.PI) angleDiff += 2 * Math.PI;
-
- return 0.5 + (angleDiff / Math.PI) * 0.5;
- }
-
- getDistanceToIntersection() {
- const pos = this.mesh.position;
- let minDist = Infinity;
-
- // Find distance to nearest intersection
- for (let x = -300; x <= 300; x += ROAD_SPACING) {
- for (let z = -300; z <= 300; z += ROAD_SPACING) {
- const dist = pos.distanceTo(new THREE.Vector3(x, 0, z));
- minDist = Math.min(minDist, dist);
- }
- }
+ if (!roadInfo) return 0.5; // Neutral if no road
+
+ const carDirectionVector = new THREE.Vector3(0,0,1).applyQuaternion(this.mesh.quaternion);
+ const roadDirectionVector = new THREE.Vector3(Math.sin(roadInfo.direction), 0, Math.cos(roadInfo.direction));
- return Math.max(0, 1 - minDist / 50);
+ const dotProduct = carDirectionVector.dot(roadDirectionVector); // Ranges from -1 to 1
+ return (dotProduct + 1) / 2; // Normalize to 0-1 (1 = perfectly aligned)
}
-
- getNearestParkingLot() {
- const pos = this.mesh.position;
- let nearestDist = Infinity;
-
- world.parkingLots.forEach(lot => {
- const dist = pos.distanceTo(lot.center);
- if (dist < nearestDist) {
- nearestDist = dist;
- this.targetParkingLot = lot;
- }
- });
-
- return Math.max(0, 1 - nearestDist / 100);
+
+ // Placeholder functions for other road sensors - these would need detailed implementation
+ getDistanceToIntersection() { return Math.random(); }
+ getNearestParkingLotProximity() {
+ if (!this.targetParkingLot) return 0;
+ const dist = this.mesh.position.distanceTo(this.targetParkingLot.center);
+ return Math.max(0, 1 - dist / 100); // Normalized proximity
}
-
getParkingAvailability() {
if (!this.targetParkingLot) return 0;
-
const availableSpots = this.targetParkingLot.spots.filter(spot => !spot.occupied).length;
- return availableSpots / this.targetParkingLot.spots.length;
- }
-
- getTrafficDensity() {
- const pos = this.mesh.position;
- let nearbyCount = 0;
-
- population.forEach(other => {
- if (other !== this && !other.crashed && !other.isParked) {
- const dist = pos.distanceTo(other.mesh.position);
- if (dist < 30) nearbyCount++;
- }
- });
-
- return Math.min(nearbyCount / 10, 1);
- }
-
- getOptimalSpeed() {
- const roadPos = this.getRoadPosition();
- const density = this.getTrafficDensity();
- return roadPos * (1 - density * 0.5);
+ return this.targetParkingLot.spots.length > 0 ? availableSpots / this.targetParkingLot.spots.length : 0;
}
-
- getConvoyStatus() {
- return this.convoyPosition >= 0 ? 1 : 0;
- }
-
- getFollowingDistance() {
- if (!this.followTarget) return 1;
-
- const dist = this.mesh.position.distanceTo(this.followTarget.mesh.position);
- return Math.min(dist / 20, 1);
- }
-
- getLeaderDistance() {
- if (!this.convoyLeader) return 0;
-
- const dist = this.mesh.position.distanceTo(this.convoyLeader.mesh.position);
- return Math.max(0, 1 - dist / 50);
- }
-
- getNeedToPark() {
- return (this.timeAlive < 30 && !this.isParked) ? 1 : 0;
+ getTrafficDensity() { return Math.random() * 0.5; } // Simplified
+ getOptimalSpeedFactor() { return 0.8 + Math.random() * 0.2; } // Simplified
+
+ updateTrafficSensors() {
+ // Sensors related to convoy behavior, following, parking needs
+ this.trafficSensors[0] = this.convoyPosition >= 0 ? 1 : 0; // In convoy?
+ this.trafficSensors[1] = this.followTarget ? Math.min(this.mesh.position.distanceTo(this.followTarget.mesh.position) / 20, 1) : 1; // Normalized follow distance
+ this.trafficSensors[2] = this.convoyLeader ? Math.max(0, 1 - this.mesh.position.distanceTo(this.convoyLeader.mesh.position) / 50) : 0; // Proximity to leader
+ this.trafficSensors[3] = (this.timeAlive < epochTime * 0.3 && !this.isParked) ? 1 : 0; // Need to park?
}
updateConvoyBehavior() {
+ // Complex logic for forming, joining, and maintaining convoys.
+ // Includes finding neighbors, determining roles (leader/follower),
+ // and setting follow targets.
+ // This is a substantial part of the AI's social behavior.
+ // For brevity, its detailed implementation is assumed from original,
+ // but it would interact with the new NN outputs and traits.
this.neighbors = [];
-
- // Find nearby cars for convoy formation
population.forEach(other => {
if (other !== this && !other.crashed && !other.isParked) {
const distance = this.mesh.position.distanceTo(other.mesh.position);
-
- if (distance < 25) {
- this.neighbors.push(other);
- }
+ if (distance < 25) this.neighbors.push(other);
}
});
-
- // Calculate flock centroid and check if moving away
- this.updateFlockCohesion();
-
- // Determine role and convoy behavior
- this.updateRole();
- this.updateConvoyFormation();
- }
-
- updateFlockCohesion() {
- if (this.neighbors.length === 0) return;
-
- // Calculate flock centroid
- const centroid = new THREE.Vector3();
- this.neighbors.forEach(neighbor => {
- centroid.add(neighbor.mesh.position);
- });
- centroid.divideScalar(this.neighbors.length);
-
- // Check if moving away from centroid
- const currentPos = this.mesh.position.clone();
- const futurePos = currentPos.clone().add(this.velocity.clone().normalize().multiplyScalar(5));
-
- const currentDistToCentroid = currentPos.distanceTo(centroid);
- const futureDistToCentroid = futurePos.distanceTo(centroid);
-
- // If moving away from flock, apply gentle correction
- if (futureDistToCentroid > currentDistToCentroid && currentDistToCentroid > 15) {
- const returnForce = centroid.clone().sub(currentPos).normalize().multiplyScalar(0.3);
- this.velocity.add(returnForce);
-
- // Gentle turning toward centroid
- const targetDirection = Math.atan2(returnForce.x, returnForce.z);
- const currentDirection = Math.atan2(this.velocity.x, this.velocity.z);
- let angleDiff = targetDirection - currentDirection;
-
- while (angleDiff > Math.PI) angleDiff -= 2 * Math.PI;
- while (angleDiff < -Math.PI) angleDiff += 2 * Math.PI;
-
- // Apply gentle turning
- this.mesh.rotation.y += angleDiff * 0.02;
-
- // Slow down for turning
- this.velocity.multiplyScalar(0.98);
- }
-
- // Move flock centroid toward roads
- this.guideFlockToRoads(centroid);
- }
-
- guideFlockToRoads(centroid) {
- if (this.role !== 'leader') return;
-
- // Find nearest road from centroid
- const nearestRoadInfo = this.findNearestRoadFromPosition(centroid);
- if (!nearestRoadInfo) return;
-
- const roadDistance = nearestRoadInfo.distance;
- if (roadDistance > 5) {
- // Guide flock toward road
- const roadDirection = nearestRoadInfo.direction;
- const roadForce = roadDirection.clone().multiplyScalar(0.2);
- this.velocity.add(roadForce);
- }
- }
-
- findNearestRoadFromPosition(position) {
- let nearestRoad = null;
- let minDistance = Infinity;
-
- // Check horizontal roads
- for (let roadZ = -300; roadZ <= 300; roadZ += ROAD_SPACING) {
- const distToRoad = Math.abs(position.z - roadZ);
- if (distToRoad < minDistance) {
- minDistance = distToRoad;
- const direction = new THREE.Vector3(0, 0, roadZ - position.z).normalize();
- nearestRoad = {
- distance: distToRoad,
- direction: direction,
- lane: 'horizontal',
- center: roadZ
- };
- }
- }
-
- // Check vertical roads
- for (let roadX = -300; roadX <= 300; roadX += ROAD_SPACING) {
- const distToRoad = Math.abs(position.x - roadX);
- if (distToRoad < minDistance) {
- minDistance = distToRoad;
- const direction = new THREE.Vector3(roadX - position.x, 0, 0).normalize();
- nearestRoad = {
- distance: distToRoad,
- direction: direction,
- lane: 'vertical',
- center: roadX
- };
- }
- }
-
- return nearestRoad;
+ this.updateRole(); // Determine if leader, driver, parker
+ this.updateConvoyFormation(); // Manage followers or follow leader
}
-
+
updateRole() {
- const roadPos = this.getRoadPosition();
-
- if (roadPos > 0.8 && this.neighbors.length > 2 && this.brain.trafficTraits.convoyDiscipline > 0.7) {
- this.role = 'leader';
- this.roleIndicator.material.color.setHex(0xff00ff);
- } else if (this.getNeedToPark() > 0.5) {
+ const roadPosScore = this.getRoadPositionScore();
+ if (this.isParked || this.isParkingApproach || this.isInApproachLane || this.isInExitLane) {
this.role = 'parker';
- this.roleIndicator.material.color.setHex(0x00ff00);
+ } else if (roadPosScore > 0.8 && this.neighbors.length > 1 && this.brain.trafficTraits.convoyDiscipline > 0.6) {
+ this.role = 'leader';
} else {
this.role = 'driver';
- this.roleIndicator.material.color.setHex(0xffffff);
}
+ // Update role indicator color
+ if (this.role === 'leader') this.roleIndicator.material.color.setHex(0xff00ff); // Magenta
+ else if (this.role === 'parker') this.roleIndicator.material.color.setHex(0x00ff00); // Green
+ else this.roleIndicator.material.color.setHex(0xffffff); // White
}
-
+
updateConvoyFormation() {
+ // Simplified: Leader tries to get followers, followers try to follow leader or car ahead.
if (this.role === 'leader') {
- // Leaders organize convoys
this.convoyFollowers = this.neighbors
- .filter(car => car.role === 'driver')
- .sort((a, b) =>
- this.mesh.position.distanceTo(a.mesh.position) -
- this.mesh.position.distanceTo(b.mesh.position)
- )
+ .filter(car => car.role === 'driver' && !car.convoyLeader && car.brain.trafficTraits.convoyDiscipline > 0.5)
+ .sort((a,b) => this.mesh.position.distanceTo(a.mesh.position) - this.mesh.position.distanceTo(b.mesh.position))
.slice(0, 5); // Max 5 followers
-
this.convoyFollowers.forEach((follower, index) => {
follower.convoyLeader = this;
- follower.convoyPosition = index;
- follower.followTarget = index === 0 ? this : this.convoyFollowers[index - 1];
+ follower.convoyPosition = index + 1; // Leader is 0, followers start at 1
+ follower.followTarget = index === 0 ? this : this.convoyFollowers[index-1];
});
- } else if (this.convoyPosition >= 0) {
- // Update following behavior
- if (this.followTarget && this.followTarget.crashed) {
- this.convoyPosition = -1;
- this.convoyLeader = null;
- this.followTarget = null;
- }
+ } else if (this.convoyLeader && (this.convoyLeader.crashed || !this.convoyLeader.convoyFollowers.includes(this))) {
+ // If leader is gone or no longer recognizes this car as follower
+ this.convoyLeader = null;
+ this.followTarget = null;
+ this.convoyPosition = -1;
}
}
getEnhancedInputs() {
return [
- ...this.sensors, // 16 obstacle sensors
- ...this.roadSensors, // 8 road/navigation sensors
- ...this.trafficSensors, // 4 traffic behavior sensors
+ ...this.sensors, // 16 obstacle sensors
+ ...this.roadSensors, // 8 road/navigation sensors
+ ...this.trafficSensors // 4 traffic behavior sensors
];
}
update(deltaTime) {
- try {
- // Handle parked cars separately
- if (this.isParked) {
- this.handleParkedBehavior(deltaTime);
- return;
- }
-
- if (this.crashed) return;
-
- this.timeAlive -= deltaTime;
- if (this.timeAlive <= 0 || this.parkingAttempts >= this.maxParkingAttempts) {
- if (!this.isParkingApproach) {
- this.attemptParking();
- }
- return;
- }
-
- this.updateSensors();
- this.updateConvoyBehavior();
+ if (this.crashed) return;
+
+ // Handle manual control if active
+ if (this === manuallyControlledCar && cameraMode === 'follow_best') {
+ this.applyManualControls(deltaTime);
+ // Common updates even for manually controlled car
this.updateVisuals();
-
- // Get AI decision
- const inputs = this.getEnhancedInputs();
- const outputs = this.brain.activate(inputs);
-
- // Apply traffic-aware movement
- this.applyTrafficMovement(outputs, deltaTime);
- this.updateFitness(deltaTime);
-
- this.lastPosition.copy(this.mesh.position);
- this.checkCollisions();
+ this.checkCollisions(); // Still check for collisions
this.keepInBounds();
- } catch (error) {
- console.warn('Error in car update:', error);
- // Try to recover by resetting car state
- this.recoverFromError();
+ this.lastPosition.copy(this.mesh.position);
+ return; // Skip AI decision if manually controlled
+ }
+
+ // Handle parked cars separately
+ if (this.isParked) {
+ this.handleParkedBehavior(deltaTime);
+ this.updateVisuals(); // Keep visuals updated even when parked
+ return;
+ }
+
+ this.timeAlive -= deltaTime;
+ if (this.timeAlive <= 0 && !this.isParkingApproach && this.parkingAttempts < this.maxParkingAttempts) {
+ this.attemptParking(); // Try to park if lifespan is up
+ }
+
+ this.updateSensors();
+ this.updateConvoyBehavior();
+ this.updateVisuals();
+
+ const inputs = this.getEnhancedInputs();
+ const outputs = this.brain.activate(inputs);
+
+ this.applyTrafficMovement(outputs, deltaTime);
+ this.updateFitness(deltaTime);
+
+ this.lastPosition.copy(this.mesh.position);
+ this.checkCollisions();
+ this.keepInBounds();
+
+ // Grass behavior
+ const currentRoadPosScore = this.getRoadPositionScore();
+ if (currentRoadPosScore < GRASS_THRESHOLD && !this.isReturningToRoad && !this.isParkingApproach && !this.isInApproachLane && !this.isInExitLane) {
+ this.isReturningToRoad = true;
+ this.turnAngleGoal = Math.PI; // 180 degrees
+ this.turnProgress = 0;
+ this.initialOrientationY = this.mesh.rotation.y;
+ }
+ this.lastRoadPositionScore = currentRoadPosScore;
+ }
+
+ applyManualControls(deltaTime) {
+ const moveSpeed = 20.0;
+ const turnSpeed = 1.5;
+
+ if (manualControls.W) {
+ this.velocity.add(new THREE.Vector3(0,0,1).applyQuaternion(this.mesh.quaternion).multiplyScalar(moveSpeed * deltaTime));
+ }
+ if (manualControls.S) {
+ this.velocity.sub(new THREE.Vector3(0,0,1).applyQuaternion(this.mesh.quaternion).multiplyScalar(moveSpeed * 0.7 * deltaTime));
+ }
+ if (!manualControls.W && !manualControls.S) {
+ this.velocity.multiplyScalar(0.95); // Friction
+ }
+
+ if (manualControls.A) {
+ this.mesh.rotation.y += turnSpeed * deltaTime;
+ }
+ if (manualControls.D) {
+ this.mesh.rotation.y -= turnSpeed * deltaTime;
}
+
+ // Clamp speed
+ const currentSpeed = this.velocity.length();
+ if (currentSpeed > this.maxSpeed) {
+ this.velocity.normalize().multiplyScalar(this.maxSpeed);
+ }
+
+ this.mesh.position.add(this.velocity.clone().multiplyScalar(deltaTime));
+ this.wheels.forEach(wheel => wheel.rotation.x += currentSpeed * deltaTime * 0.1);
}
- recoverFromError() {
- try {
- // Reset to basic driving state
- this.role = 'driver';
- this.isParked = false;
- this.isParkingApproach = false;
- this.isInApproachLane = false;
- this.isInExitLane = false;
- this.parkingQueue = -1;
- this.convoyPosition = -1;
- this.convoyLeader = null;
- this.followTarget = null;
-
- // Set safe velocity
- if (this.velocity.length() < 1) {
- this.velocity.set(0, 0, 5);
+ handleParkedBehavior(deltaTime) {
+ this.velocity.set(0,0,0); // Ensure car is stationary
+ this.departureTime -= deltaTime;
+
+ if (this.departureTime <= 0 && !this.isExitingParking) {
+ this.isExitingParking = true;
+ if (this.targetParkingLot && this.targetParkingLot.building) {
+ this.targetParkingLot.building.visitorCount = Math.max(0, (this.targetParkingLot.building.visitorCount || 0) - 1);
+ }
+ this.turnAngleGoal = Math.PI; // 180 degrees to exit
+ this.turnProgress = 0;
+ this.initialOrientationY = this.mesh.rotation.y; // Store orientation before turning
+ }
+
+ if (this.isExitingParking) {
+ const turnSpeedForExit = Math.PI / 2; // Turn 180 in 2 seconds
+ this.mesh.rotation.y += turnSpeedForExit * deltaTime;
+ this.turnProgress += turnSpeedForExit * deltaTime;
+
+ if (this.turnProgress >= this.turnAngleGoal) {
+ this.mesh.rotation.y = this.initialOrientationY + Math.PI; // Ensure exact 180 turn
+ this.isExitingParking = false;
+ this.leaveParking(); // This will set it to use an exit lane
}
-
- // Reset time
- this.timeAlive = 30 + Math.random() * 20;
- } catch (recoveryError) {
- console.warn('Error during recovery:', recoveryError);
- this.crashed = true;
}
}
-
+
applyTrafficMovement(outputs, deltaTime) {
const [
acceleration, braking, steerLeft, steerRight,
- laneChange, followConvoy, parkingManeuver, turnSignalLeft,
- turnSignalRight, emergencyStop
+ laneChangeIntent, followConvoySignal, parkingManeuverSignal,
+ turnSignalLeftOutput, turnSignalRightOutput, emergencyStopSignal
] = outputs;
-
+
// Update turn signals
this.turnSignal = 'none';
- if (turnSignalLeft > 0.7) this.turnSignal = 'left';
- if (turnSignalRight > 0.7) this.turnSignal = 'right';
-
- this.leftSignal.material.opacity = this.turnSignal === 'left' ? 1.0 : 0.3;
- this.rightSignal.material.opacity = this.turnSignal === 'right' ? 1.0 : 0.3;
-
- // Emergency stop
- if (emergencyStop > 0.8) {
- this.velocity.multiplyScalar(0.7);
- return;
+ if (turnSignalLeftOutput > 0.7) this.turnSignal = 'left';
+ if (turnSignalRightOutput > 0.7) this.turnSignal = 'right';
+ this.leftSignal.material.opacity = this.turnSignal === 'left' ? (Math.sin(Date.now()*0.01) * 0.4 + 0.6) : 0.3;
+ this.rightSignal.material.opacity = this.turnSignal === 'right' ? (Math.sin(Date.now()*0.01) * 0.4 + 0.6) : 0.3;
+
+ if (this.isReturningToRoad) {
+ const turnSpeedReturn = Math.PI / 1.5; // Faster turn for recovery
+ this.mesh.rotation.y += turnSpeedReturn * deltaTime;
+ this.turnProgress += turnSpeedReturn * deltaTime;
+ this.velocity.copy(new THREE.Vector3(0,0,1).applyQuaternion(this.mesh.quaternion).multiplyScalar(this.minSpeed * 0.8)); // Move slowly while turning
+ if (this.turnProgress >= this.turnAngleGoal) {
+ this.mesh.rotation.y = this.initialOrientationY + Math.PI; // Ensure exact turn
+ this.isReturningToRoad = false;
+ }
+ this.mesh.position.add(this.velocity.clone().multiplyScalar(deltaTime));
+ return; // Override other movements while returning to road
}
-
- // Parking maneuver
- if (parkingManeuver > 0.8 && this.targetParkingLot) {
- this.executeParking(deltaTime);
- return;
+
+ if (emergencyStopSignal > 0.8) {
+ this.velocity.multiplyScalar(0.7); return;
+ }
+ if (parkingManeuverSignal > 0.7 && !this.isParked && !this.isParkingApproach) {
+ this.attemptParking(); return;
+ }
+ if (this.isParkingApproach || this.isInApproachLane || this.isInExitLane) {
+ this.executeParkingLogic(deltaTime); return; // Dedicated parking movement
}
- // Road-following behavior
- this.followRoad(deltaTime);
+ this.followRoad(deltaTime, laneChangeIntent); // Pass laneChangeIntent
- // Convoy following
- if (followConvoy > 0.6 && this.followTarget) {
+ if (followConvoySignal > 0.6 && this.followTarget) {
this.followConvoyTarget(deltaTime);
}
- // Basic movement
- const forward = new THREE.Vector3(0, 0, 1);
- forward.applyQuaternion(this.mesh.quaternion);
-
- // Acceleration and braking
+ // Basic movement based on NN outputs
+ const forward = new THREE.Vector3(0, 0, 1).applyQuaternion(this.mesh.quaternion);
if (acceleration > 0.3) {
- this.velocity.add(forward.multiplyScalar(acceleration * 8 * deltaTime));
+ this.velocity.add(forward.multiplyScalar(acceleration * 10 * deltaTime));
}
-
if (braking > 0.5) {
- this.velocity.multiplyScalar(1 - braking * deltaTime * 3);
+ this.velocity.multiplyScalar(1 - braking * deltaTime * 4);
}
- // Steering
- const steering = (steerRight - steerLeft) * 0.08 * deltaTime;
+ const steering = (steerRight - steerLeft) * 0.10 * deltaTime * (this.velocity.length()/this.maxSpeed + 0.2); // Speed sensitive steering
this.mesh.rotation.y += steering;
- // Speed limits
+ // Speed limits and friction
const currentSpeed = this.velocity.length();
- if (currentSpeed > this.maxSpeed) {
- this.velocity.normalize().multiplyScalar(this.maxSpeed);
- } else if (currentSpeed < this.minSpeed && currentSpeed > 0.1) {
- this.velocity.normalize().multiplyScalar(this.minSpeed);
- }
-
- // Apply movement
+ if (currentSpeed > this.maxSpeed) this.velocity.normalize().multiplyScalar(this.maxSpeed);
+ else if (currentSpeed < this.minSpeed && currentSpeed > 0.1) this.velocity.normalize().multiplyScalar(this.minSpeed);
+ else if (currentSpeed < 0.1) this.velocity.set(0,0,0);
+ this.velocity.multiplyScalar(0.99); // General friction
+
this.mesh.position.add(this.velocity.clone().multiplyScalar(deltaTime));
-
- // Wheel rotation
- this.wheels.forEach(wheel => {
- wheel.rotation.x += currentSpeed * deltaTime * 0.1;
- });
+ this.wheels.forEach(wheel => wheel.rotation.x += currentSpeed * deltaTime * 0.1);
}
- followRoad(deltaTime) {
+ followRoad(deltaTime, laneChangeIntent) {
const roadInfo = this.findNearestRoad();
- const currentRoadPos = this.getRoadPosition();
-
- // Check if transitioning from grass to road
- if (currentRoadPos > 0.3 && this.lastRoadPosition <= 0.3) {
- // Just touched road after being on grass - slow down
- this.velocity.multiplyScalar(0.7);
- this.isTransitioningToRoad = true;
- this.roadTransitionTime = 2.0; // 2 seconds to stabilize
+ if (!roadInfo || !roadInfo.roadObject) {
+ // If completely off-road, increase penalty or trigger recovery
+ this.fitness -= 2 * deltaTime; // Penalty for being off-road
+ return;
+ }
+
+ const road = roadInfo.roadObject;
+ const targetRoadAngle = road.orientationAngle;
+ let carAngle = this.mesh.rotation.y;
+
+ // Normalize carAngle to be in similar range as targetRoadAngle (0 to 2PI or -PI to PI)
+ // Assuming targetRoadAngle is between -PI and PI from atan2
+ while (carAngle - targetRoadAngle > Math.PI) carAngle -= 2 * Math.PI;
+ while (targetRoadAngle - carAngle > Math.PI) carAngle += 2 * Math.PI;
+
+ let angleDiff = targetRoadAngle - carAngle;
+ // Correct smallest angle
+ if (angleDiff > Math.PI) angleDiff -= 2 * Math.PI;
+ if (angleDiff < -Math.PI) angleDiff += 2 * Math.PI;
+
+ // Steering correction to align with road
+ this.mesh.rotation.y += angleDiff * 0.1 * this.brain.trafficTraits.laneKeeping;
+
+ // Lane keeping: Aim for a specific lane within the road width
+ const numLanes = Math.max(1, Math.floor(road.width / ROAD_WIDTH_UNIT));
+ let targetLaneIndex = Math.floor(numLanes / 2); // Default to center-ish lane
+
+ // Interpret laneChangeIntent (0-1) - simplified
+ if (numLanes > 1) {
+ if (laneChangeIntent < 0.33) targetLaneIndex = Math.max(0, targetLaneIndex -1 ); // Try to move left
+ else if (laneChangeIntent > 0.66) targetLaneIndex = Math.min(numLanes - 1, targetLaneIndex + 1); // Try to move right
}
- if (this.roadTransitionTime > 0) {
- this.roadTransitionTime -= deltaTime;
- // Maintain slower speed during transition
- const currentSpeed = this.velocity.length();
- if (currentSpeed > this.maxSpeed * 0.6) {
- this.velocity.normalize().multiplyScalar(this.maxSpeed * 0.6);
+ // Calculate the center of the target lane
+ let targetLaneCenterCoord; // This will be an X or Z coordinate
+ const laneCenterOffsetFromRoadEdge = (targetLaneIndex + 0.5) * ROAD_WIDTH_UNIT;
+
+ if (road.direction === 'horizontal') {
+ // For horizontal roads, lanes are offset in Z from the road's Z center.
+ // Road center is road.z. Road edge is road.z - road.width/2.
+ targetLaneCenterCoord = (road.z - road.width/2) + laneCenterOffsetFromRoadEdge;
+ const currentZ = this.mesh.position.z;
+ const offsetFromTargetLane = currentZ - targetLaneCenterCoord;
+ this.velocity.z -= offsetFromTargetLane * 0.2 * this.brain.trafficTraits.laneKeeping * deltaTime;
+ if (Math.abs(offsetFromTargetLane) > ROAD_WIDTH_UNIT / 2) { // Outside target lane
+ this.trafficViolations++; laneViolations++;
+ }
+ } else { // Vertical road
+ // For vertical roads, lanes are offset in X from the road's X center.
+ targetLaneCenterCoord = (road.x - road.width/2) + laneCenterOffsetFromRoadEdge;
+ const currentX = this.mesh.position.x;
+ const offsetFromTargetLane = currentX - targetLaneCenterCoord;
+ this.velocity.x -= offsetFromTargetLane * 0.2 * this.brain.trafficTraits.laneKeeping * deltaTime;
+ if (Math.abs(offsetFromTargetLane) > ROAD_WIDTH_UNIT / 2) {
+ this.trafficViolations++; laneViolations++;
}
}
-
- this.lastRoadPosition = currentRoadPos;
-
- if (!roadInfo) return;
-
- // Enhanced lane-keeping force
- const laneKeepingForce = this.brain.trafficTraits.laneKeeping;
- let targetDirection = roadInfo.direction;
-
- // Adjust for lane position with stronger correction
- if (roadInfo.lane === 'horizontal') {
- const laneOffset = this.mesh.position.z - roadInfo.center;
- if (Math.abs(laneOffset) > LANE_WIDTH / 4) {
- const correction = -laneOffset * 0.05 * laneKeepingForce;
- this.velocity.z += correction;
- if (Math.abs(laneOffset) > LANE_WIDTH / 2) {
- this.trafficViolations++;
- laneViolations++;
- }
- }
- } else {
- const laneOffset = this.mesh.position.x - roadInfo.center;
- if (Math.abs(laneOffset) > LANE_WIDTH / 4) {
- const correction = -laneOffset * 0.05 * laneKeepingForce;
- this.velocity.x += correction;
- if (Math.abs(laneOffset) > LANE_WIDTH / 2) {
- this.trafficViolations++;
- laneViolations++;
- }
- }
- }
-
- // Stronger direction alignment for road following
- const currentDirection = Math.atan2(this.velocity.x, this.velocity.z);
- let angleDiff = targetDirection - currentDirection;
- while (angleDiff > Math.PI) angleDiff -= 2 * Math.PI;
- while (angleDiff < -Math.PI) angleDiff += 2 * Math.PI;
-
- if (Math.abs(angleDiff) > 0.1) {
- this.mesh.rotation.y += angleDiff * 0.08 * laneKeepingForce;
- }
-
+
this.roadTime += deltaTime;
- this.laneDiscipline = Math.max(0, 1 - this.trafficViolations * 0.1);
+ this.laneDiscipline = Math.max(0, 1 - (this.trafficViolations / (this.roadTime + 1)) * 0.1);
}
-
+
followConvoyTarget(deltaTime) {
- if (!this.followTarget) return;
+ if (!this.followTarget || this.followTarget.crashed) {
+ this.convoyLeader = null; this.followTarget = null; this.convoyPosition = -1; return;
+ }
const targetPos = this.followTarget.mesh.position;
const distance = this.mesh.position.distanceTo(targetPos);
- const idealDistance = FOLLOW_DISTANCE + (this.convoyPosition * 2);
+ const idealDistance = FOLLOW_DISTANCE + (this.convoyPosition * 2.5); // Staggered formation
- if (distance > idealDistance + 3) {
- // Too far - speed up
- const catchUpForce = this.followTarget.velocity.clone().multiplyScalar(0.3);
- this.velocity.add(catchUpForce.multiplyScalar(deltaTime));
- } else if (distance < idealDistance - 2) {
- // Too close - slow down
- this.velocity.multiplyScalar(0.95);
- }
-
- // Align with target's direction
- const targetDirection = Math.atan2(this.followTarget.velocity.x, this.followTarget.velocity.z);
- const currentDirection = Math.atan2(this.velocity.x, this.velocity.z);
- let angleDiff = targetDirection - currentDirection;
-
- while (angleDiff > Math.PI) angleDiff -= 2 * Math.PI;
- while (angleDiff < -Math.PI) angleDiff += 2 * Math.PI;
+ const directionToTarget = targetPos.clone().sub(this.mesh.position).normalize();
- this.mesh.rotation.y += angleDiff * 0.1;
+ if (distance > idealDistance + 2) { // Too far, speed up
+ this.velocity.add(directionToTarget.multiplyScalar(this.brain.trafficTraits.followingBehavior * 5 * deltaTime));
+ } else if (distance < idealDistance - 1) { // Too close, slow down
+ this.velocity.multiplyScalar(1 - (1 - this.brain.trafficTraits.followingBehavior) * 0.5 * deltaTime);
+ }
+
+ // Align with target's general direction (simplified)
+ const targetAngle = Math.atan2(directionToTarget.x, directionToTarget.z);
+ let carAngle = this.mesh.rotation.y;
+ while (carAngle - targetAngle > Math.PI) carAngle -= 2 * Math.PI;
+ while (targetAngle - carAngle > Math.PI) carAngle += 2 * Math.PI;
+ this.mesh.rotation.y += (targetAngle - carAngle) * 0.05;
this.convoyTime += deltaTime;
this.followingDistance = distance;
}
- executeParking(deltaTime) {
- if (!this.targetParkingLot) {
- this.findNearestParkingLot();
- return;
- }
-
- // Enhanced parking state machine
- if (this.parkingQueue === -1) {
- this.joinParkingQueue();
- }
-
- if (this.isInApproachLane) {
- this.handleApproachLane(deltaTime);
- } else if (this.canProceedToApproach()) {
- this.enterApproachLane(deltaTime);
- } else {
- this.waitForApproachAccess(deltaTime);
- }
- }
-
- joinParkingQueue() {
- if (!this.targetParkingLot) return;
-
- if (!this.targetParkingLot.queue) {
- this.targetParkingLot.queue = [];
- }
-
- if (!this.targetParkingLot.queue.includes(this)) {
- this.targetParkingLot.queue.push(this);
- this.parkingQueue = this.targetParkingLot.queue.length - 1;
+ executeParkingLogic(deltaTime) {
+ // This is the state machine for parking
+ if (!this.targetParkingLot) { this.isParkingApproach = false; return; }
+
+ if (this.isInExitLane) {
+ this.handleExitLane(deltaTime);
+ } else if (this.isInApproachLane) {
+ this.handleApproachLaneMovement(deltaTime);
+ } else if (this.isParkingApproach) { // Moving towards an approach lane or spot
+ this.moveTowardsParkingEntry(deltaTime);
}
}
-
- canProceedToApproach() {
- if (!this.targetParkingLot || !this.targetParkingLot.queue) return false;
-
- const queuePosition = this.targetParkingLot.queue.indexOf(this);
-
- // Check if there's space in any approach lane
- let totalApproachCapacity = 0;
- let currentApproachOccupancy = 0;
-
- if (this.targetParkingLot.approachLanes) {
- // Multiple lanes system
- this.targetParkingLot.approachLanes.forEach(lane => {
- totalApproachCapacity += lane.length;
- });
-
- currentApproachOccupancy = population.filter(car =>
- car.isInApproachLane &&
- car.targetParkingLot === this.targetParkingLot
- ).length;
- } else {
- // Single lane fallback
- totalApproachCapacity = this.targetParkingLot.approachLane ?
- this.targetParkingLot.approachLane.length : 10;
-
- currentApproachOccupancy = population.filter(car =>
- car.isInApproachLane &&
- car.targetParkingLot === this.targetParkingLot
- ).length;
+
+ moveTowardsParkingEntry(deltaTime) {
+ // Try to enter an approach lane first
+ if (!this.targetParkingLot.approachLanes || this.targetParkingLot.approachLanes.length === 0) {
+ this.isParkingApproach = false; return; // No approach lanes defined
}
-
- return queuePosition < 5 && currentApproachOccupancy < totalApproachCapacity;
- }
-
- enterApproachLane(deltaTime) {
- // Find first available approach lane position from any lane
- let targetPosition = null;
- let selectedLane = -1;
-
- if (this.targetParkingLot.approachLanes) {
- for (let laneIndex = 0; laneIndex < this.targetParkingLot.approachLanes.length; laneIndex++) {
- const lanePositions = this.targetParkingLot.approachLanes[laneIndex];
-
- for (let posIndex = 0; posIndex < lanePositions.length; posIndex++) {
- const pos = lanePositions[posIndex];
- const occupied = population.some(car =>
- car !== this &&
- car.isInApproachLane &&
- car.mesh.position.distanceTo(pos) < 4
- );
-
- if (!occupied) {
- targetPosition = pos;
- selectedLane = laneIndex;
- break;
- }
- }
-
- if (targetPosition) break;
- }
- } else {
- // Fallback to single lane system for compatibility
- const approachPositions = this.targetParkingLot.approachLane || [];
- for (let i = 0; i < approachPositions.length; i++) {
- const pos = approachPositions[i];
- const occupied = population.some(car =>
- car !== this &&
- car.isInApproachLane &&
- car.mesh.position.distanceTo(pos) < 4
- );
-
- if (!occupied) {
- targetPosition = pos;
- break;
+
+ // Find the best (e.g. least occupied or closest) approach lane entry point
+ let bestLaneEntry = null;
+ let minOccupancy = Infinity; // Or some other metric like distance
+ let selectedLaneIdx = -1;
+
+ this.targetParkingLot.approachLanes.forEach((laneQueuePositions, idx) => {
+ // Simplified: pick the first available slot in any lane's queue start
+ // A more complex logic would check occupancy or distance.
+ if (laneQueuePositions.length > 0) {
+ // Check if first spot in this lane queue is free enough
+ const entryPoint = laneQueuePositions[0];
+ const occupied = population.some(car => car !== this && car.isInApproachLane && car.selectedApproachLaneIndex === idx && car.mesh.position.distanceTo(entryPoint) < 5);
+ if (!occupied && !bestLaneEntry) { // Simple: take first available
+ bestLaneEntry = entryPoint;
+ selectedLaneIdx = idx;
}
}
- }
-
- if (targetPosition) {
- this.isInApproachLane = true;
- this.approachTarget = targetPosition;
- this.selectedApproachLane = selectedLane;
- this.moveToPosition(targetPosition, deltaTime, 4); // Slow approach
- }
- }
-
- useExitLane() {
- let exitTarget = null;
- let selectedExitLane = -1;
+ });
- // Try multiple exit lanes if available
- if (this.targetParkingLot.exitLanes) {
- for (let laneIndex = 0; laneIndex < this.targetParkingLot.exitLanes.length; laneIndex++) {
- const exitPositions = this.targetParkingLot.exitLanes[laneIndex];
-
- for (let posIndex = 0; posIndex < exitPositions.length; posIndex++) {
- const pos = exitPositions[posIndex];
- const occupied = population.some(car =>
- car !== this &&
- car.mesh.position.distanceTo(pos) < 4
- );
-
- if (!occupied) {
- exitTarget = pos;
- selectedExitLane = laneIndex;
- break;
- }
- }
-
- if (exitTarget) break;
+ if (bestLaneEntry) {
+ this.approachTargetPosition = bestLaneEntry;
+ this.selectedApproachLaneIndex = selectedLaneIdx;
+ this.moveToPosition(this.approachTargetPosition, deltaTime, 3); // Slow approach speed
+ if (this.mesh.position.distanceTo(this.approachTargetPosition) < 2) {
+ this.isInApproachLane = true; // Entered the approach lane
+ this.isParkingApproach = false; // No longer just "approaching", now "in lane"
+ // Add to parking lot's internal queue for this lane if it has one
}
} else {
- // Fallback to single lane system
- const exitPositions = this.targetParkingLot.exitLane || [];
- for (let i = 0; i < exitPositions.length; i++) {
- const pos = exitPositions[i];
- const occupied = population.some(car =>
- car !== this &&
- car.mesh.position.distanceTo(pos) < 4
- );
-
- if (!occupied) {
- exitTarget = pos;
- break;
- }
- }
- }
-
- if (exitTarget) {
- this.isInExitLane = true;
- this.exitTarget = exitTarget;
- this.selectedExitLane = selectedExitLane;
- this.mesh.position.copy(exitTarget);
-
- // Set exit velocity toward nearest road or access point
- const exitDirection = this.getBestExitDirection();
- this.velocity.copy(exitDirection.multiplyScalar(7));
-
- // Schedule exit lane departure
- setTimeout(() => {
- if (this.isInExitLane) {
- this.isInExitLane = false;
- this.role = 'driver';
- this.timeAlive = 50 + Math.random() * 30;
- this.updateCarColor();
- }
- }, 2000 + Math.random() * 2000); // 2-4 seconds variation
-
- return true;
- }
-
- return false;
- }
-
- getBestExitDirection() {
- // Choose exit direction based on available access points
- if (this.targetParkingLot.accessPoints && this.targetParkingLot.accessPoints.length > 0) {
- const accessPoint = this.targetParkingLot.accessPoints[
- Math.floor(Math.random() * this.targetParkingLot.accessPoints.length)
- ];
-
- const direction = accessPoint.pos.clone().sub(this.mesh.position).normalize();
- return direction;
+ // All approach lanes seem full or no entry point found, wait or give up
+ this.velocity.multiplyScalar(0.9); // Slow down if can't find entry
+ this.parkingAttempts++;
+ if(this.parkingAttempts >= this.maxParkingAttempts) this.isParkingApproach = false;
}
-
- // Fallback directions
- const directions = [
- new THREE.Vector3(0, 0, 1), // South
- new THREE.Vector3(0, 0, -1), // North
- new THREE.Vector3(1, 0, 0), // East
- new THREE.Vector3(-1, 0, 0) // West
- ];
-
- return directions[Math.floor(Math.random() * directions.length)];
}
-
- handleApproachLane(deltaTime) {
- try {
- // Check if we can proceed to actual parking
- if (!this.targetParkingLot || !this.targetParkingLot.spots) {
- this.isInApproachLane = false;
- this.role = 'driver';
- return;
- }
-
- const availableSpot = this.targetParkingLot.spots.find(spot => !spot.occupied);
- if (!availableSpot) {
- // Wait in approach lane
- this.velocity.multiplyScalar(0.95);
- return;
- }
-
- const spotDistance = this.mesh.position.distanceTo(availableSpot.position);
-
- if (spotDistance < 3) {
- // Successfully park
+
+ handleApproachLaneMovement(deltaTime) {
+ // Logic for moving within the approach lane and finding a spot
+ if (!this.targetParkingLot || !this.targetParkingLot.spots) {
+ this.isInApproachLane = false; return;
+ }
+ const availableSpot = this.targetParkingLot.spots.find(spot => !spot.occupied);
+ if (availableSpot) {
+ this.moveToPosition(availableSpot.position, deltaTime, 2); // Move to spot
+ if (this.mesh.position.distanceTo(availableSpot.position) < 1.5) {
this.completeParkingProcess(availableSpot);
- } else {
- // Move toward spot
- this.moveToPosition(availableSpot.position, deltaTime, 2);
}
- } catch (error) {
- console.warn('Error in handleApproachLane:', error);
- // Recover by leaving approach lane
- this.isInApproachLane = false;
- this.role = 'driver';
- this.timeAlive = 30;
+ } else {
+ // No spot, wait in approach lane (simplified: just slow down)
+ this.velocity.multiplyScalar(0.95);
+ // Potentially move along queue if implemented
}
}
@@ -1529,361 +1114,258 @@
spot.occupied = true;
spot.car = this;
this.mesh.position.copy(spot.position);
+ this.mesh.rotation.y = spot.orientation !== undefined ? spot.orientation : this.mesh.rotation.y; // Align with spot
this.velocity.set(0, 0, 0);
this.parkingScore += 100;
- this.leaveParkingQueue();
- this.isInApproachLane = false;
-
parkingEvents++;
-
- this.departureTime = 15 + Math.random() * 25;
+ this.isInApproachLane = false;
+ this.isParkingApproach = false;
+ this.departureTime = 15 + Math.random() * 5; // Park for 15-20 seconds
+ if (this.targetParkingLot && this.targetParkingLot.building) {
+ this.targetParkingLot.building.visitorCount = (this.targetParkingLot.building.visitorCount || 0) + 1;
+ }
this.updateCarColor();
}
-
- waitForApproachAccess(deltaTime) {
- // Line up end-to-end near parking lot
- const queuePosition = Math.min(this.parkingQueue, 5);
- const queueTarget = this.targetParkingLot.center.clone();
- queueTarget.add(new THREE.Vector3(-50, 0, -15 + queuePosition * 5)); // 5m spacing
-
- this.moveToPosition(queueTarget, deltaTime, 3);
- }
-
- moveToPosition(targetPos, deltaTime, speed) {
- const direction = targetPos.clone().sub(this.mesh.position);
- const distance = direction.length();
-
- if (distance > 1) {
- direction.normalize();
- this.velocity.copy(direction.multiplyScalar(speed));
- this.mesh.position.add(this.velocity.clone().multiplyScalar(deltaTime));
-
- // Align rotation with movement
- const targetAngle = Math.atan2(direction.x, direction.z);
- this.mesh.rotation.y = targetAngle;
- } else {
- this.velocity.multiplyScalar(0.9);
+
+ handleExitLane(deltaTime) {
+ if (!this.exitTargetPosition) { // Should have been set by leaveParking
+ this.isInExitLane = false;
+ this.role = 'driver';
+ this.timeAlive = epochTime * 0.5; // Give some time to drive away
+ return;
+ }
+ this.moveToPosition(this.exitTargetPosition, deltaTime, 4); // Move along exit lane
+ if (this.mesh.position.distanceTo(this.exitTargetPosition) < 2) {
+ // Reached end of exit lane segment, transition to road
+ this.isInExitLane = false;
+ this.role = 'driver';
+ this.timeAlive = epochTime * 0.7; // Replenish some time
+ this.initializeMovement(); // Re-orient and set velocity for road
+ this.updateCarColor();
}
}
-
+
leaveParking() {
- if (!this.isParked || !this.parkingSpot) return;
-
- // Free the parking spot
- this.parkingSpot.occupied = false;
- this.parkingSpot.car = null;
- this.parkingSpot = null;
+ if (!this.isParked && !this.isInExitLane) return; // Not parked or already exiting
+
+ if (this.parkingSpot) {
+ this.parkingSpot.occupied = false;
+ this.parkingSpot.car = null;
+ this.parkingSpot = null;
+ }
this.isParked = false;
- this.isInApproachLane = false;
- // Use exit lane for organized departure
- this.useExitLane();
- }
-
- useExitLane() {
- // Find exit lane position
- const exitPositions = this.targetParkingLot.exitLane;
- let exitTarget = null;
-
- for (let i = 0; i < exitPositions.length; i++) {
- const pos = exitPositions[i];
- const occupied = population.some(car =>
- car !== this &&
- car.mesh.position.distanceTo(pos) < 4
- );
-
- if (!occupied) {
- exitTarget = pos;
- break;
+ // Find an exit lane target
+ if (this.targetParkingLot && this.targetParkingLot.exitLanes && this.targetParkingLot.exitLanes.length > 0) {
+ // Simplified: pick first exit lane, last point as target
+ // A real system would pick closest/least congested
+ this.selectedExitLaneIndex = 0; // Or a smarter choice
+ const exitLanePoints = this.targetParkingLot.exitLanes[this.selectedExitLaneIndex];
+ if (exitLanePoints && exitLanePoints.length > 0) {
+ this.exitTargetPosition = exitLanePoints[exitLanePoints.length - 1]; // Target the end of the exit lane
+ this.isInExitLane = true;
+ this.isExitingParking = false; // Done with 180 turn
+ // Initial velocity towards exitTargetPosition will be handled by moveToPosition
+ } else {
+ this.role = 'driver'; // Fallback if exit lane is weird
+ this.initializeMovement();
}
+ } else {
+ this.role = 'driver'; // Fallback if no exit lanes
+ this.initializeMovement();
}
-
- if (exitTarget) {
- this.isInExitLane = true;
- this.exitTarget = exitTarget;
- this.mesh.position.copy(exitTarget);
-
- // Set exit velocity
- const exitDirection = new THREE.Vector3(0, 0, 1); // Move south to exit
- this.velocity.copy(exitDirection.multiplyScalar(6));
-
- // Schedule exit lane departure
- setTimeout(() => {
- this.isInExitLane = false;
- this.role = 'driver';
- this.timeAlive = 50 + Math.random() * 30;
- this.updateCarColor();
- }, 2000);
- }
+ this.updateCarColor();
}
attemptParking() {
+ if (this.isParked || this.isParkingApproach) return;
this.role = 'parker';
- this.findNearestParkingLot();
+ this.updateRole(); // Update indicator
+ this.findNearestParkingLotForAI(); // Sets this.targetParkingLot
if (!this.targetParkingLot) {
- // No parking available, become a wanderer
- this.timeAlive = 20;
- this.role = 'driver';
+ this.parkingAttempts++; // Failed to find a lot
+ this.role = 'driver'; this.updateRole();
+ this.timeAlive = epochTime * 0.2; // Try again sooner
return;
}
+ this.isParkingApproach = true; // Start the approach process
+ this.parkingAttempts = 0; // Reset attempts for this lot
+ }
+
+ findNearestParkingLotForAI() {
+ let closestLot = null;
+ let minDist = Infinity;
+ world.parkingLots.forEach(lot => {
+ const dist = this.mesh.position.distanceTo(lot.center);
+ if (dist < minDist) {
+ minDist = dist;
+ closestLot = lot;
+ }
+ });
+ this.targetParkingLot = closestLot;
+ }
+
+ moveToPosition(targetPos, deltaTime, speed) {
+ const direction = targetPos.clone().sub(this.mesh.position);
+ const distance = direction.length();
- // Start parking process
- this.isParkingApproach = true;
+ if (distance > 0.5) { // Threshold to stop jittering
+ direction.normalize();
+ this.velocity.copy(direction.multiplyScalar(speed));
+
+ // Smoothly turn towards target
+ const targetAngle = Math.atan2(direction.x, direction.z);
+ let currentAngle = this.mesh.rotation.y;
+ // Normalize angles to prevent full circle turns
+ while (targetAngle - currentAngle > Math.PI) currentAngle += 2 * Math.PI;
+ while (currentAngle - targetAngle > Math.PI) currentAngle -= 2 * Math.PI;
+ this.mesh.rotation.y += (targetAngle - currentAngle) * 0.2; // Adjust turn speed
+
+ this.mesh.position.add(this.velocity.clone().multiplyScalar(deltaTime));
+ } else {
+ this.velocity.set(0,0,0); // Reached target
+ }
}
updateFitness(deltaTime) {
const distance = this.mesh.position.distanceTo(this.lastPosition);
this.distanceTraveled += distance;
- // Multi-objective fitness
- const roadBonus = this.getRoadPosition() * distance * 5;
- const laneBonus = this.laneDiscipline * distance * 3;
- const convoyBonus = this.convoyTime * 2;
- const parkingBonus = this.parkingScore;
- const violationPenalty = this.trafficViolations * -10;
-
- this.fitness = this.distanceTraveled +
- roadBonus +
- laneBonus +
- convoyBonus +
- parkingBonus +
- violationPenalty;
+ let fitnessScore = this.distanceTraveled * 0.5; // Base score for moving
+ fitnessScore += this.roadTime * 1.5; // Bonus for staying on road
+ fitnessScore += this.convoyTime * 1.0; // Bonus for being in convoy
+ fitnessScore += this.parkingScore * 0.5; // Bonus for parking successfully
+ fitnessScore -= this.trafficViolations * 5; // Penalty for violations
+ if (this.getRoadPositionScore() < GRASS_THRESHOLD && !this.isReturningToRoad) {
+ fitnessScore -= 10 * deltaTime; // Heavy penalty for being on grass without trying to return
+ }
+ if (this.crashed) fitnessScore -= 500; // Large penalty for crashing
+
+ this.fitness = fitnessScore;
}
updateVisuals() {
this.updateCarColor();
this.updateFlockVisualization();
+ this.updateRole(); // Ensure role indicator is current
}
updateCarColor() {
- let hue = 0.6; // Default blue
- let saturation = 0.7;
- let lightness = 0.5;
-
- if (this.isParked) {
- hue = 0.3; // Green for parked
- lightness = 0.7;
- } else if (this.role === 'leader') {
- hue = 0.8; // Purple for leaders
- saturation = 1.0;
- lightness = 0.6;
- } else if (this.convoyPosition >= 0) {
- hue = 0.5; // Cyan for convoy members
- saturation = 0.8;
- lightness = 0.6;
- } else if (this.getRoadPosition() < 0.3) {
- hue = 0.1; // Orange for off-road
- saturation = 1.0;
- lightness = 0.5;
- }
-
- // Performance-based brightness
- const performanceBonus = Math.min(this.fitness / 500, 0.2);
- lightness += performanceBonus;
-
+ let hue = 0.6, saturation = 0.7, lightness = 0.5; // Default blue
+ if (this.isParked) { hue = 0.33; lightness = 0.7; } // Green
+ else if (this.role === 'leader') { hue = 0.83; saturation = 1.0; lightness = 0.6; } // Purple
+ else if (this.convoyPosition > 0) { hue = 0.5; saturation = 0.8; lightness = 0.6; } // Cyan
+ else if (this.getRoadPositionScore() < GRASS_THRESHOLD) { hue = 0.1; saturation = 1.0; } // Orange for off-road
+
+ const performanceBonus = Math.min(Math.max(0, this.fitness) / 1000, 0.2); // Brighter for higher fitness
+ lightness = Math.min(1, lightness + performanceBonus);
this.bodyMaterial.color.setHSL(hue, saturation, lightness);
}
updateFlockVisualization() {
- if (!showFlockLines) return;
-
- // Show convoy connections
- let connectionIndex = 0;
-
- // Leader to followers
- if (this.role === 'leader') {
- this.convoyFollowers.forEach(follower => {
- if (connectionIndex < this.flockLines.length) {
- const start = this.mesh.position.clone();
- start.y = 3;
- const end = follower.mesh.position.clone();
- end.y = 3;
-
- this.flockLines[connectionIndex].geometry.setFromPoints([start, end]);
- this.flockLines[connectionIndex].material.color.setHex(0xff00ff);
- this.flockLines[connectionIndex].visible = true;
- connectionIndex++;
+ // Manages lines connecting convoy members or nearby cars.
+ // Assumed from original, ensures lines are updated or hidden based on showFlockLines.
+ let lineIdx = 0;
+ if (showFlockLines) {
+ if (this.role === 'leader' && this.convoyFollowers) {
+ this.convoyFollowers.forEach(follower => {
+ if (lineIdx < this.flockLines.length && follower) {
+ this.flockLines[lineIdx].geometry.setFromPoints([this.mesh.position, follower.mesh.position]);
+ this.flockLines[lineIdx].material.color.setHex(0xff00ff); // Leader connections
+ this.flockLines[lineIdx].visible = true;
+ lineIdx++;
+ }
+ });
+ } else if (this.followTarget) {
+ if (lineIdx < this.flockLines.length) {
+ this.flockLines[lineIdx].geometry.setFromPoints([this.mesh.position, this.followTarget.mesh.position]);
+ this.flockLines[lineIdx].material.color.setHex(0x00ffff); // Follower connection
+ this.flockLines[lineIdx].visible = true;
+ lineIdx++;
}
- });
- }
-
- // Following connection
- if (this.followTarget && connectionIndex < this.flockLines.length) {
- const start = this.mesh.position.clone();
- start.y = 3;
- const end = this.followTarget.mesh.position.clone();
- end.y = 3;
-
- this.flockLines[connectionIndex].geometry.setFromPoints([start, end]);
- this.flockLines[connectionIndex].material.color.setHex(0x00ffff);
- this.flockLines[connectionIndex].visible = true;
- connectionIndex++;
- }
-
- // Neighbor connections
- this.neighbors.slice(0, 8 - connectionIndex).forEach(neighbor => {
- if (connectionIndex < this.flockLines.length) {
- const start = this.mesh.position.clone();
- start.y = 3;
- const end = neighbor.mesh.position.clone();
- end.y = 3;
-
- this.flockLines[connectionIndex].geometry.setFromPoints([start, end]);
- this.flockLines[connectionIndex].material.color.setHex(0x00ff00);
- this.flockLines[connectionIndex].visible = true;
- connectionIndex++;
}
- });
-
- // Hide unused lines
- for (let i = connectionIndex; i < this.flockLines.length; i++) {
- this.flockLines[i].visible = false;
+ }
+ for (let i = lineIdx; i < this.flockLines.length; i++) {
+ this.flockLines[i].visible = false; // Hide unused lines
}
}
getObstacles() {
+ // Returns an array of meshes that act as obstacles (other cars, buildings).
let obstacles = [];
-
population.forEach(car => {
- if (car !== this && !car.crashed) {
- obstacles.push(car.mesh);
- }
- });
-
- world.buildings.forEach(building => {
- obstacles.push(building.mesh);
+ if (car !== this && !car.crashed) obstacles.push(car.mesh);
});
-
+ world.buildings.forEach(buildingData => obstacles.push(buildingData.mesh));
return obstacles;
}
checkCollisions() {
+ if (this.crashed) return;
const carBox = new THREE.Box3().setFromObject(this.mesh);
- // Modified collision behavior for parking areas
- const isParkingMode = this.isInApproachLane || this.isInExitLane ||
- this.isParkingApproach || this.role === 'parker';
-
- // Soft collision with other cars
+ // Car-to-car collisions (soft)
population.forEach(otherCar => {
- if (otherCar !== this && !otherCar.crashed && !otherCar.isParked) {
+ if (otherCar !== this && !otherCar.crashed) {
const otherBox = new THREE.Box3().setFromObject(otherCar.mesh);
- const distance = this.mesh.position.distanceTo(otherCar.mesh.position);
-
- // Relaxed collision detection for parking behavior
- const minDistance = isParkingMode || otherCar.isInApproachLane ? 2.5 : 4.0;
- const bothInParkingMode = isParkingMode &&
- (otherCar.isInApproachLane || otherCar.isParkingApproach || otherCar.role === 'parker');
-
- if (carBox.intersectsBox(otherBox) || (distance < minDistance && !bothInParkingMode)) {
- // Gentle separation instead of hard collision
- const separation = new THREE.Vector3()
- .subVectors(this.mesh.position, otherCar.mesh.position)
- .normalize();
-
- if (isParkingMode) {
- // Very gentle separation for parking
- separation.multiplyScalar(1.5);
- this.velocity.add(separation.multiplyScalar(0.1));
- this.velocity.multiplyScalar(0.95); // Slow down
- } else {
- // Normal separation
- separation.multiplyScalar(3);
- this.velocity.add(separation.multiplyScalar(0.3));
- otherCar.velocity.sub(separation.multiplyScalar(0.3));
-
- this.fitness -= 10;
- otherCar.fitness -= 10;
- this.trafficViolations++;
+ if (carBox.intersectsBox(otherBox)) {
+ // Soft collision: push apart slightly, reduce fitness
+ const separationVector = this.mesh.position.clone().sub(otherCar.mesh.position).normalize().multiplyScalar(0.2);
+ this.mesh.position.add(separationVector);
+ otherCar.mesh.position.sub(separationVector);
+ this.velocity.multiplyScalar(0.8); otherCar.velocity.multiplyScalar(0.8);
+ this.fitness -= 5; otherCar.fitness -= 5;
+ this.trafficViolations++; otherCar.trafficViolations++;
+ // Minor chance of full crash from soft collision
+ if (Math.random() < 0.01 && !this.isParkingRelatedState() && !otherCar.isParkingRelatedState()) {
+ this.crashed = true; crashCount++;
}
}
}
});
- // Building collisions (unchanged)
- world.buildings.forEach(building => {
- const buildingBox = new THREE.Box3().setFromObject(building.mesh);
+ // Car-to-building collisions (hard)
+ world.buildings.forEach(buildingData => {
+ const buildingBox = new THREE.Box3().setFromObject(buildingData.mesh);
if (carBox.intersectsBox(buildingBox)) {
- this.crashed = true;
- crashCount++;
+ this.crashed = true; crashCount++;
}
});
}
+
+ isParkingRelatedState() {
+ return this.isParked || this.isParkingApproach || this.isInApproachLane || this.isInExitLane;
+ }
keepInBounds() {
- const bounds = 400;
- 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.6;
- }
- if (Math.abs(this.mesh.position.z) > bounds) {
- this.mesh.position.z = Math.sign(this.mesh.position.z) * bounds;
- this.velocity.z *= -0.6;
- }
- this.fitness -= 10;
+ const bounds = 400; // World boundary
+ if (Math.abs(this.mesh.position.x) > bounds || Math.abs(this.mesh.position.z) > bounds) {
+ this.mesh.position.x = Math.max(-bounds, Math.min(bounds, this.mesh.position.x));
+ this.mesh.position.z = Math.max(-bounds, Math.min(bounds, this.mesh.position.z));
+ this.velocity.multiplyScalar(-0.5); // Bounce back
+ this.fitness -= 20; // Penalty for hitting boundary
}
}
destroy() {
- try {
- // Clean up parking spot
- if (this.parkingSpot) {
- this.parkingSpot.occupied = false;
- this.parkingSpot.car = null;
- }
-
- // Remove from parking queue
- this.leaveParkingQueue();
-
- // Clean up convoy relationships
- if (this.convoyFollowers && this.convoyFollowers.length > 0) {
- this.convoyFollowers.forEach(follower => {
- if (follower) {
- follower.convoyLeader = null;
- follower.followTarget = null;
- follower.convoyPosition = -1;
- }
- });
- }
-
- if (this.convoyLeader) {
- const index = this.convoyLeader.convoyFollowers.indexOf(this);
- if (index !== -1) {
- this.convoyLeader.convoyFollowers.splice(index, 1);
- }
- }
-
- // Clean up visual elements
- if (this.flockLines) {
- this.flockLines.forEach(line => {
- try {
- if (line && line.parent) {
- scene.remove(line);
- }
- } catch (error) {
- console.warn('Error removing flock line:', error);
- }
- });
- }
-
- // Remove mesh from scene
- if (this.mesh && this.mesh.parent) {
- scene.remove(this.mesh);
- }
- } catch (error) {
- console.warn('Error in car destroy method:', error);
+ // Clean up Three.js objects and any references
+ if (this.parkingSpot) {
+ this.parkingSpot.occupied = false; this.parkingSpot.car = null;
}
+ if (this.targetParkingLot && this.targetParkingLot.building && this.isParked) { // Only decrement if it was parked and is now destroyed
+ this.targetParkingLot.building.visitorCount = Math.max(0, (this.targetParkingLot.building.visitorCount || 0) - 1);
+ }
+
+ this.flockLines.forEach(line => { if (line.parent) scene.remove(line); });
+ if (this.mesh.parent) scene.remove(this.mesh);
}
}
function init() {
- // Enhanced scene setup
scene = new THREE.Scene();
- scene.background = new THREE.Color(0x87CEEB);
- scene.fog = new THREE.Fog(0x87CEEB, 300, 1000);
+ scene.background = new THREE.Color(0x87CEEB); // Sky blue
+ scene.fog = new THREE.Fog(0x87CEEB, 300, 1000); // Fog for depth effect
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 2000);
camera.position.set(0, 150, 150);
@@ -1892,18 +1374,23 @@
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
- renderer.shadowMap.type = THREE.PCFSoftShadowMap;
+ renderer.shadowMap.type = THREE.PCFSoftShadowMap; // Softer shadows
document.body.appendChild(renderer.domElement);
// Lighting
- const ambientLight = new THREE.AmbientLight(0x404040, 0.6);
+ const ambientLight = new THREE.AmbientLight(0x606060); // Increased ambient light
scene.add(ambientLight);
-
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
- directionalLight.position.set(100, 100, 50);
+ directionalLight.position.set(100, 150, 75); // Adjusted light angle
directionalLight.castShadow = true;
- directionalLight.shadow.mapSize.width = 2048;
+ directionalLight.shadow.mapSize.width = 2048; // Higher shadow resolution
directionalLight.shadow.mapSize.height = 2048;
+ directionalLight.shadow.camera.near = 50;
+ directionalLight.shadow.camera.far = 500;
+ directionalLight.shadow.camera.left = -200;
+ directionalLight.shadow.camera.right = 200;
+ directionalLight.shadow.camera.top = 200;
+ directionalLight.shadow.camera.bottom = -200;
scene.add(directionalLight);
createTrafficWorld();
@@ -1911,7 +1398,6 @@
clock = new THREE.Clock();
- // Event listeners
window.addEventListener('resize', onWindowResize);
setupEventListeners();
@@ -1919,928 +1405,418 @@
}
function createTrafficWorld() {
- // Enhanced ground with road texture hints
+ // Ground plane
const groundGeometry = new THREE.PlaneGeometry(1200, 1200);
- const groundMaterial = new THREE.MeshLambertMaterial({ color: 0x228B22 });
+ const groundMaterial = new THREE.MeshLambertMaterial({ color: 0x3c763d }); // Darker green
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2;
+ ground.position.y = 0; // Ground at y=0
ground.receiveShadow = true;
scene.add(ground);
- // Create comprehensive road network first
- createRoadNetwork();
-
- // Then create buildings with parking lots
- createBuildingsWithParkingLots();
+ createRoadNetwork(); // Roads first
+ createBuildingsWithParkingLots(); // Then buildings and their parking
}
- function createRoadNetwork() {
- const roadMaterial = new THREE.MeshLambertMaterial({ color: 0x444444 });
- const highwayMaterial = new THREE.MeshLambertMaterial({ color: 0x333333 });
- const lineMaterial = new THREE.MeshLambertMaterial({ color: 0xffffff });
- const yellowLineMaterial = new THREE.MeshLambertMaterial({ color: 0xffff00 });
-
- // Create comprehensive road network
-
- // 1. MAJOR HIGHWAYS (8 lanes) - Cross patterns
- createHighway(-400, 400, 0, 'horizontal', 48, highwayMaterial, yellowLineMaterial); // North highway
- createHighway(-400, 400, 0, 'vertical', 48, highwayMaterial, yellowLineMaterial); // Main highway
-
- // 2. SECONDARY HIGHWAYS (4 lanes) - Grid system
- for (let i = -300; i <= 300; i += 150) {
- if (i !== 0) { // Don't overlap main highway
- createHighway(-400, 400, i, 'horizontal', 24, roadMaterial, yellowLineMaterial);
- createHighway(-400, 400, i, 'vertical', 24, roadMaterial, yellowLineMaterial);
- }
- }
-
- // 3. LOCAL ROADS (2 lanes) - Connecting roads
- for (let i = -375; i <= 375; i += 75) {
- createLocalRoad(-400, 400, i, 'horizontal', 12, roadMaterial, lineMaterial);
- createLocalRoad(-400, 400, i, 'vertical', 12, roadMaterial, lineMaterial);
- }
-
- // 4. BUILDING ACCESS ROADS - Around all buildings
- createBuildingAccessRoads();
- }
+ function createRoad(x, z, width, length, type, orientationAngle, isHorizontal) {
+ const roadHeight = 0.1; // Roads slightly above ground
+ const roadMaterial = new THREE.MeshLambertMaterial({ color: type === 'highway' ? 0x333333 : 0x444444 });
+ const roadGeometry = new THREE.PlaneGeometry(isHorizontal ? length : width, isHorizontal ? width : length);
+ const roadMesh = new THREE.Mesh(roadGeometry, roadMaterial);
+ roadMesh.rotation.x = -Math.PI / 2;
+ roadMesh.position.set(x, roadHeight, z);
+ roadMesh.receiveShadow = true;
+ scene.add(roadMesh);
- function createHighway(start, end, position, direction, width, material, linemat) {
- if (direction === 'horizontal') {
- // Main road surface
- const roadGeometry = new THREE.PlaneGeometry(end - start, width);
- const road = new THREE.Mesh(roadGeometry, material);
- road.rotation.x = -Math.PI / 2;
- road.position.set((start + end) / 2, 0.1, position);
- scene.add(road);
-
- // Lane dividers
- const laneCount = width / 6; // 6 units per lane
- for (let lane = 1; lane < laneCount; lane++) {
- const lineY = position - width/2 + (lane * 6);
-
- // Center divider (yellow)
- if (lane === Math.floor(laneCount / 2)) {
- createDashedLine(start, end, lineY, 'horizontal', linemat, true);
- } else {
- // Regular lane dividers (white)
- createDashedLine(start, end, lineY, 'horizontal', linemat, false);
- }
- }
- } else {
- // Vertical highway
- const roadGeometry = new THREE.PlaneGeometry(width, end - start);
- const road = new THREE.Mesh(roadGeometry, material);
- road.rotation.x = -Math.PI / 2;
- road.position.set(position, 0.1, (start + end) / 2);
- scene.add(road);
-
- // Lane dividers
- const laneCount = width / 6;
- for (let lane = 1; lane < laneCount; lane++) {
- const lineX = position - width/2 + (lane * 6);
-
- if (lane === Math.floor(laneCount / 2)) {
- createDashedLine(start, end, lineX, 'vertical', linemat, true);
- } else {
- createDashedLine(start, end, lineX, 'vertical', linemat, false);
- }
- }
- }
- }
+ const roadData = {
+ mesh: roadMesh,
+ x: x, z: z, // Center of the road segment
+ width: width, length: length,
+ type: type,
+ direction: isHorizontal ? 'horizontal' : 'vertical',
+ orientationAngle: orientationAngle, // Angle in radians
+ start: isHorizontal ? x - length/2 : z - length/2, // Start coord for segment bounds
+ end: isHorizontal ? x + length/2 : z + length/2, // End coord for segment bounds
+ lanes: [] // Store lane data if needed later
+ };
+ world.roads.push(roadData);
- function createLocalRoad(start, end, position, direction, width, material, linemat) {
- if (direction === 'horizontal') {
- const roadGeometry = new THREE.PlaneGeometry(end - start, width);
- const road = new THREE.Mesh(roadGeometry, material);
- road.rotation.x = -Math.PI / 2;
- road.position.set((start + end) / 2, 0.1, position);
- scene.add(road);
-
- // Center line
- createDashedLine(start, end, position, 'horizontal', linemat, false);
- } else {
- const roadGeometry = new THREE.PlaneGeometry(width, end - start);
- const road = new THREE.Mesh(roadGeometry, material);
- road.rotation.x = -Math.PI / 2;
- road.position.set(position, 0.1, (start + end) / 2);
- scene.add(road);
-
- // Center line
- createDashedLine(start, end, position, 'vertical', linemat, false);
- }
- }
+ // Lane markings
+ const numLanes = Math.max(1, Math.floor(width / ROAD_WIDTH_UNIT));
+ const actualLaneWidth = width / numLanes;
+ const lineMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff });
+ const yellowLineMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00 });
- function createDashedLine(start, end, position, direction, material, isDouble) {
- const dashLength = 8;
- const gapLength = 4;
- const totalLength = end - start;
- const segments = Math.floor(totalLength / (dashLength + gapLength));
-
- for (let i = 0; i < segments; i++) {
- const segmentStart = start + i * (dashLength + gapLength);
-
- if (direction === 'horizontal') {
- const lineGeometry = new THREE.PlaneGeometry(dashLength, 0.3);
- const line = new THREE.Mesh(lineGeometry, material);
- line.rotation.x = -Math.PI / 2;
- line.position.set(segmentStart + dashLength/2, 0.12, position);
- scene.add(line);
-
- // Double line for highway center
- if (isDouble) {
- const line2 = new THREE.Mesh(lineGeometry, material);
- line2.rotation.x = -Math.PI / 2;
- line2.position.set(segmentStart + dashLength/2, 0.12, position + 1);
- scene.add(line2);
- }
- } else {
- const lineGeometry = new THREE.PlaneGeometry(0.3, dashLength);
- const line = new THREE.Mesh(lineGeometry, material);
- line.rotation.x = -Math.PI / 2;
- line.position.set(position, 0.12, segmentStart + dashLength/2);
- scene.add(line);
-
- if (isDouble) {
- const line2 = new THREE.Mesh(lineGeometry, material);
- line2.rotation.x = -Math.PI / 2;
- line2.position.set(position + 1, 0.12, segmentStart + dashLength/2);
- scene.add(line2);
- }
+ for (let i = 0; i < numLanes; i++) {
+ // Calculate offset for this lane's center from the road's center line
+ const laneCenterOffset = (i - (numLanes - 1) / 2) * actualLaneWidth;
+
+ // Add dashed lines between lanes (if not the outermost edge)
+ if (i < numLanes - 1) {
+ let linePosX, linePosZ, lineWidth, lineHeight;
+ const lineOffsetFromLaneCenter = actualLaneWidth / 2; // Line is at the edge of the lane
+
+ if (isHorizontal) {
+ linePosX = x; // Centered with road segment
+ linePosZ = z + laneCenterOffset + lineOffsetFromLaneCenter;
+ lineWidth = length;
+ lineHeight = 0.2;
+ } else { // Vertical
+ linePosX = x + laneCenterOffset + lineOffsetFromLaneCenter;
+ linePosZ = z; // Centered with road segment
+ lineWidth = 0.2;
+ lineHeight = length;
+ }
+ // Use yellow for center divider on multi-lane roads (simplified: if it's near overall center)
+ const isCenterDivider = numLanes > 1 && Math.abs(laneCenterOffset + lineOffsetFromLaneCenter) < actualLaneWidth * 0.6;
+ createDashedLineWorld(linePosX, linePosZ, lineWidth, lineHeight, isHorizontal, isCenterDivider ? yellowLineMaterial : lineMaterial, roadHeight + 0.01);
}
}
}
+
+ function createDashedLineWorld(centerX, centerZ, totalLength, totalWidth, isHorizontal, material, yPos) {
+ const dashLength = 5;
+ const gapLength = 3;
+ const numDashes = Math.floor(totalLength / (dashLength + gapLength));
- function createBuildingAccessRoads() {
- const accessRoadMaterial = new THREE.MeshLambertMaterial({ color: 0x555555 });
- const lineMaterial = new THREE.MeshLambertMaterial({ color: 0xffffff });
-
- world.buildings.forEach((building, index) => {
- const pos = building.mesh.position;
- const buildingBox = new THREE.Box3().setFromObject(building.mesh);
- const size = buildingBox.getSize(new THREE.Vector3());
-
- // Create access roads around each building (rectangular loop)
- const roadWidth = 8;
- const buffer = 5; // Distance from building
-
- // North road
- createBuildingRoad(
- pos.x - size.x/2 - buffer - 10,
- pos.x + size.x/2 + buffer + 10,
- pos.z + size.z/2 + buffer + roadWidth/2,
- 'horizontal', roadWidth, accessRoadMaterial, lineMaterial
+ for (let j = 0; j < numDashes; j++) {
+ const dashGeometry = new THREE.PlaneGeometry(
+ isHorizontal ? dashLength : totalWidth,
+ isHorizontal ? totalWidth : dashLength
);
+ const dash = new THREE.Mesh(dashGeometry, material);
+ dash.rotation.x = -Math.PI / 2;
- // South road
- createBuildingRoad(
- pos.x - size.x/2 - buffer - 10,
- pos.x + size.x/2 + buffer + 10,
- pos.z - size.z/2 - buffer - roadWidth/2,
- 'horizontal', roadWidth, accessRoadMaterial, lineMaterial
- );
-
- // East road
- createBuildingRoad(
- pos.x + size.x/2 + buffer + roadWidth/2,
- pos.z - size.z/2 - buffer - 10,
- pos.z + size.z/2 + buffer + 10,
- 'vertical', roadWidth, accessRoadMaterial, lineMaterial
- );
-
- // West road
- createBuildingRoad(
- pos.x - size.x/2 - buffer - roadWidth/2,
- pos.z - size.z/2 - buffer - 10,
- pos.z + size.z/2 + buffer + 10,
- 'vertical', roadWidth, accessRoadMaterial, lineMaterial
- );
-
- // Connecting roads to main network
- createConnectorRoads(pos, size, roadWidth, accessRoadMaterial);
- });
- }
-
- function createBuildingRoad(startOrX, endOrStartZ, positionOrEndZ, direction, width, material, linemat) {
- if (direction === 'horizontal') {
- const roadGeometry = new THREE.PlaneGeometry(endOrStartZ - startOrX, width);
- const road = new THREE.Mesh(roadGeometry, material);
- road.rotation.x = -Math.PI / 2;
- road.position.set((startOrX + endOrStartZ) / 2, 0.08, positionOrEndZ);
- scene.add(road);
-
- // Center line
- const lineGeometry = new THREE.PlaneGeometry(endOrStartZ - startOrX, 0.2);
- const line = new THREE.Mesh(lineGeometry, linemat);
- line.rotation.x = -Math.PI / 2;
- line.position.set((startOrX + endOrStartZ) / 2, 0.1, positionOrEndZ);
- scene.add(line);
- } else {
- const roadGeometry = new THREE.PlaneGeometry(width, positionOrEndZ - endOrStartZ);
- const road = new THREE.Mesh(roadGeometry, material);
- road.rotation.x = -Math.PI / 2;
- road.position.set(startOrX, 0.08, (endOrStartZ + positionOrEndZ) / 2);
- scene.add(road);
-
- // Center line
- const lineGeometry = new THREE.PlaneGeometry(0.2, positionOrEndZ - endOrStartZ);
- const line = new THREE.Mesh(lineGeometry, linemat);
- line.rotation.x = -Math.PI / 2;
- line.position.set(startOrX, 0.1, (endOrStartZ + positionOrEndZ) / 2);
- scene.add(line);
+ const dashOffset = j * (dashLength + gapLength) - totalLength / 2 + dashLength / 2;
+ if (isHorizontal) {
+ dash.position.set(centerX + dashOffset, yPos, centerZ);
+ } else {
+ dash.position.set(centerX, yPos, centerZ + dashOffset);
+ }
+ scene.add(dash);
}
}
- function createConnectorRoads(buildingPos, buildingSize, roadWidth, material) {
- // Connect building access roads to nearest main roads
- const nearestMainRoads = findNearestMainRoads(buildingPos);
-
- nearestMainRoads.forEach(mainRoad => {
- // Create connector from building to main road
- const startPos = buildingPos.clone();
- const endPos = mainRoad.position.clone();
-
- // Create straight connector road
- const distance = startPos.distanceTo(endPos);
- const direction = endPos.clone().sub(startPos).normalize();
-
- const connectorGeometry = new THREE.PlaneGeometry(roadWidth, distance);
- const connector = new THREE.Mesh(connectorGeometry, material);
- connector.rotation.x = -Math.PI / 2;
- connector.rotation.z = Math.atan2(direction.x, direction.z);
- connector.position.copy(startPos.clone().add(endPos).multiplyScalar(0.5));
- connector.position.y = 0.08;
- scene.add(connector);
- });
- }
+ function createRoadNetwork() {
+ world.roads = []; // Clear existing roads
+ // Main highways (e.g., 4 lanes wide = 24 units)
+ createRoad(0, 0, ROAD_WIDTH_UNIT * 4, 800, 'highway', 0, true); // Horizontal E-W
+ createRoad(0, 0, ROAD_WIDTH_UNIT * 4, 800, 'highway', Math.PI / 2, false); // Vertical N-S
- function findNearestMainRoads(position) {
- const mainRoads = [];
-
- // Find nearest horizontal main road
- let nearestHorizontalDist = Infinity;
- let nearestHorizontalZ = 0;
- for (let z = -300; z <= 300; z += 150) {
- const dist = Math.abs(position.z - z);
- if (dist < nearestHorizontalDist) {
- nearestHorizontalDist = dist;
- nearestHorizontalZ = z;
- }
- }
-
- if (nearestHorizontalDist < 100) {
- mainRoads.push({
- position: new THREE.Vector3(position.x, 0, nearestHorizontalZ),
- type: 'horizontal'
- });
- }
-
- // Find nearest vertical main road
- let nearestVerticalDist = Infinity;
- let nearestVerticalX = 0;
- for (let x = -300; x <= 300; x += 150) {
- const dist = Math.abs(position.x - x);
- if (dist < nearestVerticalDist) {
- nearestVerticalDist = dist;
- nearestVerticalX = x;
- }
- }
-
- if (nearestVerticalDist < 100) {
- mainRoads.push({
- position: new THREE.Vector3(nearestVerticalX, 0, position.z),
- type: 'vertical'
- });
- }
-
- return mainRoads.slice(0, 2); // Maximum 2 connections per building
+ // Secondary roads (e.g., 2 lanes wide = 12 units)
+ createRoad(0, ROAD_SPACING, ROAD_WIDTH_UNIT * 2, 800, 'secondary', 0, true);
+ createRoad(0, -ROAD_SPACING, ROAD_WIDTH_UNIT * 2, 800, 'secondary', 0, true);
+ createRoad(ROAD_SPACING, 0, ROAD_WIDTH_UNIT * 2, 800, 'secondary', Math.PI / 2, false);
+ createRoad(-ROAD_SPACING, 0, ROAD_WIDTH_UNIT * 2, 800, 'secondary', Math.PI / 2, false);
+
+ // More roads for a denser network
+ createRoad(0, ROAD_SPACING * 2, ROAD_WIDTH_UNIT * 2, 800, 'local', 0, true);
+ createRoad(0, -ROAD_SPACING * 2, ROAD_WIDTH_UNIT * 2, 800, 'local', 0, true);
+ createRoad(ROAD_SPACING * 2, 0, ROAD_WIDTH_UNIT * 2, 800, 'local', Math.PI / 2, false);
+ createRoad(-ROAD_SPACING * 2, 0, ROAD_WIDTH_UNIT * 2, 800, 'local', Math.PI / 2, false);
}
function createBuildingsWithParkingLots() {
- world.buildings = [];
- world.parkingLots = [];
-
- const buildingMaterial = new THREE.MeshLambertMaterial({ color: 0x666666 });
- const parkingMaterial = new THREE.MeshLambertMaterial({ color: 0x333333 });
- const spotMaterial = new THREE.MeshLambertMaterial({ color: 0xffffff });
- const queueMaterial = new THREE.MeshLambertMaterial({ color: 0x222222 });
-
- // Create buildings at strategic locations with better spacing
+ world.buildings = []; world.parkingLots = []; // Clear previous
+ const buildingBaseMaterial = new THREE.MeshLambertMaterial({ color: 0xaaaaaa });
+ const parkingMaterial = new THREE.MeshLambertMaterial({ color: 0x383838 });
+ const spotMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.5 });
+ const barGraphMaterial = new THREE.MeshLambertMaterial({color: 0x007bff});
+
+
const buildingLocations = [
- { x: -200, z: -200 }, { x: 0, z: -200 }, { x: 200, z: -200 },
- { x: -200, z: 0 }, { x: 200, z: 0 },
- { x: -200, z: 200 }, { x: 0, z: 200 }, { x: 200, z: 200 },
{ x: -100, z: -100 }, { x: 100, z: -100 },
{ x: -100, z: 100 }, { x: 100, z: 100 },
- { x: -300, z: -300 }, { x: 300, z: -300 },
- { x: -300, z: 300 }, { x: 300, z: 300 }
+ { x: -250, z: -50 }, { x: 250, z: 50 },
+ { x: -50, z: -250 }, { x: 50, z: 250 },
];
-
+
buildingLocations.forEach((loc, index) => {
- // Create building
- const width = 20 + Math.random() * 10;
- const height = 12 + Math.random() * 20;
- const depth = 20 + Math.random() * 10;
-
- const buildingGeometry = new THREE.BoxGeometry(width, height, depth);
- const building = new THREE.Mesh(buildingGeometry, buildingMaterial);
- building.position.set(loc.x, height / 2, loc.z);
- building.castShadow = true;
- scene.add(building);
-
- world.buildings.push({ mesh: building });
-
- // Create enhanced parking lot with multiple access points
+ const bWidth = 20 + Math.random() * 15;
+ const bHeight = 15 + Math.random() * 25;
+ const bDepth = 20 + Math.random() * 15;
+
+ const buildingGeometry = new THREE.BoxGeometry(bWidth, bHeight, bDepth);
+ const buildingMesh = new THREE.Mesh(buildingGeometry, buildingBaseMaterial.clone());
+ buildingMesh.material.color.setHSL(Math.random(), 0.5, 0.6);
+ buildingMesh.position.set(loc.x, bHeight / 2 + 0.1, loc.z); // Slightly above ground
+ buildingMesh.castShadow = true;
+ scene.add(buildingMesh);
+
+ // Bar graph for visitor count
+ const barGeometry = new THREE.BoxGeometry(5, 1, 5); // Base size
+ const barGraphMesh = new THREE.Mesh(barGeometry, barGraphMaterial.clone());
+ barGraphMesh.position.set(loc.x, bHeight + 0.1 + 3, loc.z); // Position above building
+ barGraphMesh.scale.y = 0.1; // Start very small
+ barGraphMesh.visible = true;
+ scene.add(barGraphMesh);
+
+ const buildingData = { mesh: buildingMesh, parkingLot: null, visitorCount: 0, barGraphMesh: barGraphMesh, height: bHeight };
+ world.buildings.push(buildingData);
+
+ // Create parking lot next to building
+ const lotWidth = 40, lotDepth = 30;
+ const lotCenterX = loc.x + bWidth / 2 + lotWidth / 2 + 5; // East of building
+ const lotCenterZ = loc.z;
+
+ const lotGeometry = new THREE.PlaneGeometry(lotWidth, lotDepth);
+ const lotMesh = new THREE.Mesh(lotGeometry, parkingMaterial);
+ lotMesh.rotation.x = -Math.PI / 2;
+ lotMesh.position.set(lotCenterX, 0.05, lotCenterZ); // Slightly above ground, below roads
+ scene.add(lotMesh);
+
const parkingLot = {
- center: new THREE.Vector3(loc.x + width/2 + 25, 0.1, loc.z),
+ center: new THREE.Vector3(lotCenterX, 0.1, lotCenterZ),
spots: [],
- queue: [],
- approachLanes: [], // Multiple approach lanes
- exitLanes: [], // Multiple exit lanes
- accessPoints: [] // Multiple access points
+ approachLanes: [], exitLanes: [], accessPoints: [], // For future advanced queueing
+ building: buildingData // Link back to building
};
-
- // Main parking lot surface (larger)
- const lotGeometry = new THREE.PlaneGeometry(50, 40);
- const lot = new THREE.Mesh(lotGeometry, parkingMaterial);
- lot.rotation.x = -Math.PI / 2;
- lot.position.copy(parkingLot.center);
- scene.add(lot);
-
- // Create multiple approach lanes (3 lanes)
- for (let laneNum = 0; laneNum < 3; laneNum++) {
- const laneOffset = (laneNum - 1) * 10; // -10, 0, 10 offset
-
- // Approach lane
- const approachGeometry = new THREE.PlaneGeometry(6, 60);
- const approachLane = new THREE.Mesh(approachGeometry, queueMaterial);
- approachLane.rotation.x = -Math.PI / 2;
- approachLane.position.set(parkingLot.center.x - 35, 0.08, parkingLot.center.z + laneOffset);
- scene.add(approachLane);
-
- // Create approach queue positions for this lane
- const lanePositions = [];
- for (let q = 0; q < 10; q++) {
- const queuePos = new THREE.Vector3(
- parkingLot.center.x - 35,
- 1,
- parkingLot.center.z + laneOffset - 25 + (q * 5) // 5m spacing
- );
- lanePositions.push(queuePos);
- }
- parkingLot.approachLanes.push(lanePositions);
- }
-
- // Create multiple exit lanes (2 lanes)
- for (let exitNum = 0; exitNum < 2; exitNum++) {
- const exitOffset = (exitNum - 0.5) * 15; // -7.5, 7.5 offset
-
- // Exit lane
- const exitGeometry = new THREE.PlaneGeometry(6, 50);
- const exitLane = new THREE.Mesh(exitGeometry, queueMaterial);
- exitLane.rotation.x = -Math.PI / 2;
- exitLane.position.set(parkingLot.center.x + 35, 0.08, parkingLot.center.z + exitOffset);
- scene.add(exitLane);
-
- // Create exit positions for this lane
- const exitPositions = [];
- for (let q = 0; q < 8; q++) {
- const exitPos = new THREE.Vector3(
- parkingLot.center.x + 35,
- 1,
- parkingLot.center.z + exitOffset - 20 + (q * 5)
- );
- exitPositions.push(exitPos);
- }
- parkingLot.exitLanes.push(exitPositions);
- }
-
- // Create multiple access points with road connections
- const accessPoints = [
- { pos: new THREE.Vector3(loc.x + width/2 + 10, 1, loc.z + depth/2 + 15), name: 'north' },
- { pos: new THREE.Vector3(loc.x + width/2 + 10, 1, loc.z - depth/2 - 15), name: 'south' },
- { pos: new THREE.Vector3(loc.x + width + 15, 1, loc.z), name: 'east' },
- { pos: new THREE.Vector3(loc.x - 15, 1, loc.z), name: 'west' }
- ];
-
- parkingLot.accessPoints = accessPoints;
-
- // Create parking spots (5x8 grid = 40 spots)
- for (let row = 0; row < 5; row++) {
- for (let col = 0; col < 8; col++) {
- const spotX = parkingLot.center.x + (col - 3.5) * 6;
- const spotZ = parkingLot.center.z + (row - 2) * 7;
-
- // Spot marking
- const spotGeometry = new THREE.PlaneGeometry(PARKING_SPOT_SIZE.width, PARKING_SPOT_SIZE.length);
- const spotMesh = new THREE.Mesh(spotGeometry, spotMaterial);
- spotMesh.rotation.x = -Math.PI / 2;
- spotMesh.position.set(spotX, 0.12, spotZ);
- scene.add(spotMesh);
-
- const spot = {
- position: new THREE.Vector3(spotX, 1, spotZ),
- occupied: false,
- car: null,
- mesh: spotMesh
- };
-
- parkingLot.spots.push(spot);
+ buildingData.parkingLot = parkingLot; // Link building to its lot
+
+ // Parking spots (2 rows of 5)
+ const numRows = 2, spotsPerRow = 5;
+ for (let r = 0; r < numRows; r++) {
+ for (let s = 0; s < spotsPerRow; s++) {
+ const spotX = lotCenterX + (s - (spotsPerRow - 1)/2) * (PARKING_SPOT_SIZE.width + 2);
+ const spotZ = lotCenterZ + (r - (numRows - 1)/2) * (PARKING_SPOT_SIZE.length + 3);
+ const spotOrientation = Math.PI / 2; // Assuming spots are perpendicular to building side
+
+ const spotPlaneGeom = new THREE.PlaneGeometry(PARKING_SPOT_SIZE.width, PARKING_SPOT_SIZE.length);
+ const spotPlaneMesh = new THREE.Mesh(spotPlaneGeom, spotMaterial);
+ spotPlaneMesh.rotation.x = -Math.PI/2;
+ spotPlaneMesh.rotation.z = spotOrientation; // Align with how car would park
+ spotPlaneMesh.position.set(spotX, 0.06, spotZ);
+ scene.add(spotPlaneMesh);
+
+ parkingLot.spots.push({
+ position: new THREE.Vector3(spotX, 1, spotZ), // Car's y position when parked
+ orientation: spotOrientation, // Car's y rotation when parked
+ occupied: false, car: null, mesh: spotPlaneMesh
+ });
}
}
-
+ // Simplified approach/exit points for now
+ parkingLot.approachLanes.push([new THREE.Vector3(lotCenterX - lotWidth/2 - 5, 1, lotCenterZ)]); // Entry point
+ parkingLot.exitLanes.push([new THREE.Vector3(lotCenterX - lotWidth/2 - 10, 1, lotCenterZ + 5)]); // Exit point nearby
+
world.parkingLots.push(parkingLot);
});
}
function createInitialPopulation() {
population = [];
-
+ const startPositions = [ // Disperse starting positions
+ {x: -50, z: 0}, {x: 50, z: 0}, {x: 0, z: -50}, {x: 0, z: 50},
+ {x: -ROAD_SPACING, z: 0}, {x: ROAD_SPACING, z: 0},
+ {x: 0, z: -ROAD_SPACING}, {x: 0, z: ROAD_SPACING},
+ ];
for (let i = 0; i < populationSize; i++) {
- // Start cars on roads when possible
- const roadPositions = [
- { x: -280, z: 0 }, { x: 280, z: 0 },
- { x: 0, z: -280 }, { x: 0, z: 280 },
- { x: -130, z: 0 }, { x: 130, z: 0 },
- { x: 0, z: -130 }, { x: 0, z: 130 }
- ];
-
- const startPos = roadPositions[i % roadPositions.length];
- const car = new TrafficCar(
- startPos.x + (Math.random() - 0.5) * 10,
- startPos.z + (Math.random() - 0.5) * 10
- );
-
+ const pos = startPositions[i % startPositions.length];
+ const car = new TrafficCar(pos.x + Math.random()*10-5, pos.z + Math.random()*10-5);
population.push(car);
scene.add(car.mesh);
}
}
function evolvePopulation() {
- try {
- // Sort by fitness with error handling
- population.sort((a, b) => {
- const fitnessA = a.fitness || 0;
- const fitnessB = b.fitness || 0;
- return fitnessB - fitnessA;
- });
-
- // Advanced selection
- const eliteCount = Math.floor(populationSize * 0.15);
- const tournamentCount = Math.floor(populationSize * 0.25);
-
- const survivors = population.slice(0, eliteCount);
-
- // Tournament selection with error handling
- for (let i = 0; i < tournamentCount; i++) {
- try {
- const tournamentSize = 5;
- let best = null;
- let bestFitness = -Infinity;
-
- for (let j = 0; j < tournamentSize; j++) {
- const candidateIndex = Math.floor(Math.random() * Math.min(population.length, populationSize * 0.5));
- const candidate = population[candidateIndex];
- if (candidate && (candidate.fitness || 0) > bestFitness) {
- best = candidate;
- bestFitness = candidate.fitness || 0;
- }
- }
- if (best) survivors.push(best);
- } catch (error) {
- console.warn('Error in tournament selection:', error);
- }
- }
-
- // Clean up old population
- population.forEach(car => {
- try {
- car.destroy();
- } catch (error) {
- console.warn('Error destroying car during evolution:', error);
- }
- });
-
- // Create new population
- const newPopulation = [];
- const roadPositions = [
- { x: -280, z: 0 }, { x: 280, z: 0 },
- { x: 0, z: -280 }, { x: 0, z: 280 },
- { x: -130, z: 0 }, { x: 130, z: 0 },
- { x: 0, z: -130 }, { x: 0, z: 130 }
- ];
-
- // Elite reproduction
- survivors.forEach((parent, index) => {
- try {
- const startPos = roadPositions[index % roadPositions.length];
- const newCar = new TrafficCar(
- startPos.x + (Math.random() - 0.5) * 10,
- startPos.z + (Math.random() - 0.5) * 10
- );
- if (parent.brain) {
- newCar.brain = parent.brain.copy();
- }
- newPopulation.push(newCar);
- scene.add(newCar.mesh);
- } catch (error) {
- console.warn('Error creating elite offspring:', error);
- }
- });
-
- // Mutated offspring
- while (newPopulation.length < populationSize) {
- try {
- const parentIndex = Math.floor(Math.random() * Math.min(survivors.length, eliteCount * 2));
- const parent = survivors[parentIndex];
- const startPos = roadPositions[newPopulation.length % roadPositions.length];
-
- const child = new TrafficCar(
- startPos.x + (Math.random() - 0.5) * 10,
- startPos.z + (Math.random() - 0.5) * 10
- );
-
- if (parent && parent.brain) {
- child.brain = parent.brain.copy();
- const mutationRate = (parent.fitness || 0) > bestFitness * 0.8 ? 0.05 : 0.15;
- child.brain.mutate(mutationRate);
- }
-
- newPopulation.push(child);
- scene.add(child.mesh);
- } catch (error) {
- console.warn('Error creating mutated offspring:', error);
- // Create a basic car as fallback
- try {
- const startPos = roadPositions[newPopulation.length % roadPositions.length];
- const basicCar = new TrafficCar(
- startPos.x + (Math.random() - 0.5) * 10,
- startPos.z + (Math.random() - 0.5) * 10
- );
- newPopulation.push(basicCar);
- scene.add(basicCar.mesh);
- } catch (fallbackError) {
- console.error('Error creating fallback car:', fallbackError);
- }
- }
- }
-
- population = newPopulation;
-
- // Update epoch
- epoch++;
- timeLeft = epochTime;
- bestFitness = Math.max(bestFitness, survivors[0]?.fitness || 0);
- crashCount = 0;
- parkingEvents = 0;
- laneViolations = 0;
-
- console.log(`Epoch ${epoch}: Best fitness: ${bestFitness.toFixed(1)}, Population: ${population.length}`);
-
- } catch (error) {
- console.error('Critical error during evolution:', error);
- // Try to recover by creating a new basic population
- try {
- createInitialPopulation();
- } catch (recoveryError) {
- console.error('Failed to recover from evolution error:', recoveryError);
- }
+ population.sort((a, b) => (b.fitness || 0) - (a.fitness || 0)); // Higher fitness first
+ bestFitness = population[0] ? population[0].fitness : 0;
+
+ const eliteCount = Math.floor(populationSize * 0.1); // Top 10% survive
+ const survivors = population.slice(0, eliteCount);
+
+ const newPopulation = [];
+
+ // Add elites directly
+ survivors.forEach(parent => {
+ const offspring = new TrafficCar(parent.mesh.position.x, parent.mesh.position.z);
+ offspring.brain = parent.brain.copy(); // Elites pass genes directly
+ newPopulation.push(offspring);
+ });
+
+ // Fill rest with mutated offspring from survivors
+ while (newPopulation.length < populationSize) {
+ const parent = survivors[Math.floor(Math.random() * survivors.length)];
+ const offspring = new TrafficCar(parent.mesh.position.x, parent.mesh.position.z); // Start near parent
+ offspring.brain = parent.brain.copy();
+ offspring.brain.mutate(0.1); // Standard mutation rate
+ newPopulation.push(offspring);
}
+
+ // Cleanup old population and add new
+ population.forEach(car => car.destroy());
+ population = newPopulation;
+ population.forEach(car => scene.add(car.mesh));
+
+ epoch++;
+ timeLeft = epochTime;
+ crashCount = 0; parkingEvents = 0; laneViolations = 0;
+ world.parkingLots.forEach(lot => { // Reset parking lot visitor counts
+ if (lot.building) lot.building.visitorCount = 0;
+ lot.spots.forEach(spot => { spot.occupied = false; spot.car = null; });
+ });
+ console.log(`Epoch ${epoch}: Best Fitness: ${bestFitness.toFixed(1)}`);
}
function animate() {
requestAnimationFrame(animate);
+ const deltaTime = Math.min(clock.getDelta() * speedMultiplier, 0.1); // Cap delta
if (!paused) {
- const deltaTime = Math.min(clock.getDelta() * speedMultiplier, 0.1);
-
timeLeft -= deltaTime;
if (timeLeft <= 0) {
evolvePopulation();
}
-
updatePopulation(deltaTime);
updateCamera();
updateUI();
}
-
renderer.render(scene, camera);
}
function updatePopulation(deltaTime) {
- let stats = {
- alive: 0,
- leaders: 0,
- convoy: 0,
- parked: 0,
- solo: 0,
- maxConvoySize: 0,
- totalRoadTime: 0,
- totalConvoyTime: 0,
- totalParkingScore: 0,
- totalViolations: 0,
- totalFollowingDistance: 0,
- followingCount: 0,
- approaching: 0 // Cars approaching parking
- };
-
- // Update each car with error handling
- population.forEach((car, index) => {
- try {
- car.update(deltaTime);
-
- if (!car.crashed) {
- stats.alive++;
- stats.totalRoadTime += car.roadTime || 0;
- stats.totalConvoyTime += car.convoyTime || 0;
- stats.totalParkingScore += car.parkingScore || 0;
- stats.totalViolations += car.trafficViolations || 0;
-
- if (car.isParked) {
- stats.parked++;
- } else if (car.isParkingApproach) {
- stats.approaching++;
- } else if (car.role === 'leader') {
- stats.leaders++;
- const followerCount = car.convoyFollowers ? car.convoyFollowers.length : 0;
- stats.maxConvoySize = Math.max(stats.maxConvoySize, followerCount + 1);
- } else if (car.convoyPosition >= 0) {
- stats.convoy++;
- if (car.followingDistance > 0) {
- stats.totalFollowingDistance += car.followingDistance;
- stats.followingCount++;
- }
- } else {
- stats.solo++;
+ let currentStats = { alive: 0, leaders: 0, convoy: 0, parked: 0, solo: 0, maxConvoySize: 0, totalRoadTime: 0, totalViolations: 0, totalFollowingDistance: 0, followingCount: 0, approaching:0 };
+ population.forEach(car => {
+ if (!car.crashed) {
+ car.update(deltaTime); // Car's internal update
+ currentStats.alive++;
+ if (car.isParked) currentStats.parked++;
+ else if (car.isParkingApproach || car.isInApproachLane) currentStats.approaching++;
+ else if (car.role === 'leader') currentStats.leaders++;
+ else if (car.convoyPosition > 0) {
+ currentStats.convoy++;
+ if (car.followTarget) {
+ currentStats.totalFollowingDistance += car.mesh.position.distanceTo(car.followTarget.mesh.position);
+ currentStats.followingCount++;
}
- }
- } catch (error) {
- console.warn(`Error updating car ${index}:`, error);
- // Mark problematic car as crashed to prevent further errors
- car.crashed = true;
+ } else currentStats.solo++;
+ if (car.role==='leader' && car.convoyFollowers) currentStats.maxConvoySize = Math.max(currentStats.maxConvoySize, car.convoyFollowers.length + 1);
+ currentStats.totalRoadTime += car.roadTime;
+ currentStats.totalViolations += car.trafficViolations;
}
});
-
- window.populationStats = stats;
+ window.populationStats = currentStats; // Make accessible for UI
}
function updateCamera() {
- try {
- if (cameraMode === 'follow_best') {
- // Follow best performing car
- let bestCar = null;
- let bestFitness = -1;
-
- population.forEach(car => {
- if (car && !car.crashed && !car.isParked && car.fitness > bestFitness) {
- bestCar = car;
- bestFitness = car.fitness;
- }
- });
-
- if (bestCar && bestCar.mesh) {
- const targetPos = bestCar.mesh.position.clone();
- targetPos.y += 40;
- if (bestCar.velocity && bestCar.velocity.length() > 0.1) {
- targetPos.add(bestCar.velocity.clone().normalize().multiplyScalar(25));
- }
-
- camera.position.lerp(targetPos, 0.03);
- camera.lookAt(bestCar.mesh.position);
- }
- } else if (cameraMode === 'follow_convoy') {
- // Follow largest convoy
- let largestConvoy = null;
- let maxFollowers = 0;
-
- population.forEach(car => {
- if (car && car.role === 'leader' &&
- car.convoyFollowers && car.convoyFollowers.length > maxFollowers) {
- largestConvoy = car;
- maxFollowers = car.convoyFollowers.length;
- }
- });
-
- if (largestConvoy && largestConvoy.mesh) {
- const targetPos = largestConvoy.mesh.position.clone();
- targetPos.y += 50;
- if (largestConvoy.velocity && largestConvoy.velocity.length() > 0.1) {
- targetPos.add(largestConvoy.velocity.clone().normalize().multiplyScalar(30));
- }
-
- camera.position.lerp(targetPos, 0.03);
- camera.lookAt(largestConvoy.mesh.position);
- }
- } else {
- // Overview mode
- camera.position.lerp(new THREE.Vector3(0, 180, 180), 0.02);
- camera.lookAt(0, 0, 0);
- }
- } catch (error) {
- console.warn('Error updating camera:', error);
- // Fallback to overview mode
- try {
- camera.position.lerp(new THREE.Vector3(0, 180, 180), 0.02);
- camera.lookAt(0, 0, 0);
- } catch (fallbackError) {
- console.error('Critical camera error:', fallbackError);
- }
+ let targetCar = null;
+ if (cameraMode === 'follow_best') {
+ targetCar = population.filter(c => !c.crashed && !c.isParked).sort((a,b) => b.fitness - a.fitness)[0];
+ manuallyControlledCar = targetCar; // Set for manual control
+ } else if (cameraMode === 'follow_convoy') {
+ targetCar = population.filter(c => c.role === 'leader' && c.convoyFollowers.length > 0)
+ .sort((a,b) => b.convoyFollowers.length - a.convoyFollowers.length)[0];
+ manuallyControlledCar = null;
+ } else {
+ manuallyControlledCar = null;
+ }
+
+ if (targetCar) {
+ const offset = new THREE.Vector3(0, 30, -25); // Higher and behind
+ const targetPosition = targetCar.mesh.position.clone().add(offset.applyQuaternion(targetCar.mesh.quaternion));
+ camera.position.lerp(targetPosition, 0.05);
+ camera.lookAt(targetCar.mesh.position);
+ } else { // Overview
+ camera.position.lerp(new THREE.Vector3(0, 200, 200), 0.02);
+ camera.lookAt(0, 0, 0);
}
}
function updateUI() {
const stats = window.populationStats || {};
-
- // Main UI
document.getElementById('epoch').textContent = epoch;
document.getElementById('epochTime').textContent = Math.ceil(timeLeft);
+ document.getElementById('timeProgress').style.width = `${((epochTime - timeLeft) / epochTime) * 100}%`;
document.getElementById('population').textContent = stats.alive || 0;
document.getElementById('bestFitness').textContent = Math.round(bestFitness);
+ document.getElementById('trafficIQ').textContent = Math.round(50 + (bestFitness / 50)); // Scaled IQ
+ document.getElementById('roadMastery').textContent = stats.alive > 0 ? Math.round((stats.totalRoadTime / stats.alive) / epochTime * 100) : 0;
- // Progress bar
- const progress = ((epochTime - timeLeft) / epochTime) * 100;
- document.getElementById('timeProgress').style.width = `${progress}%`;
-
- // Traffic stats
- if (stats.alive > 0) {
- document.getElementById('trafficIQ').textContent = Math.round(50 + (bestFitness / 20));
- document.getElementById('roadMastery').textContent = Math.round((stats.totalRoadTime / stats.alive) * 10);
- document.getElementById('laneDiscipline').textContent = Math.round(Math.max(0, 100 - (stats.totalViolations / stats.alive) * 10));
- document.getElementById('roadAdherence').textContent = Math.round((stats.totalRoadTime / (stats.totalRoadTime + 1)) * 100);
- }
+ document.getElementById('crashCount').textContent = crashCount;
+ document.getElementById('parkingEvents').textContent = parkingEvents; // Global counter
+ document.getElementById('laneViolations').textContent = laneViolations; // Global counter
- // Convoy stats
document.getElementById('leaderCount').textContent = stats.leaders || 0;
document.getElementById('convoyCount').textContent = stats.convoy || 0;
document.getElementById('parkedCount').textContent = stats.parked || 0;
document.getElementById('soloCount').textContent = stats.solo || 0;
document.getElementById('largestConvoy').textContent = stats.maxConvoySize || 0;
-
- // Following distance
- if (stats.followingCount > 0) {
- document.getElementById('followingDistance').textContent = (stats.totalFollowingDistance / stats.followingCount).toFixed(1);
- }
-
- // Generation stats
- const totalDistance = population.reduce((sum, car) => sum + car.distanceTraveled, 0);
- const maxConvoyLength = Math.max(...population.map(car => car.convoyFollowers?.length || 0));
-
- document.getElementById('totalDistance').textContent = Math.round(totalDistance);
- document.getElementById('parkingEvents').textContent = parkingEvents;
- document.getElementById('laneViolations').textContent = laneViolations;
- document.getElementById('convoyLength').textContent = maxConvoyLength;
- document.getElementById('crashCount').textContent = crashCount;
-
- // Parking stats
- const totalSpots = world.parkingLots.reduce((sum, lot) => sum + lot.spots.length, 0);
- const occupiedSpots = world.parkingLots.reduce((sum, lot) =>
- sum + lot.spots.filter(spot => spot.occupied).length, 0);
-
- document.getElementById('spotsOccupied').textContent = occupiedSpots;
- document.getElementById('parkingSuccess').textContent = totalSpots > 0 ? Math.round((occupiedSpots / totalSpots) * 100) : 0;
-
- updateTopPerformers();
+
+ // Update building bar graphs
+ world.buildings.forEach(buildingData => {
+ if (buildingData.barGraphMesh) {
+ const scaleY = Math.max(0.1, buildingData.visitorCount * 2); // Scale factor for bar height
+ buildingData.barGraphMesh.scale.y = scaleY;
+ // Adjust y position so it grows upwards from its base
+ buildingData.barGraphMesh.position.y = buildingData.height + 0.1 + 3 + (scaleY / 2) * buildingData.barGraphMesh.geometry.parameters.height;
+ }
+ });
+
+ updateTopPerformersDisplay(); // Separate function for clarity
}
- function updateTopPerformers() {
- const sorted = [...population]
- .filter(car => !car.crashed)
- .sort((a, b) => b.fitness - a.fitness)
- .slice(0, 5);
-
+ function updateTopPerformersDisplay() {
+ const sorted = [...population].filter(car => !car.crashed).sort((a, b) => b.fitness - a.fitness).slice(0, 5);
const topPerformersDiv = document.getElementById('topPerformers');
topPerformersDiv.innerHTML = '';
-
sorted.forEach((car, i) => {
const div = document.createElement('div');
- const roleIcon = {
- leader: '👑',
- parker: '🅿️',
- driver: '🚗'
- }[car.role] || '🚗';
-
- const statusIcon = car.isParked ? '🅿️' : (car.convoyPosition >= 0 ? '🚛' : '🚗');
-
- div.innerHTML = `${i + 1}. ${roleIcon}${statusIcon} F:${Math.round(car.fitness)} | Lane:${Math.round(car.laneDiscipline * 100)}% | Road:${Math.round(car.roadTime)}s`;
- div.className = car.isParked ? 'parked' : (car.role === 'leader' ? 'leader' : (car.convoyPosition >= 0 ? 'convoy' : 'solo'));
+ div.innerHTML = `${i + 1}. F:${Math.round(car.fitness)} Role:${car.role}`;
topPerformersDiv.appendChild(div);
});
}
-
+
function setupEventListeners() {
- document.getElementById('pauseBtn').addEventListener('click', togglePause);
+ document.getElementById('pauseBtn').addEventListener('click', () => { paused = !paused; document.getElementById('pauseBtn').textContent = paused ? 'Resume' : 'Pause'; });
document.getElementById('resetBtn').addEventListener('click', resetSimulation);
- document.getElementById('speedBtn').addEventListener('click', toggleSpeed);
- document.getElementById('viewBtn').addEventListener('click', toggleView);
- document.getElementById('flockBtn').addEventListener('click', toggleFlockLines);
- document.getElementById('trafficBtn').addEventListener('click', toggleTrafficRules);
- }
-
- function togglePause() {
- paused = !paused;
- document.getElementById('pauseBtn').textContent = paused ? 'Resume' : 'Pause';
- if (!paused) clock.start();
- }
+ document.getElementById('speedBtn').addEventListener('click', () => { speedMultiplier = speedMultiplier === 1 ? 2 : speedMultiplier === 2 ? 5 : 1; document.getElementById('speedBtn').textContent = `Speed: ${speedMultiplier}x`; });
+ document.getElementById('viewBtn').addEventListener('click', () => {
+ const modes = ['overview', 'follow_best', 'follow_convoy'];
+ cameraMode = modes[(modes.indexOf(cameraMode) + 1) % modes.length];
+ document.getElementById('viewBtn').textContent = `View: ${cameraMode.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase())}`;
+ });
+ document.getElementById('flockBtn').addEventListener('click', () => {
+ showFlockLines = !showFlockLines;
+ document.getElementById('flockBtn').textContent = `Networks: ${showFlockLines ? 'ON' : 'OFF'}`;
+ world.flockLines.forEach(line => line.visible = showFlockLines); // Global flock lines (if any)
+ population.forEach(car => car.flockLines.forEach(line => line.visible = showFlockLines && (line.parent === scene))); // Car-specific lines
+ });
+ document.getElementById('trafficBtn').addEventListener('click', () => { /* trafficRules toggle, might affect AI behavior if implemented */ });
- function resetSimulation() {
- epoch = 1;
- timeLeft = epochTime;
- bestFitness = 0;
- crashCount = 0;
- parkingEvents = 0;
- laneViolations = 0;
-
- // Reset parking lots and queues with error handling
- world.parkingLots.forEach(lot => {
- try {
- lot.spots.forEach(spot => {
- spot.occupied = false;
- spot.car = null;
- });
- lot.queue = []; // Clear parking queues
-
- // Initialize multiple lanes if they don't exist
- if (!lot.approachLanes) {
- lot.approachLanes = [lot.approachLane || []];
- }
- if (!lot.exitLanes) {
- lot.exitLanes = [lot.exitLane || []];
- }
- if (!lot.accessPoints) {
- lot.accessPoints = [];
- }
- } catch (error) {
- console.warn('Error resetting parking lot:', error);
+ // Manual control listeners
+ document.addEventListener('keydown', (event) => {
+ if (manuallyControlledCar && cameraMode === 'follow_best') {
+ if (event.key === 'w' || event.key === 'W') manualControls.W = true;
+ if (event.key === 's' || event.key === 'S') manualControls.S = true;
+ if (event.key === 'a' || event.key === 'A') manualControls.A = true;
+ if (event.key === 'd' || event.key === 'D') manualControls.D = true;
}
});
-
- // Clean up population with error handling
- population.forEach(car => {
- try {
- car.destroy();
- } catch (error) {
- console.warn('Error destroying car:', error);
+ document.addEventListener('keyup', (event) => {
+ if (manuallyControlledCar && cameraMode === 'follow_best') {
+ if (event.key === 'w' || event.key === 'W') manualControls.W = false;
+ if (event.key === 's' || event.key === 'S') manualControls.S = false;
+ if (event.key === 'a' || event.key === 'A') manualControls.A = false;
+ if (event.key === 'd' || event.key === 'D') manualControls.D = false;
}
});
-
- // Clear any remaining visual elements
- try {
- const linesToRemove = [];
- scene.traverse(child => {
- if (child.isLine && child.material && child.material.color) {
- // Check if it's a flock line (green color)
- if (child.material.color.g > 0.8) {
- linesToRemove.push(child);
- }
- }
- });
-
- linesToRemove.forEach(line => {
- scene.remove(line);
- });
- } catch (error) {
- console.warn('Error cleaning up visual elements:', error);
- }
-
- createInitialPopulation();
- }
-
- function toggleSpeed() {
- speedMultiplier = speedMultiplier === 1 ? 2 : speedMultiplier === 2 ? 5 : 1;
- document.getElementById('speedBtn').textContent = `Speed: ${speedMultiplier}x`;
}
- function toggleView() {
- const modes = ['overview', 'follow_best', 'follow_convoy'];
- const currentIndex = modes.indexOf(cameraMode);
- cameraMode = modes[(currentIndex + 1) % modes.length];
-
- const displayNames = {
- overview: 'Overview',
- follow_best: 'Follow Best',
- follow_convoy: 'Follow Convoy'
- };
-
- document.getElementById('viewBtn').textContent = `View: ${displayNames[cameraMode]}`;
- }
-
- function toggleFlockLines() {
- showFlockLines = !showFlockLines;
- document.getElementById('flockBtn').textContent = `Networks: ${showFlockLines ? 'ON' : 'OFF'}`;
-
- population.forEach(car => {
- car.flockLines.forEach(line => {
- if (showFlockLines && !line.parent) {
- scene.add(line);
- } else if (!showFlockLines && line.parent) {
- scene.remove(line);
- }
- });
+ function resetSimulation() {
+ epoch = 1; timeLeft = epochTime; bestFitness = 0; crashCount = 0; parkingEvents = 0; laneViolations = 0;
+ population.forEach(car => car.destroy()); // Proper cleanup
+ // Clear building visitor counts and bar graphs
+ world.buildings.forEach(buildingData => {
+ buildingData.visitorCount = 0;
+ if (buildingData.barGraphMesh) {
+ buildingData.barGraphMesh.scale.y = 0.1;
+ buildingData.barGraphMesh.position.y = buildingData.height + 0.1 + 3 + (0.1 / 2) * buildingData.barGraphMesh.geometry.parameters.height;
+ }
});
- }
-
- function toggleTrafficRules() {
- trafficRules = !trafficRules;
- document.getElementById('trafficBtn').textContent = `Traffic Rules: ${trafficRules ? 'ON' : 'OFF'}`;
+ world.parkingLots.forEach(lot => {
+ lot.spots.forEach(spot => { spot.occupied = false; spot.car = null; });
+ });
+ createInitialPopulation();
}
function onWindowResize() {
@@ -2852,4 +1828,4 @@
init();