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(); - \ No newline at end of file +