User prompt
Agrega una preferencia para que busquen autos que estén solos así evitar agrupamientos de autos y que se queden atorados
User prompt
Agrega una preferencia para que busquen autos que estén solos así evitar agrupamientos de autos y que se queden atorados
User prompt
Haz que lo autos tiendan a alejarse de los grupos grandes
User prompt
Agrega colores más variados
User prompt
Agranda el círculo
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ var EnemyCar = Container.expand(function () { var self = Container.call(this); var enemyCarGraphics = self.attachAsset('Cars', { anchorX: 0.5, anchorY: 0.5 }); // Assign random color (excluding red) - expanded palette with more variety var enemyColors = [0x0066ff, // Blue 0x00ff66, // Green 0xffff00, // Yellow 0xff8800, // Orange 0x8800ff, // Purple 0x00ffff, // Cyan 0xff00ff, // Magenta 0x888888, // Gray 0xffffff, // White 0x4169e1, // Royal Blue 0x32cd32, // Lime Green 0xff1493, // Deep Pink 0x00ced1, // Dark Turquoise 0x9370db, // Medium Purple 0xffd700, // Gold 0xff6347, // Tomato 0x20b2aa, // Light Sea Green 0xda70d6, // Orchid 0x87ceeb, // Sky Blue 0xf0e68c, // Khaki 0xdda0dd, // Plum 0x98fb98, // Pale Green 0xf5deb3, // Wheat 0xcd5c5c, // Indian Red 0x40e0d0, // Turquoise 0xee82ee, // Violet 0x90ee90, // Light Green 0xffb6c1, // Light Pink 0xffa500 // Orange Red ]; var randomColorIndex = Math.floor(Math.random() * enemyColors.length); enemyCarGraphics.tint = enemyColors[randomColorIndex]; // Basic properties for enemy car with weight self.velocityX = 0; self.velocityY = 0; self.rotation = 0; self.weight = 1.0 + Math.random() * 1.5; // Random weight between 1.0 and 2.5 return self; }); var Particle = Container.expand(function () { var self = Container.call(this); // Random particle size between 10.8-25.2 pixels (20% smaller than original) var particleSize = 10.8 + Math.random() * 14.4; var particleGraphics = self.attachAsset('ParticulasVel', { anchorX: 0.5, anchorY: 0.5, scaleX: particleSize / 30, scaleY: particleSize / 30 }); // Random initial properties (20% smaller velocities) self.velocityX = (Math.random() - 0.5) * 3.2; self.velocityY = Math.random() * 2.4 + 0.8; self.lifespan = 20 + Math.random() * 20; // Reduced lifespan: 0.33-0.67 seconds at 60fps self.age = 0; self.update = function () { // Update position self.x += self.velocityX; self.y += self.velocityY; // Age particle self.age++; // Fade out over time var fadeProgress = self.age / self.lifespan; particleGraphics.alpha = 1 - fadeProgress; // Scale down over time var scaleProgress = 1 - fadeProgress * 0.5; particleGraphics.scaleX = particleSize / 30 * scaleProgress; particleGraphics.scaleY = particleSize / 30 * scaleProgress; // Apply gravity and air resistance (20% reduced for smaller scale) self.velocityY += 0.08; // Reduced gravity self.velocityX *= 0.984; // Slightly less air resistance self.velocityY *= 0.984; }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x000000 }); /**** * Game Code ****/ // Create gameplay background - 4/5 of screen height (top portion) 6; var gameplayBackground = game.attachAsset('gameplayBg', { x: 0, y: 0, anchorX: 0, anchorY: 0 }); // Create carPlayer character on top of gameplayBackground var carPlayer = gameplayBackground.attachAsset('CarPlayer', { x: 1024, // Center horizontally y: 1800, // Position in lower portion of gameplay area anchorX: 0.5, anchorY: 0.5 }); // Create array to store multiple enemy cars var enemyCars = []; var numEnemyCars = 5 + Math.floor(Math.random() * 10); // Random between 5-14 cars var totalCars = numEnemyCars + 1; // Include player in formation // Position player in circular formation first var centerX = 1024; // Center of gameplay area var centerY = 1093; // Center of gameplay area (2186/2) var spawnRadius = 700 + Math.random() * 300; // Larger spawn radius between 700-1000 pixels from center var angleStep = Math.PI * 2 / totalCars; // Evenly divide circle by total number of cars (including player) // Position player at first angle position var playerSpawnAngle = 0 * angleStep; // Player gets first position var angleVariation = (Math.random() - 0.5) * 0.2; // Smaller variation for better formation playerSpawnAngle += angleVariation; // Calculate player spawn position carPlayer.x = centerX + Math.cos(playerSpawnAngle) * spawnRadius; carPlayer.y = centerY + Math.sin(playerSpawnAngle) * spawnRadius; // Ensure player stays within gameplay bounds carPlayer.x = Math.max(80, Math.min(1968, carPlayer.x)); carPlayer.y = Math.max(80, Math.min(2106, carPlayer.y)); // Calculate player rotation to face the center var playerDeltaX = centerX - carPlayer.x; var playerDeltaY = centerY - carPlayer.y; carPlayer.rotation = Math.atan2(playerDeltaX, -playerDeltaY); // Rotation to face center // Create multiple enemy cars in a circular formation for (var carIndex = 0; carIndex < numEnemyCars; carIndex++) { var enemyCar = new EnemyCar(); // Calculate circular spawn position (starting from position 1, since player is at position 0) var enemySpawnAngle = (carIndex + 1) * angleStep; // Enemy cars get positions 1, 2, 3, etc. // Add slight random offset to avoid perfect symmetry var angleVariation = (Math.random() - 0.5) * 0.2; // Smaller variation for better formation enemySpawnAngle += angleVariation; // Calculate spawn position enemyCar.x = centerX + Math.cos(enemySpawnAngle) * spawnRadius; enemyCar.y = centerY + Math.sin(enemySpawnAngle) * spawnRadius; // Ensure cars stay within gameplay bounds enemyCar.x = Math.max(80, Math.min(1968, enemyCar.x)); enemyCar.y = Math.max(80, Math.min(2106, enemyCar.y)); // Calculate rotation to face the center var deltaX = centerX - enemyCar.x; var deltaY = centerY - enemyCar.y; enemyCar.rotation = Math.atan2(deltaX, -deltaY); // Rotation to face center // Initialize enemy car health to 100 enemyCar.health = 100; // Enemy starts with full health // Give each AI car individual tactical preferences and personality enemyCar.tacticalPersonality = { // Primary strategy preference probabilities (0-1) directPreference: 0.2 + Math.random() * 0.6, // 0.2-0.8 preference for direct pursuit ambushPreference: 0.1 + Math.random() * 0.7, // 0.1-0.8 preference for ambush tactics intimidationPreference: 0.1 + Math.random() * 0.5, // 0.1-0.6 preference for intimidation // Speed-based tactical preferences slowTargetStrategy: Math.floor(Math.random() * 3), // 0=direct, 1=intimidate, 2=ambush for slow targets fastTargetStrategy: Math.floor(Math.random() * 3), // 0=direct, 1=intimidate, 2=ambush for fast targets mediumTargetStrategy: Math.floor(Math.random() * 3), // 0=direct, 1=intimidate, 2=ambush for medium targets // Individual behavioral traits aggressiveness: 0.3 + Math.random() * 0.7, // 0.3-1.0 how aggressive this car is patience: 0.2 + Math.random() * 0.8, // 0.2-1.0 how long car sticks to one strategy adaptability: 0.1 + Math.random() * 0.9, // 0.1-1.0 how quickly car changes strategies // Individual strategy duration preferences (in frames) minStrategyDuration: 30 + Math.floor(Math.random() * 60), // 0.5-1.5 seconds minimum maxStrategyDuration: 90 + Math.floor(Math.random() * 180), // 1.5-4.5 seconds maximum // Personal distance preferences preferredAttackDistance: 100 + Math.random() * 200, // 100-300 preferred distance for attacks preferredIntimidationDistance: 120 + Math.random() * 100, // 120-220 preferred intimidation distance personalSpaceRadius: 80 + Math.random() * 120, // 80-200 personal space when maneuvering // Individual flight and evasion preferences flightTendency: 0.2 + Math.random() * 0.6, // 0.2-0.8 how likely to flee when pursued bravery: 0.1 + Math.random() * 0.8, // 0.1-0.9 how brave this car is (opposes flight tendency) panicThreshold: 0.3 + Math.random() * 0.5, // 0.3-0.8 pursuit intensity needed to trigger panic // Preferred evasion tactics (0-1 probability for each) preferSpeedEscape: Math.random(), // 0-1 preference for straight-line speed escapes preferZigzagEscape: Math.random(), // 0-1 preference for zigzag evasion patterns preferCircularEscape: Math.random(), // 0-1 preference for circular evasion patterns // Flight decision factors flightDistance: 150 + Math.random() * 200, // 150-350 distance at which flight is considered maxFlightDuration: 120 + Math.random() * 240, // 2-6 seconds maximum flight time before reconsidering counterAttackChance: 0.2 + Math.random() * 0.5, // 0.2-0.7 base chance to counter-attack instead of fleeing riskTolerance: 0.1 + Math.random() * 0.7, // 0.1-0.8 tolerance for risky situations // Health-based behavioral modifiers baseAggressiveness: 0.3 + Math.random() * 0.7, // Store original aggressiveness baseFlightTendency: 0.2 + Math.random() * 0.6, // Store original flight tendency healthAggressionBonus: 0.5 + Math.random() * 0.3, // 0.5-0.8 how much health affects aggression healthFlightBonus: 0.3 + Math.random() * 0.4, // 0.3-0.7 how much low health increases flight // Camping behavior (new personality type) isCamper: Math.random() < 0.25, // 25% chance to be a camper campingPatience: 0.6 + Math.random() * 0.4, // 0.6-1.0 how patient camper is campingThreshold: 0.6 + Math.random() * 0.3, // 0.6-0.9 health percentage to start attacking campingDistance: 200 + Math.random() * 150, // 200-350 preferred camping distance opportunismLevel: 0.7 + Math.random() * 0.3 // 0.7-1.0 how opportunistic the camper is }; // Add anti-grouping preferences for smarter car distribution enemyCar.tacticalPersonality.antiGroupingPreference = 0.4 + Math.random() * 0.5; // 0.4-0.9 preference for avoiding groups enemyCar.tacticalPersonality.isolationSeekingLevel = 0.3 + Math.random() * 0.6; // 0.3-0.9 how much to seek isolated targets enemyCar.tacticalPersonality.clusterAvoidanceRadius = 120 + Math.random() * 80; // 120-200 pixels to consider as "clustered" enemyCar.tacticalPersonality.maxClusterTolerance = 2 + Math.floor(Math.random() * 2); // Max 2-3 cars in nearby area before avoiding // Add to array and scene enemyCars.push(enemyCar); gameplayBackground.addChild(enemyCar); } // For backward compatibility, keep reference to first enemy car var enemyCar = enemyCars[0]; // Create UI background - 1/5 of screen height (bottom portion) var uiBackground = game.attachAsset('uiBg', { x: 0, y: 2186, anchorX: 0, anchorY: 0 }); // Create player health bar at the top right of UI with 20px margin var healthBarBg = uiBackground.attachAsset('BarBg', { x: 1800, y: 40, anchorX: 0.5, anchorY: 0.5, scaleX: 4, scaleY: 0.3 }); var healthBar = uiBackground.attachAsset('Bar', { x: 1800, y: 40, anchorX: 0.5, anchorY: 0.5, scaleX: 4, scaleY: 0.3 }); // Tint health bar with player car color (red) healthBar.tint = 0xff0000; // Create enemy health bars array var enemyHealthBars = []; var enemyHealthBarBgs = []; // Function to create enemy health bars function createEnemyHealthBar(enemyIndex, enemyColor) { // Use same dimensions and scaling as player health bar var barX = healthBarBg.x; var barY = healthBarBg.y + 40 + enemyIndex * 35; var barAnchorX = healthBarBg.anchorX || 0.5; var barAnchorY = healthBarBg.anchorY || 0.5; var barScaleX = healthBarBg.scaleX || 4; var barScaleY = healthBarBg.scaleY || 0.3; // Create background for enemy health bar var enemyHealthBarBg = uiBackground.attachAsset('BarBg', { x: barX, y: barY, anchorX: barAnchorX, anchorY: barAnchorY, scaleX: barScaleX, scaleY: barScaleY }); // Create enemy health bar var enemyHealthBar = uiBackground.attachAsset('Bar', { x: barX, y: barY, anchorX: barAnchorX, anchorY: barAnchorY, scaleX: barScaleX, scaleY: barScaleY }); // Tint health bar with enemy car color enemyHealthBar.tint = enemyColor; enemyHealthBarBgs.push(enemyHealthBarBg); enemyHealthBars.push(enemyHealthBar); } // Create health bars for all initial enemy cars for (var healthBarIdx = 0; healthBarIdx < enemyCars.length; healthBarIdx++) { var enemyCarGraphics = enemyCars[healthBarIdx].children[0]; // Get the car graphics var enemyColor = enemyCarGraphics.tint || 0xffffff; // Get the tint color createEnemyHealthBar(healthBarIdx, enemyColor); } // Create speed display text var speedText = new Text2('Speed: 0', { size: 60, fill: 0x000000 }); speedText.anchor.set(0, 0.5); speedText.x = 50; speedText.y = 2459; // Center vertically in UI area game.addChild(speedText); // Create joystickBG centered in UI background var joystickBG = uiBackground.attachAsset('JoystickBG', { x: 1024, // Center horizontally in UI y: 273, // Center vertically in UI (546/2 = 273) anchorX: 0.5, anchorY: 0.5 }); // Create point object that will follow touch position var point = null; // Create JoystickPoinr that will follow point position smoothly var joystickPoinr = game.attachAsset('JoystickPoinr', { x: 1024, y: 2459, anchorX: 0.5, anchorY: 0.5 }); // Variables for smooth movement var targetX = 1024; var targetY = 2459; var smoothSpeed = 0.2; // Variables for smooth rotation var targetRotation = 0; var baseRotationSpeed = 0.052; // Variables for realistic car physics var currentVelocity = 0; var acceleration = 0.16; var deceleration = 0.44; var maxSpeed = 15.36; // Variables for drift physics var velocityX = 0; var velocityY = 0; var driftFactor = 0.85; // How much momentum is retained (lower = more drift) var gripFactor = 0.3; // How quickly car aligns with direction (lower = more drift) // Player car weight var playerCarWeight = 1.2; // Player car is moderately heavy // Health system variables var maxHealth = 100; // Maximum health for all cars var playerHealth = maxHealth; // Player starts with full health // Damage system variables var minDamageSpeed = 2; // Minimum speed to cause damage var maxDamageSpeed = 12; // Speed at which maximum damage occurs var minDamage = 5; // Minimum damage at low speeds var maxDamage = 25; // Maximum damage at high speeds // Helper function to calculate damage based on speed function calculateSpeedDamage(speed) { if (speed < minDamageSpeed) { return 0; // No damage below minimum speed } // Normalize speed to 0-1 range var speedRatio = Math.min(1, (speed - minDamageSpeed) / (maxDamageSpeed - minDamageSpeed)); // Calculate damage using quadratic curve for more realistic impact var damage = minDamage + (maxDamage - minDamage) * speedRatio * speedRatio; return Math.round(damage); } // Handle touch down - create and show point game.down = function (x, y, obj) { // Create point at touch position point = game.attachAsset('Puntero', { x: x, y: y, anchorX: 0.5, anchorY: 0.5 }); }; // Handle touch move - update point position game.move = function (x, y, obj) { if (point) { point.x = x; point.y = y; // Calculate joystickBG world position var joystickWorldX = joystickBG.x + uiBackground.x; var joystickWorldY = joystickBG.y + uiBackground.y; // Calculate distance from joystick center var deltaX = x - joystickWorldX; var deltaY = y - joystickWorldY; var distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); var maxRadius = joystickBG.width / 2; // Limit movement to joystick radius if (distance > maxRadius) { var angle = Math.atan2(deltaY, deltaX); deltaX = Math.cos(angle) * maxRadius; deltaY = Math.sin(angle) * maxRadius; } // Update target position for smooth movement (constrained) targetX = joystickWorldX + deltaX; targetY = joystickWorldY + deltaY; } }; // Handle touch up - remove point game.up = function (x, y, obj) { if (point) { point.destroy(); point = null; // Reset target position to joystick center var joystickWorldX = joystickBG.x + uiBackground.x; var joystickWorldY = joystickBG.y + uiBackground.y; targetX = joystickWorldX; targetY = joystickWorldY; } }; // Update function for smooth movement game.update = function () { // Smoothly move JoystickPoinr towards target position var deltaX = targetX - joystickPoinr.x; var deltaY = targetY - joystickPoinr.y; joystickPoinr.x += deltaX * smoothSpeed; joystickPoinr.y += deltaY * smoothSpeed; // Double-check that joystickPoinr stays within bounds var joystickWorldX = joystickBG.x + uiBackground.x; var joystickWorldY = joystickBG.y + uiBackground.y; var currentDeltaX = joystickPoinr.x - joystickWorldX; var currentDeltaY = joystickPoinr.y - joystickWorldY; var currentDistance = Math.sqrt(currentDeltaX * currentDeltaX + currentDeltaY * currentDeltaY); var maxRadius = joystickBG.width / 2; if (currentDistance > maxRadius) { var angle = Math.atan2(currentDeltaY, currentDeltaX); joystickPoinr.x = joystickWorldX + Math.cos(angle) * maxRadius; joystickPoinr.y = joystickWorldY + Math.sin(angle) * maxRadius; } // Update car rotation based on joystick position var joystickOffsetX = joystickPoinr.x - joystickWorldX; var joystickOffsetY = joystickPoinr.y - joystickWorldY; var joystickDistance = Math.sqrt(joystickOffsetX * joystickOffsetX + joystickOffsetY * joystickOffsetY); // Calculate power based on distance from center (0 to 1) var power = Math.min(joystickDistance / maxRadius, 1); // Only rotate if joystick is moved significantly from center if (joystickDistance > 10) { var joystickAngle = Math.atan2(joystickOffsetX, -joystickOffsetY); targetRotation = joystickAngle; } // Smoothly interpolate car rotation towards target var rotationDelta = targetRotation - carPlayer.rotation; // Handle angle wrapping for shortest rotation path while (rotationDelta > Math.PI) { rotationDelta -= 2 * Math.PI; } while (rotationDelta < -Math.PI) { rotationDelta += 2 * Math.PI; } // Calculate rotation speed based on current velocity (slower speed = much slower turning) var speedRatio = Math.sqrt(velocityX * velocityX + velocityY * velocityY) / maxSpeed; var dynamicRotationSpeed = baseRotationSpeed * Math.max(0.1, speedRatio); // Minimum 10% rotation speed carPlayer.rotation += rotationDelta * dynamicRotationSpeed; // Calculate target velocity based on joystick power var targetVelocity = maxSpeed * power; // Apply smooth velocity transitions with exponential interpolation if (power > 0.1) { // Accelerating - smooth exponential approach to target velocity var velocityDiff = targetVelocity - currentVelocity; var accelerationRate = 0.004; // Smooth acceleration rate (20% slower for smaller car) currentVelocity += velocityDiff * accelerationRate; } else { // Decelerating when joystick is near center - smooth exponential decay var decelerationRate = 0.048; // Smooth deceleration rate (20% slower for smaller car) currentVelocity *= 1 - decelerationRate; if (Math.abs(currentVelocity) < 0.1) { currentVelocity = 0; } } // Limit velocity to max speed currentVelocity = Math.min(currentVelocity, maxSpeed); // Calculate intended movement direction based on car rotation and current velocity var intendedMoveX = Math.sin(carPlayer.rotation) * currentVelocity; var intendedMoveY = -Math.cos(carPlayer.rotation) * currentVelocity; // Calculate turning friction based on dynamic rotation speed var rotationFriction = Math.abs(rotationDelta * dynamicRotationSpeed) * 0.8; // Reduced friction intensity for smoother feel var frictionMultiplier = Math.max(0.85, 1 - rotationFriction); // Less velocity reduction when turning (min 0.85 for more natural feel) // Apply drift physics - blend current momentum with intended direction velocityX = velocityX * driftFactor + intendedMoveX * gripFactor; velocityY = velocityY * driftFactor + intendedMoveY * gripFactor; // Apply turning friction to reduce velocity when steering velocityX *= frictionMultiplier; velocityY *= frictionMultiplier; // Apply some base deceleration to drift momentum velocityX *= 0.98; velocityY *= 0.98; // Update car position using drift momentum carPlayer.x += velocityX; carPlayer.y += velocityY; // Keep car within gameplay area bounds with realistic collision physics var halfCarWidth = 16; // CarPlayer width is 32, so half is 16 var halfCarHeight = 24; // CarPlayer height is 47.36, so half is ~24 // Calculate current speed for impact calculations var currentSpeed = Math.sqrt(velocityX * velocityX + velocityY * velocityY); var impactThreshold = 2; // Minimum speed to trigger impact effects // Realistic collision physics with energy loss and proper angles if (carPlayer.x < halfCarWidth) { carPlayer.x = halfCarWidth; // Calculate impact intensity based on perpendicular velocity component var impactVelocity = Math.abs(velocityX); var energyLoss = 0.4 + impactVelocity / maxSpeed * 0.3; // More energy loss at higher speeds // Realistic bounce with energy conservation velocityX = -velocityX * (1 - energyLoss); velocityY *= 0.8; // Friction reduces parallel velocity component // Visual feedback for significant impacts if (impactVelocity > impactThreshold) { LK.effects.flashObject(carPlayer, 0xff4444, 200); } } if (carPlayer.x > 2048 - halfCarWidth) { carPlayer.x = 2048 - halfCarWidth; // Calculate impact intensity based on perpendicular velocity component var impactVelocity = Math.abs(velocityX); var energyLoss = 0.4 + impactVelocity / maxSpeed * 0.3; // More energy loss at higher speeds // Realistic bounce with energy conservation velocityX = -velocityX * (1 - energyLoss); velocityY *= 0.8; // Friction reduces parallel velocity component // Visual feedback for significant impacts if (impactVelocity > impactThreshold) { LK.effects.flashObject(carPlayer, 0xff4444, 200); } } if (carPlayer.y < halfCarHeight) { carPlayer.y = halfCarHeight; // Calculate impact intensity based on perpendicular velocity component var impactVelocity = Math.abs(velocityY); var energyLoss = 0.4 + impactVelocity / maxSpeed * 0.3; // More energy loss at higher speeds // Realistic bounce with energy conservation velocityY = -velocityY * (1 - energyLoss); velocityX *= 0.8; // Friction reduces parallel velocity component // Visual feedback for significant impacts if (impactVelocity > impactThreshold) { LK.effects.flashObject(carPlayer, 0xff4444, 200); } } if (carPlayer.y > 2186 - halfCarHeight) { carPlayer.y = 2186 - halfCarHeight; // Calculate impact intensity based on perpendicular velocity component var impactVelocity = Math.abs(velocityY); var energyLoss = 0.4 + impactVelocity / maxSpeed * 0.3; // More energy loss at higher speeds // Realistic bounce with energy conservation velocityY = -velocityY * (1 - energyLoss); velocityX *= 0.8; // Friction reduces parallel velocity component // Visual feedback for significant impacts if (impactVelocity > impactThreshold) { LK.effects.flashObject(carPlayer, 0xff4444, 200); } } // Apply smooth braking to player car after wall collision for more natural deceleration if (!carPlayer.smoothBraking) carPlayer.smoothBraking = false; if (!carPlayer.brakeFrames) carPlayer.brakeFrames = 0; var currentPlayerSpeed = Math.sqrt(velocityX * velocityX + velocityY * velocityY); // If player car was just launched (high velocity), enable smooth braking if (currentPlayerSpeed > maxSpeed * 0.6 && !carPlayer.smoothBraking) { carPlayer.smoothBraking = true; carPlayer.brakeFrames = 0; } // Smooth braking logic: apply a gentle, progressive friction for a short period after wall collision if (carPlayer.smoothBraking) { // Braking lasts for 18 frames (~0.3s at 60fps) var brakeDuration = 18; var brakeProgress = Math.min(1, carPlayer.brakeFrames / brakeDuration); // Start with gentle friction, increase to normal friction var minFriction = 0.96; var maxFriction = 0.92 - currentPlayerSpeed / maxSpeed * 0.05; var frictionRate = minFriction + (maxFriction - minFriction) * brakeProgress; velocityX *= frictionRate; velocityY *= frictionRate; carPlayer.brakeFrames++; if (carPlayer.brakeFrames >= brakeDuration) { carPlayer.smoothBraking = false; } } else if (currentPlayerSpeed > 0.1) { // Normal progressive friction when not actively controlling if (power <= 0.1) { // Only apply extra friction when not accelerating var frictionRate = 0.92 - currentPlayerSpeed / maxSpeed * 0.05; // More friction at higher speeds velocityX *= frictionRate; velocityY *= frictionRate; } } else { // Stop very slow movement to prevent endless drift if (power <= 0.1) { velocityX = 0; velocityY = 0; } } // Particle system for car exhaust (global, supports all cars) if (!game.particles) { game.particles = []; } // Helper function to emit particles for any car function emitCarParticles(car, vx, vy, rotation, maxSpeed, gameplayBackground) { var totalSpeed = Math.sqrt(vx * vx + vy * vy); var speedRatio = totalSpeed / maxSpeed; var particleFrequency = Math.max(1, Math.floor(8 - speedRatio * 6)); if (speedRatio > 0.05 && LK.ticks % particleFrequency === 0) { // Calculate particle spawn position behind the car var particleSpawnX = car.x - Math.sin(rotation) * 55; var particleSpawnY = car.y + Math.cos(rotation) * 55; var particleCount = Math.max(1, Math.floor(speedRatio * 3)); for (var p = 0; p < particleCount; p++) { var newParticle = new Particle(); newParticle.x = particleSpawnX + (Math.random() - 0.5) * 19.2; newParticle.y = particleSpawnY + (Math.random() - 0.5) * 19.2; newParticle.velocityX += -vx * 0.12 + (Math.random() - 0.5) * 2.4; newParticle.velocityY += -vy * 0.12 + (Math.random() - 0.5) * 2.4; game.particles.push(newParticle); gameplayBackground.addChild(newParticle); // Tween particle color for variety var colors = [0xffffff, 0xcccccc, 0x999999, 0x666666]; var randomColor = colors[Math.floor(Math.random() * colors.length)]; tween(newParticle.children[0], { tint: randomColor }, { duration: 100 }); } } } // Optimize particle emissions - reduce frequency if (LK.ticks % 2 === 0) { // Only emit particles every other frame // Emit particles for player car emitCarParticles(carPlayer, velocityX, velocityY, carPlayer.rotation, maxSpeed, gameplayBackground); // Emit particles for all enemy cars for (var carIdx = 0; carIdx < enemyCars.length; carIdx++) { var currentEnemyCar = enemyCars[carIdx]; var enemyTotalVX = (currentEnemyCar.aiVelocityX || 0) + (currentEnemyCar.velocityX || 0); var enemyTotalVY = (currentEnemyCar.aiVelocityY || 0) + (currentEnemyCar.velocityY || 0); emitCarParticles(currentEnemyCar, enemyTotalVX, enemyTotalVY, currentEnemyCar.rotation, maxSpeed, gameplayBackground); } } // Update and clean up particles (batch process) var particlesToRemove = []; for (var i = 0; i < game.particles.length; i++) { var particle = game.particles[i]; if (particle.age >= particle.lifespan) { particlesToRemove.push(i); } } // Remove particles in reverse order to maintain indices for (var r = particlesToRemove.length - 1; r >= 0; r--) { var idx = particlesToRemove[r]; game.particles[idx].destroy(); game.particles.splice(idx, 1); } // Check collision between player car and all enemy cars using smaller collision boxes (optimized) if (!carPlayer.lastColliding) carPlayer.lastColliding = []; // Initialize collision tracking array for all cars while (carPlayer.lastColliding.length < enemyCars.length) { carPlayer.lastColliding.push(false); } // Define smaller collision boxes (approximately 60% of actual asset size for more precise collision) - cache constants var playerCollisionWidth = 38; // Reduced from 64px width var playerCollisionHeight = 57; // Reduced from ~95px height var enemyCollisionWidth = 38; // Reduced from 64px width var enemyCollisionHeight = 57; // Reduced from ~94px height var halfPlayerWidth = playerCollisionWidth / 2; var halfPlayerHeight = playerCollisionHeight / 2; var halfEnemyWidth = enemyCollisionWidth / 2; var halfEnemyHeight = enemyCollisionHeight / 2; var playerLeft = carPlayer.x - halfPlayerWidth; var playerRight = carPlayer.x + halfPlayerWidth; var playerTop = carPlayer.y - halfPlayerHeight; var playerBottom = carPlayer.y + halfPlayerHeight; // Check collision with each enemy car for (var enemyIdx = 0; enemyIdx < enemyCars.length; enemyIdx++) { var currentEnemyCar = enemyCars[enemyIdx]; var enemyLeft = currentEnemyCar.x - halfEnemyWidth; var enemyRight = currentEnemyCar.x + halfEnemyWidth; var enemyTop = currentEnemyCar.y - halfEnemyHeight; var enemyBottom = currentEnemyCar.y + halfEnemyHeight; // More precise collision detection using smaller bounding boxes var currentColliding = !(playerRight < enemyLeft || playerLeft > enemyRight || playerBottom < enemyTop || playerTop > enemyBottom); if (!carPlayer.lastColliding[enemyIdx] && currentColliding) { // Collision just started - determine collision responsibility var playerSpeed = Math.sqrt(velocityX * velocityX + velocityY * velocityY); var enemySpeed = Math.sqrt(currentEnemyCar.velocityX * currentEnemyCar.velocityX + currentEnemyCar.velocityY * currentEnemyCar.velocityY); // Calculate collision direction (from player car to enemy car) var collisionDeltaX = currentEnemyCar.x - carPlayer.x; var collisionDeltaY = currentEnemyCar.y - carPlayer.y; var collisionDistance = Math.sqrt(collisionDeltaX * collisionDeltaX + collisionDeltaY * collisionDeltaY); // Normalize collision direction if (collisionDistance > 0) { collisionDeltaX /= collisionDistance; collisionDeltaY /= collisionDistance; } // Determine collision responsibility based on velocity vectors and approach angles var playerCrasher = false; var enemyCrasher = false; var bothCrashed = false; // Calculate approach vectors (movement direction toward collision point) var playerApproachX = 0, playerApproachY = 0; var enemyApproachX = 0, enemyApproachY = 0; if (playerSpeed > 0.1) { // Normalize player velocity to get approach direction playerApproachX = velocityX / playerSpeed; playerApproachY = velocityY / playerSpeed; } if (enemySpeed > 0.1) { // Normalize enemy velocity to get approach direction var enemyTotalVelX = (currentEnemyCar.aiVelocityX || 0) + currentEnemyCar.velocityX; var enemyTotalVelY = (currentEnemyCar.aiVelocityY || 0) + currentEnemyCar.velocityY; var enemyTotalSpeed = Math.sqrt(enemyTotalVelX * enemyTotalVelX + enemyTotalVelY * enemyTotalVelY); if (enemyTotalSpeed > 0.1) { enemyApproachX = enemyTotalVelX / enemyTotalSpeed; enemyApproachY = enemyTotalVelY / enemyTotalSpeed; } } // Calculate how much each car is moving toward the collision var playerTowardCollision = 0; var enemyTowardCollision = 0; if (playerSpeed > 0.1) { // Dot product of player movement with collision direction playerTowardCollision = playerApproachX * collisionDeltaX + playerApproachY * collisionDeltaY; } if (enemySpeed > 0.1) { // Dot product of enemy movement with opposite collision direction (toward player) enemyTowardCollision = enemyApproachX * -collisionDeltaX + enemyApproachY * -collisionDeltaY; } // Speed thresholds for determining crasher responsibility var minCrasherSpeed = maxSpeed * 0.15; // 15% of max speed minimum to be considered crasher var dominantCrasherThreshold = 0.3; // How much more one car must be approaching to be sole crasher // Determine responsibility based on approach analysis if (playerSpeed > minCrasherSpeed && enemySpeed > minCrasherSpeed) { // Both cars moving significantly if (playerTowardCollision > dominantCrasherThreshold && enemyTowardCollision > dominantCrasherThreshold) { // Both approaching collision point - head-on or mutual collision bothCrashed = true; } else if (playerTowardCollision > enemyTowardCollision + dominantCrasherThreshold) { // Player moving much more toward collision playerCrasher = true; } else if (enemyTowardCollision > playerTowardCollision + dominantCrasherThreshold) { // Enemy moving much more toward collision enemyCrasher = true; } else { // Similar approach - treat as mutual collision bothCrashed = true; } } else if (playerSpeed > minCrasherSpeed && enemySpeed <= minCrasherSpeed) { // Only player moving significantly - player is crasher playerCrasher = true; } else if (enemySpeed > minCrasherSpeed && playerSpeed <= minCrasherSpeed) { // Only enemy moving significantly - enemy is crasher enemyCrasher = true; } else { // Both moving very slowly - treat as mutual collision bothCrashed = true; } // Calculate momentum transfer using conservation of momentum var totalMass = playerCarWeight + currentEnemyCar.weight; var massRatio1 = (playerCarWeight - currentEnemyCar.weight) / totalMass; var massRatio2 = 2 * currentEnemyCar.weight / totalMass; var massRatio3 = 2 * playerCarWeight / totalMass; var massRatio4 = (currentEnemyCar.weight - playerCarWeight) / totalMass; // Apply different energy loss based on collision responsibility var relativeSpeed = Math.max(playerSpeed, enemySpeed); var minLoss, maxLoss; var speedNorm = Math.min(1, relativeSpeed / maxSpeed); // 0 to 1 if (bothCrashed) { // Mutual collision - both get significant energy loss minLoss = 0.22; // Higher base energy loss for mutual crashes maxLoss = 0.70; // Higher max energy loss for mutual crashes } else { // Single crasher scenario - crasher gets less penalty minLoss = 0.15; // Lower base energy loss maxLoss = 0.60; // Lower max energy loss } var energyLoss = minLoss + (maxLoss - minLoss) * speedNorm; var restitution = 1 - energyLoss; // Apply different restitution based on responsibility var playerRestitution = restitution; var enemyRestitution = restitution; if (playerCrasher) { // Player is crasher - gets less penalty (keeps more momentum) playerRestitution = restitution * 1.15; // 15% less energy loss enemyRestitution = restitution * 0.85; // 15% more energy loss for crashed car } else if (enemyCrasher) { // Enemy is crasher - gets less penalty enemyRestitution = restitution * 1.15; // 15% less energy loss playerRestitution = restitution * 0.85; // 15% more energy loss for crashed car } // For bothCrashed, both use same restitution // Separate cars to prevent overlap var separationDistance = 70; carPlayer.x = currentEnemyCar.x - collisionDeltaX * separationDistance; carPlayer.y = currentEnemyCar.y - collisionDeltaY * separationDistance; // Calculate new velocities based on mass and current motion var playerImpactX = velocityX * massRatio1 + currentEnemyCar.velocityX * massRatio2; var playerImpactY = velocityY * massRatio1 + currentEnemyCar.velocityY * massRatio2; velocityX = playerImpactX * playerRestitution; velocityY = playerImpactY * playerRestitution; // Enemy car velocity change var enemyImpactX = velocityX * massRatio3 + currentEnemyCar.velocityX * massRatio4; var enemyImpactY = velocityY * massRatio3 + currentEnemyCar.velocityY * massRatio4; currentEnemyCar.velocityX = enemyImpactX * enemyRestitution; currentEnemyCar.velocityY = enemyImpactY * enemyRestitution; // Launch enemy car away from collision var launchSpeed = Math.max(playerSpeed, enemySpeed); var minPush = 0.4; var maxPush = 1.5; var pushMultiplier = minPush + (maxPush - minPush) * speedNorm; var launchDirX = currentEnemyCar.x - carPlayer.x; var launchDirY = currentEnemyCar.y - carPlayer.y; var launchDist = Math.sqrt(launchDirX * launchDirX + launchDirY * launchDirY); if (launchDist < 0.01) { var playerMoveNorm = Math.sqrt(velocityX * velocityX + velocityY * velocityY); if (playerMoveNorm > 0.01) { launchDirX = velocityX / playerMoveNorm; launchDirY = velocityY / playerMoveNorm; } else { launchDirX = 0; launchDirY = -1; } } else { launchDirX /= launchDist; launchDirY /= launchDist; } currentEnemyCar.velocityX += launchDirX * launchSpeed * pushMultiplier; currentEnemyCar.velocityY += launchDirY * launchSpeed * pushMultiplier; // Add directional push based on collision angle and relative mass var pushIntensity = (playerSpeed + enemySpeed) * (0.5 + 0.5 * speedNorm); var playerPushRatio = currentEnemyCar.weight / totalMass; var enemyPushRatio = playerCarWeight / totalMass; velocityX += -collisionDeltaX * pushIntensity * playerPushRatio * 0.7; velocityY += -collisionDeltaY * pushIntensity * playerPushRatio * 0.7; currentEnemyCar.velocityX += collisionDeltaX * pushIntensity * enemyPushRatio * 0.7; currentEnemyCar.velocityY += collisionDeltaY * pushIntensity * enemyPushRatio * 0.7; // Reset acceleration and velocity state based on responsibility if (playerCrasher) { // Player is crasher - smaller penalty to acceleration currentVelocity *= 0.3; // Keep 30% of acceleration state } else { // Player crashed into or mutual - full reset currentVelocity = 0; // Reset player's acceleration state to restart from zero } if (enemyCrasher) { // Enemy is crasher - smaller penalty to acceleration if (typeof currentEnemyCar.aiCurrentVelocity !== "undefined") { currentEnemyCar.aiCurrentVelocity *= 0.3; // Keep 30% of AI acceleration state } } else { // Enemy crashed into or mutual - full reset if (typeof currentEnemyCar.aiCurrentVelocity !== "undefined") { currentEnemyCar.aiCurrentVelocity = 0; // Reset enemy's AI acceleration state } } // Calculate speed-based damage var collisionSpeed = Math.max(playerSpeed, enemySpeed); var baseDamage = calculateSpeedDamage(collisionSpeed); // Apply damage with responsibility-based penalties var playerDamage = 0; var enemyDamage = 0; if (bothCrashed) { // Both crashed - equal damage playerDamage = baseDamage; enemyDamage = baseDamage; } else if (playerCrasher) { // Player is crasher - takes less damage playerDamage = Math.round(baseDamage * 0.7); // 30% less damage enemyDamage = Math.round(baseDamage * 1.3); // 30% more damage } else if (enemyCrasher) { // Enemy is crasher - takes less damage enemyDamage = Math.round(baseDamage * 0.7); // 30% less damage playerDamage = Math.round(baseDamage * 1.3); // 30% more damage } else { // Fallback - equal damage playerDamage = baseDamage; enemyDamage = baseDamage; } // Apply damage to player if (playerDamage > 0) { playerHealth = Math.max(0, playerHealth - playerDamage); // Check for player death if (playerHealth <= 0) { LK.showGameOver(); return; // Exit update loop } } // Apply damage to enemy if (enemyDamage > 0) { currentEnemyCar.health = Math.max(0, currentEnemyCar.health - enemyDamage); // Check for enemy death if (currentEnemyCar.health <= 0) { // Remove enemy from game currentEnemyCar.destroy(); enemyCars.splice(enemyIdx, 1); carPlayer.lastColliding.splice(enemyIdx, 1); // Remove corresponding health bar if (enemyIdx < enemyHealthBars.length) { enemyHealthBars[enemyIdx].destroy(); enemyHealthBarBgs[enemyIdx].destroy(); enemyHealthBars.splice(enemyIdx, 1); enemyHealthBarBgs.splice(enemyIdx, 1); // Reposition remaining health bars for (var repositionIdx = enemyIdx; repositionIdx < enemyHealthBars.length; repositionIdx++) { enemyHealthBars[repositionIdx].y = 80 + repositionIdx * 35; enemyHealthBarBgs[repositionIdx].y = 80 + repositionIdx * 35; } } // Check for victory condition if (enemyCars.length === 0) { LK.showYouWin(); return; // Exit update loop } continue; // Skip visual feedback for destroyed enemy } } // Visual feedback based on collision responsibility if (bothCrashed) { // Both crashed - flash both in orange LK.effects.flashObject(carPlayer, 0xff8800, 500); LK.effects.flashObject(currentEnemyCar, 0xff8800, 500); } else if (playerCrasher) { // Player crashed into enemy - flash player red, enemy yellow LK.effects.flashObject(carPlayer, 0xff4400, 400); LK.effects.flashObject(currentEnemyCar, 0xffff00, 600); } else if (enemyCrasher) { // Enemy crashed into player - flash enemy red, player yellow LK.effects.flashObject(currentEnemyCar, 0xff4400, 400); LK.effects.flashObject(carPlayer, 0xffff00, 600); } else { // Default fallback LK.effects.flashObject(carPlayer, 0xff0000, 500); } } // Update last collision state for this enemy carPlayer.lastColliding[enemyIdx] = currentColliding; } // --- Enemy Cars AI Movement Logic (Optimized) --- // Process AI for each enemy car - stagger AI updates to reduce load var aiUpdateOffset = LK.ticks % enemyCars.length; // Stagger AI updates for (var aiCarIdx = 0; aiCarIdx < enemyCars.length; aiCarIdx++) { var enemyCar = enemyCars[aiCarIdx]; // Only update AI for one car per frame (staggered) var shouldUpdateAI = aiCarIdx === aiUpdateOffset; // Dynamic target selection system - choose best target based on position and velocity if (typeof enemyCar.currentTarget === "undefined") enemyCar.currentTarget = null; if (typeof enemyCar.targetSelectionTimer === "undefined") enemyCar.targetSelectionTimer = 0; if (typeof enemyCar.targetSelectionInterval === "undefined") enemyCar.targetSelectionInterval = 60; // Re-evaluate every second // Pursuit detection system - track who is following this AI car if (typeof enemyCar.pursuitDetection === "undefined") { enemyCar.pursuitDetection = { pursuers: [], // List of potential pursuers with tracking data beingPursued: false, pursuitIntensity: 0, // 0-1 scale of how intensely being pursued pursuitDuration: 0, // How long being pursued in frames lastPursuitCheck: 0, evasionMode: false, // Whether currently evading evasionStrategy: 0, // 0=speed, 1=zigzag, 2=circles evasionTimer: 0, evasionDuration: 0, counterAttackMode: false, // Whether to turn and fight counterAttackTarget: null }; } // Re-evaluate target selection periodically (only for the current AI car being updated) if (shouldUpdateAI) { enemyCar.targetSelectionTimer++; // Pursuit detection - analyze if other cars are targeting this one if (LK.ticks % 10 === 0) { // Check every 10 frames for performance enemyCar.pursuitDetection.pursuers = []; // Check all potential pursuers (player + other AI cars) var potentialPursuers = []; // Add player as potential pursuer potentialPursuers.push({ object: carPlayer, velocityX: velocityX, velocityY: velocityY, isPlayer: true }); // Add other enemy cars as potential pursuers for (var pursuerId = 0; pursuerId < enemyCars.length; pursuerId++) { if (pursuerId !== aiCarIdx) { var otherCar = enemyCars[pursuerId]; potentialPursuers.push({ object: otherCar, velocityX: (otherCar.aiVelocityX || 0) + (otherCar.velocityX || 0), velocityY: (otherCar.aiVelocityY || 0) + (otherCar.velocityY || 0), isPlayer: false }); } } // Analyze each potential pursuer for (var pIdx = 0; pIdx < potentialPursuers.length; pIdx++) { var pursuer = potentialPursuers[pIdx]; var pursuerId = pIdx; // Calculate distance and relative positioning var pursuerDist = Math.sqrt((pursuer.object.x - enemyCar.x) * (pursuer.object.x - enemyCar.x) + (pursuer.object.y - enemyCar.y) * (pursuer.object.y - enemyCar.y)); // Only consider pursuers within reasonable range if (pursuerDist < 500 && pursuerDist > 50) { // Calculate if pursuer is moving toward this car var toThisCar = { x: enemyCar.x - pursuer.object.x, y: enemyCar.y - pursuer.object.y }; var toThisCarMag = Math.sqrt(toThisCar.x * toThisCar.x + toThisCar.y * toThisCar.y); if (toThisCarMag > 0) { toThisCar.x /= toThisCarMag; toThisCar.y /= toThisCarMag; // Calculate pursuer's velocity direction var pursuerSpeed = Math.sqrt(pursuer.velocityX * pursuer.velocityX + pursuer.velocityY * pursuer.velocityY); if (pursuerSpeed > 1) { var pursuerDir = { x: pursuer.velocityX / pursuerSpeed, y: pursuer.velocityY / pursuerSpeed }; // Dot product to see if pursuer is moving toward this car var alignment = toThisCar.x * pursuerDir.x + toThisCar.y * pursuerDir.y; // Consider as pursuer if moving toward this car and close enough if (alignment > 0.3) { // 30 degree cone var pursuitScore = alignment * (1 - pursuerDist / 500) * (pursuerSpeed / maxSpeed); // Additional checks for AI pursuers - see if they're targeting this car if (!pursuer.isPlayer && pursuer.object.currentTarget) { if (pursuer.object.currentTarget.object === enemyCar) { pursuitScore += 0.4; // Bonus if we're their target } } if (pursuitScore > 0.3) { enemyCar.pursuitDetection.pursuers.push({ object: pursuer.object, distance: pursuerDist, score: pursuitScore, isPlayer: pursuer.isPlayer }); } } } } } } // Determine if being pursued based on pursuer analysis var totalPursuitScore = 0; var closestPursuerDist = Infinity; var strongestPursuer = null; for (var p = 0; p < enemyCar.pursuitDetection.pursuers.length; p++) { var pursuerData = enemyCar.pursuitDetection.pursuers[p]; totalPursuitScore += pursuerData.score; if (pursuerData.distance < closestPursuerDist) { closestPursuerDist = pursuerData.distance; strongestPursuer = pursuerData; } } // Update pursuit state var wasPursued = enemyCar.pursuitDetection.beingPursued; enemyCar.pursuitDetection.beingPursued = totalPursuitScore > 0.5 && enemyCar.pursuitDetection.pursuers.length > 0; enemyCar.pursuitDetection.pursuitIntensity = Math.min(1, totalPursuitScore); if (enemyCar.pursuitDetection.beingPursued) { enemyCar.pursuitDetection.pursuitDuration++; // Decide on response strategy when first detected or strategy expired if (!wasPursued || enemyCar.pursuitDetection.evasionTimer > enemyCar.pursuitDetection.evasionDuration) { var personality = enemyCar.tacticalPersonality; // Decision factors var currentSpeed = Math.sqrt((enemyCar.aiVelocityX || 0) * (enemyCar.aiVelocityX || 0) + (enemyCar.aiVelocityY || 0) * (enemyCar.aiVelocityY || 0)); var speedRatio = currentSpeed / maxSpeed; var pursuerThreat = enemyCar.pursuitDetection.pursuitIntensity; // Personality-based decision making var fleeChance = 0.6 + personality.patience * 0.2 - personality.aggressiveness * 0.3; var counterAttackChance = 0.4 + personality.aggressiveness * 0.4 - personality.patience * 0.2; // Situational modifiers if (speedRatio > 0.7) fleeChance += 0.2; // Easier to flee when fast if (closestPursuerDist < 150) counterAttackChance += 0.3; // More likely to fight when cornered if (pursuerThreat > 0.8) counterAttackChance += 0.2; // Fight when heavily pursued // Individual flight decision based on personality traits and health var flightPrefs = enemyCar.tacticalPersonality; var shouldFlee = false; var shouldCounterAttack = false; // Health-based behavioral modifications var healthRatio = enemyCar.health / maxHealth; // 0-1, where 1 is full health var healthAggressionModifier = (healthRatio - 0.5) * flightPrefs.healthAggressionBonus; // +aggressive when healthy, -aggressive when hurt var healthFlightModifier = (0.5 - healthRatio) * flightPrefs.healthFlightBonus; // +flight when hurt, -flight when healthy // Update dynamic personality based on current health flightPrefs.aggressiveness = Math.max(0.1, Math.min(1.0, flightPrefs.baseAggressiveness + healthAggressionModifier)); flightPrefs.flightTendency = Math.max(0.1, Math.min(0.9, flightPrefs.baseFlightTendency + healthFlightModifier)); // Calculate individual flight decision factors var baseFlightChance = flightPrefs.flightTendency; var baseCounterChance = flightPrefs.counterAttackChance; // Health-based modifiers for decision making var healthFlightBonus = healthRatio < 0.3 ? 0.4 : healthRatio < 0.6 ? 0.2 : 0; // More likely to flee when very hurt var healthCounterBonus = healthRatio > 0.7 ? 0.3 : healthRatio > 0.4 ? 0.1 : 0; // More likely to fight when healthy // Modify chances based on individual traits var braveryBonus = flightPrefs.bravery * 0.3; // Brave cars more likely to fight var panicPenalty = pursuerThreat > flightPrefs.panicThreshold ? 0.2 : 0; // Panic reduces counter-attack var riskFactor = (1 - flightPrefs.riskTolerance) * 0.25; // Risk-averse cars flee more // Apply individual modifiers including health-based changes var adjustedFlightChance = baseFlightChance + riskFactor + panicPenalty + healthFlightBonus - braveryBonus * 0.5; var adjustedCounterChance = baseCounterChance + braveryBonus + healthCounterBonus - panicPenalty - riskFactor; // Situational modifiers based on individual thresholds if (speedRatio > 0.7 && speedRatio > flightPrefs.riskTolerance) { adjustedFlightChance += 0.2; // Fast cars with low risk tolerance flee more } if (closestPursuerDist < flightPrefs.flightDistance) { if (flightPrefs.bravery > 0.6) { adjustedCounterChance += 0.3; // Brave cars fight when cornered } else { adjustedFlightChance += 0.2; // Others flee when too close } } if (pursuerThreat > 0.8 && flightPrefs.panicThreshold < 0.5) { adjustedFlightChance += 0.3; // Easy-to-panic cars flee under heavy pursuit } // Make individual decision if (Math.random() < Math.max(0.1, Math.min(0.9, adjustedCounterChance))) { // Turn and fight enemyCar.pursuitDetection.counterAttackMode = true; enemyCar.pursuitDetection.counterAttackTarget = strongestPursuer ? strongestPursuer.object : null; enemyCar.pursuitDetection.evasionMode = false; enemyCar.pursuitDetection.evasionDuration = 120 + Math.floor(Math.random() * 180); // 2-5 seconds } else { // Flee - choose evasion strategy based on individual preferences enemyCar.pursuitDetection.evasionMode = true; enemyCar.pursuitDetection.counterAttackMode = false; // Individual evasion strategy selection based on preferences and situation var strategyScores = [flightPrefs.preferSpeedEscape, flightPrefs.preferZigzagEscape, flightPrefs.preferCircularEscape]; // Situational bonuses based on individual traits if (speedRatio < 0.4) { strategyScores[0] += 0.3; // Speed strategy bonus when slow } else if (speedRatio > 0.7) { strategyScores[1] += 0.2; // Zigzag bonus when fast (harder to predict) strategyScores[2] += 0.15; } if (closestPursuerDist < flightPrefs.flightDistance * 0.7) { strategyScores[1] += 0.25; // Zigzag when close strategyScores[2] += 0.3; // Circles when close } else if (closestPursuerDist > flightPrefs.flightDistance * 1.5) { strategyScores[0] += 0.4; // Speed when far } // Individual trait bonuses if (flightPrefs.adaptability > 0.7) { strategyScores[1] += 0.2; // Adaptive cars prefer zigzag } if (flightPrefs.patience > 0.6) { strategyScores[2] += 0.2; // Patient cars can handle circles } if (flightPrefs.aggressiveness > 0.6) { strategyScores[0] += 0.15; // Aggressive cars prefer speed } // Select strategy with highest score var maxScore = Math.max(strategyScores[0], strategyScores[1], strategyScores[2]); var bestStrategies = []; for (var si = 0; si < 3; si++) { if (strategyScores[si] >= maxScore - 0.1) { bestStrategies.push(si); } } enemyCar.pursuitDetection.evasionStrategy = bestStrategies[Math.floor(Math.random() * bestStrategies.length)]; // Individual evasion duration based on personality var baseDuration = Math.floor(flightPrefs.maxFlightDuration * 0.6); // 60% of max var personalityDuration = Math.floor(flightPrefs.maxFlightDuration * 0.4 * flightPrefs.patience); // Patience affects duration enemyCar.pursuitDetection.evasionDuration = baseDuration + personalityDuration; } enemyCar.pursuitDetection.evasionTimer = 0; } } else { enemyCar.pursuitDetection.pursuitDuration = 0; enemyCar.pursuitDetection.evasionMode = false; enemyCar.pursuitDetection.counterAttackMode = false; } } enemyCar.pursuitDetection.evasionTimer++; } if ((enemyCar.targetSelectionTimer >= enemyCar.targetSelectionInterval || enemyCar.currentTarget === null) && shouldUpdateAI) { enemyCar.targetSelectionTimer = 0; // Create list of all potential targets (player + other enemy cars) var potentialTargets = []; // Add player as potential target potentialTargets.push({ object: carPlayer, velocityX: velocityX, velocityY: velocityY, weight: playerCarWeight, isPlayer: true }); // Add other enemy cars as potential targets for (var targetIdx = 0; targetIdx < enemyCars.length; targetIdx++) { if (targetIdx !== aiCarIdx) { // Don't target self var otherCar = enemyCars[targetIdx]; potentialTargets.push({ object: otherCar, velocityX: (otherCar.aiVelocityX || 0) + (otherCar.velocityX || 0), velocityY: (otherCar.aiVelocityY || 0) + (otherCar.velocityY || 0), weight: otherCar.weight, isPlayer: false }); } } // Evaluate each target and assign scores var bestTarget = null; var bestScore = -1; for (var evalIdx = 0; evalIdx < potentialTargets.length; evalIdx++) { var target = potentialTargets[evalIdx]; var targetSpeed = Math.sqrt(target.velocityX * target.velocityX + target.velocityY * target.velocityY); var targetDistance = Math.sqrt((target.object.x - enemyCar.x) * (target.object.x - enemyCar.x) + (target.object.y - enemyCar.y) * (target.object.y - enemyCar.y)); // Calculate target score based on multiple factors var score = 0; // --- Enhanced: Health factor with camper behavior --- var targetHealth = 100; if (typeof target.object.health !== "undefined") { targetHealth = target.object.health; } else if (target.isPlayer && typeof playerHealth !== "undefined") { targetHealth = playerHealth; } // --- New: Anti-grouping and isolation preference system --- var isolationScore = 0; var clusterPenalty = 0; // Count nearby cars around this target var nearbyCarCount = 0; var clusterRadius = enemyCar.tacticalPersonality.clusterAvoidanceRadius; // Check distance to all other potential targets around this target for (var clusterCheck = 0; clusterCheck < potentialTargets.length; clusterCheck++) { if (clusterCheck !== evalIdx) { var otherTarget = potentialTargets[clusterCheck]; var distanceToOther = Math.sqrt((target.object.x - otherTarget.object.x) * (target.object.x - otherTarget.object.x) + (target.object.y - otherTarget.object.y) * (target.object.y - otherTarget.object.y)); if (distanceToOther < clusterRadius) { nearbyCarCount++; } } } // Calculate isolation bonus - prefer targets that are alone if (nearbyCarCount === 0) { // Target is completely isolated - major bonus isolationScore = enemyCar.tacticalPersonality.isolationSeekingLevel * 25; } else if (nearbyCarCount === 1) { // Target has one nearby car - moderate bonus isolationScore = enemyCar.tacticalPersonality.isolationSeekingLevel * 15; } else { // Target is in a cluster - potential penalty isolationScore = 0; } // Calculate cluster avoidance penalty if (nearbyCarCount > enemyCar.tacticalPersonality.maxClusterTolerance) { // Too many cars clustered around target - apply penalty var excessCars = nearbyCarCount - enemyCar.tacticalPersonality.maxClusterTolerance; clusterPenalty = enemyCar.tacticalPersonality.antiGroupingPreference * excessCars * 20; } // Apply isolation bonus and cluster penalty score += isolationScore; score -= clusterPenalty; // Base health scoring - prefer weaker targets var healthScore = Math.max(0, 30 - targetHealth / maxHealth * 30); // 0-30 points, lower health = higher score // Camper behavior modification if (enemyCar.tacticalPersonality.isCamper) { var targetHealthRatio = targetHealth / maxHealth; var myHealthRatio = enemyCar.health / maxHealth; var campingThreshold = enemyCar.tacticalPersonality.campingThreshold; // Campers are very selective - only attack when conditions are right if (myHealthRatio > campingThreshold && targetHealthRatio < 0.7) { // Camper is healthy and target is weakened - major bonus for opportunistic attack healthScore += enemyCar.tacticalPersonality.opportunismLevel * 40; // Up to 40 bonus points } else if (myHealthRatio < campingThreshold) { // Camper is hurt - reduce target priority significantly unless target is very weak if (targetHealthRatio > 0.3) { healthScore *= 0.3; // Reduce priority for healthy targets when camper is hurt } } else if (targetHealthRatio > 0.8) { // Target is too healthy for camper's comfort - major penalty healthScore *= 0.1; // Almost ignore healthy targets } } else { // Non-camper behavior - health-based aggression affects targeting var myHealthRatio = enemyCar.health / maxHealth; var aggressionModifier = enemyCar.tacticalPersonality.aggressiveness; if (myHealthRatio > 0.7 && aggressionModifier > 0.6) { // Healthy and aggressive - willing to attack stronger targets healthScore *= 0.8; // Slight reduction in health preference } else if (myHealthRatio < 0.4) { // Hurt - strongly prefer weak targets healthScore *= 1.5; // Increase health preference } } score += healthScore; // Distance factor - prefer closer targets (0-30 points, slightly reduced to balance health) var distanceScore = Math.max(0, 30 - targetDistance / 1000 * 30); score += distanceScore; // Speed factor - prefer faster targets for more exciting gameplay (0-20 points) var speedRatio = targetSpeed / maxSpeed; var speedScore = speedRatio * 20; score += speedScore; // Weight factor - heavier targets are more satisfying to hit (0-10 points) var weightScore = Math.min(10, target.weight * 4); score += weightScore; // Player bonus - slight preference for player to maintain engagement (0-10 points) if (target.isPlayer) { score += 10; } // --- New: Distance to preferred attack range (prefer targets at optimal distance) --- var preferredDist = enemyCar.tacticalPersonality ? enemyCar.tacticalPersonality.preferredAttackDistance || 200 : 200; var distToPreferred = Math.abs(targetDistance - preferredDist); var optimalDistScore = Math.max(0, 10 - distToPreferred / 400 * 10); // 0-10 points, closer to preferred = higher score += optimalDistScore; // Velocity alignment factor - prefer targets moving in interesting directions (0-10 points) if (targetSpeed > 1) { var enemySpeed = Math.sqrt((enemyCar.aiVelocityX || 0) * (enemyCar.aiVelocityX || 0) + (enemyCar.aiVelocityY || 0) * (enemyCar.aiVelocityY || 0)); if (enemySpeed > 1) { // Calculate if target and enemy are moving in similar or opposite directions var enemyVelX = enemyCar.aiVelocityX || 0; var enemyVelY = enemyCar.aiVelocityY || 0; var dotProduct = (target.velocityX * enemyVelX + target.velocityY * enemyVelY) / (targetSpeed * enemySpeed); // Prefer head-on collisions (opposite directions) - more exciting var alignmentScore = (1 - dotProduct) * 5; // 0-10 points score += alignmentScore; } } // Avoid recently targeted objects to prevent fixation (penalty) if (enemyCar.currentTarget && target.object === enemyCar.currentTarget.object) { // Apply small penalty to current target to encourage switching score *= 0.9; } // Select best target if (score > bestScore) { bestScore = score; bestTarget = target; } } // Update current target enemyCar.currentTarget = bestTarget; // Vary target selection interval for more dynamic behavior enemyCar.targetSelectionInterval = 45 + Math.floor(Math.random() * 90); // 0.75-2.25 seconds } // Enemy car can switch between 'follow target' and 'free roam' AI modes if (typeof enemyCar.aiMode === "undefined") enemyCar.aiMode = "follow"; if (typeof enemyCar.aiModeTimer === "undefined") enemyCar.aiModeTimer = 0; if (typeof enemyCar.aiModeDuration === "undefined") enemyCar.aiModeDuration = 0; // AI mode switching logic: change mode at intervals with speed-based preferences enemyCar.aiModeTimer++; if (enemyCar.aiModeTimer > enemyCar.aiModeDuration) { // Check if being pursued - this overrides normal mode selection if (enemyCar.pursuitDetection.beingPursued && (enemyCar.pursuitDetection.evasionMode || enemyCar.pursuitDetection.counterAttackMode)) { // Being pursued - use special pursuit response mode if (enemyCar.pursuitDetection.counterAttackMode && enemyCar.pursuitDetection.counterAttackTarget) { // Turn and fight - target the pursuer enemyCar.aiMode = "follow"; // Override current target to pursuer enemyCar.currentTarget = { object: enemyCar.pursuitDetection.counterAttackTarget, velocityX: enemyCar.pursuitDetection.counterAttackTarget === carPlayer ? velocityX : (enemyCar.pursuitDetection.counterAttackTarget.aiVelocityX || 0) + (enemyCar.pursuitDetection.counterAttackTarget.velocityX || 0), velocityY: enemyCar.pursuitDetection.counterAttackTarget === carPlayer ? velocityY : (enemyCar.pursuitDetection.counterAttackTarget.aiVelocityY || 0) + (enemyCar.pursuitDetection.counterAttackTarget.velocityY || 0), weight: enemyCar.pursuitDetection.counterAttackTarget === carPlayer ? playerCarWeight : enemyCar.pursuitDetection.counterAttackTarget.weight, isPlayer: enemyCar.pursuitDetection.counterAttackTarget === carPlayer }; } else if (enemyCar.pursuitDetection.evasionMode) { // Fleeing - use special evasion mode enemyCar.aiMode = "evade"; } } else { // Normal mode selection with camper behavior var myHealthRatio = enemyCar.health / maxHealth; var isCamper = enemyCar.tacticalPersonality.isCamper; var campingThreshold = enemyCar.tacticalPersonality.campingThreshold; var campingPatience = enemyCar.tacticalPersonality.campingPatience; if (isCamper) { // Camper-specific mode selection if (myHealthRatio > campingThreshold) { // Camper is healthy - check if any targets are weakened enough var hasWeakTargets = false; var weakestTargetHealth = 1.0; // Check all potential targets for weakness for (var camperTargetCheck = 0; camperTargetCheck < enemyCars.length + 1; camperTargetCheck++) { var checkTargetHealth = 1.0; if (camperTargetCheck < enemyCars.length && camperTargetCheck !== aiCarIdx) { checkTargetHealth = enemyCars[camperTargetCheck].health / maxHealth; } else if (camperTargetCheck === enemyCars.length) { checkTargetHealth = playerHealth / maxHealth; } if (checkTargetHealth < 0.7) { hasWeakTargets = true; } weakestTargetHealth = Math.min(weakestTargetHealth, checkTargetHealth); } if (hasWeakTargets && weakestTargetHealth < 0.5) { // Opportunity detected - switch to aggressive hunting enemyCar.aiMode = "follow"; } else if (Math.random() < campingPatience) { // Patient waiting - use enhanced free roam to position and wait enemyCar.aiMode = "camp"; } else { // Impatient camper - light engagement enemyCar.aiMode = "follow"; } } else { // Camper is hurt - very defensive behavior if (myHealthRatio < 0.3) { // Critically hurt - pure evasion enemyCar.aiMode = "evade"; } else { // Somewhat hurt - careful engagement with weak targets only if (enemyCar.currentTarget) { var targetHealth = 100; if (typeof enemyCar.currentTarget.object.health !== "undefined") { targetHealth = enemyCar.currentTarget.object.health; } else if (enemyCar.currentTarget.isPlayer) { targetHealth = playerHealth; } var targetHealthRatio = targetHealth / maxHealth; if (targetHealthRatio < myHealthRatio * 0.7) { // Target is significantly weaker - cautious attack enemyCar.aiMode = "follow"; } else { // Target too strong - avoid enemyCar.aiMode = "evade"; } } else { enemyCar.aiMode = "camp"; } } } } else { // Non-camper normal mode selection with health-based modifications // Calculate target speed for mode preference based on current target var targetSpeed = 0; if (enemyCar.currentTarget) { targetSpeed = Math.sqrt(enemyCar.currentTarget.velocityX * enemyCar.currentTarget.velocityX + enemyCar.currentTarget.velocityY * enemyCar.currentTarget.velocityY); } var targetSpeedRatio = targetSpeed / maxSpeed; // 0 to 1 var followChance; // Health-modified mode preferences var healthModifier = 0; if (myHealthRatio > 0.7) { // Healthy - more aggressive healthModifier = 0.1 * enemyCar.tacticalPersonality.aggressiveness; } else if (myHealthRatio < 0.4) { // Hurt - more cautious healthModifier = -0.2; } // Speed-based mode preferences with health modification: if (targetSpeedRatio > 0.7) { // Very fast - prefer follow mode for ambush tactics followChance = 0.9 + healthModifier; } else if (targetSpeedRatio > 0.4) { // Fast to medium - moderate follow preference followChance = 0.8 + healthModifier; } else if (targetSpeedRatio > 0.2) { // Medium - balanced but still prefer follow followChance = 0.7 + healthModifier; } else { // Slow - prefer follow mode for direct pursuit followChance = 0.85 + healthModifier; } // Clamp follow chance followChance = Math.max(0.1, Math.min(0.95, followChance)); // Pick mode based on calculated preference if (Math.random() < followChance) { enemyCar.aiMode = "follow"; } else { enemyCar.aiMode = "free"; } } } // Duration: 1.2s to 3.5s (72 to 210 frames) enemyCar.aiModeDuration = 72 + Math.floor(Math.random() * 138); enemyCar.aiModeTimer = 0; } // --- AI movement logic depending on mode --- var aiDeltaX, aiDeltaY, aiDistance, aiTargetRotation, aiPower, aiTargetVelocity; // Edge avoidance logic - calculate repulsion from boundaries var edgeAvoidanceX = 0; var edgeAvoidanceY = 0; var edgeBuffer = 200; // Distance from edge to start avoiding var avoidanceStrength = 0.3; // How strongly to avoid edges // Check distance from each edge and calculate avoidance force if (enemyCar.x < edgeBuffer) { // Too close to left edge, push right var leftForce = (edgeBuffer - enemyCar.x) / edgeBuffer; edgeAvoidanceX += leftForce * avoidanceStrength; } if (enemyCar.x > 2048 - edgeBuffer) { // Too close to right edge, push left var rightForce = (enemyCar.x - (2048 - edgeBuffer)) / edgeBuffer; edgeAvoidanceX -= rightForce * avoidanceStrength; } if (enemyCar.y < edgeBuffer) { // Too close to top edge, push down var topForce = (edgeBuffer - enemyCar.y) / edgeBuffer; edgeAvoidanceY += topForce * avoidanceStrength; } if (enemyCar.y > 2186 - edgeBuffer) { // Too close to bottom edge, push up var bottomForce = (enemyCar.y - (2186 - edgeBuffer)) / edgeBuffer; edgeAvoidanceY -= bottomForce * avoidanceStrength; } if (enemyCar.aiMode === "follow") { // Initialize follow strategy if not set if (typeof enemyCar.followStrategy === "undefined") { enemyCar.followStrategy = 0; enemyCar.followStrategyTimer = 0; enemyCar.followStrategyDuration = 60 + Math.floor(Math.random() * 60); // 1-2 seconds for adaptive strategy } // Individual strategy selection based on each car's unique personality enemyCar.followStrategyTimer++; if (enemyCar.followStrategyTimer > enemyCar.followStrategyDuration) { // Calculate target speed for strategy selection var targetSpeed = 0; if (enemyCar.currentTarget) { targetSpeed = Math.sqrt(enemyCar.currentTarget.velocityX * enemyCar.currentTarget.velocityX + enemyCar.currentTarget.velocityY * enemyCar.currentTarget.velocityY); } var targetSpeedRatio = targetSpeed / maxSpeed; // 0 to 1 var relativeDistance = Math.sqrt(aiDeltaX * aiDeltaX + aiDeltaY * aiDeltaY); var relativeAngle = Math.atan2(aiDeltaX, -aiDeltaY); var enemyAngle = enemyCar.rotation; var angleDifference = Math.abs(relativeAngle - enemyAngle); while (angleDifference > Math.PI) angleDifference = 2 * Math.PI - angleDifference; var isTargetAhead = angleDifference < Math.PI / 3; // Within 60 degrees ahead var isTargetBehind = angleDifference > 2 * Math.PI / 3; // More than 120 degrees behind // Individual strategy selection based on car's tactical personality var personality = enemyCar.tacticalPersonality; var strategyScores = [0, 0, 0]; // [direct, intimidation, ambush] // Base preference scores from personality strategyScores[0] = personality.directPreference; strategyScores[1] = personality.intimidationPreference; strategyScores[2] = personality.ambushPreference; // Modify scores based on target speed and individual preferences if (targetSpeedRatio < 0.3) { // Slow target - use individual slow target strategy preference var preferredStrategy = personality.slowTargetStrategy; strategyScores[preferredStrategy] += 0.4; } else if (targetSpeedRatio > 0.6) { // Fast target - use individual fast target strategy preference var preferredStrategy = personality.fastTargetStrategy; strategyScores[preferredStrategy] += 0.4; } else { // Medium speed target - use individual medium target strategy preference var preferredStrategy = personality.mediumTargetStrategy; strategyScores[preferredStrategy] += 0.3; } // Situational modifiers based on individual traits if (targetSpeedRatio < 0.05) { // Target is nearly stationary - intimidation gets bonus for patient cars if (personality.patience > 0.6) { strategyScores[1] += 0.3; // Patient cars prefer intimidation // Initialize intimidation counter if not set if (typeof enemyCar.intimidationCount === "undefined") { enemyCar.intimidationCount = 0; enemyCar.maxIntimidations = Math.floor(personality.patience * 4) + 1; // 1-4 based on patience } // Check if we've completed enough intimidations if (enemyCar.intimidationCount >= enemyCar.maxIntimidations) { strategyScores[0] += 0.5; // Switch to direct after enough intimidation strategyScores[1] -= 0.3; } } else { strategyScores[0] += 0.4; // Impatient cars go direct } } // Distance-based individual preferences if (relativeDistance < personality.preferredAttackDistance) { // Within preferred attack range - aggressive cars prefer direct strategyScores[0] += personality.aggressiveness * 0.3; } else if (relativeDistance > personality.preferredAttackDistance * 2) { // Far away - adaptive cars prefer ambush strategyScores[2] += personality.adaptability * 0.3; } // Positional modifiers with individual traits if (isTargetBehind || relativeDistance < personality.personalSpaceRadius) { // Target behind or in personal space - use intimidation for positioning strategyScores[1] += 0.2; } else if (isTargetAhead && relativeDistance < personality.preferredAttackDistance * 1.5) { // Target ahead and in preferred range if (personality.aggressiveness > 0.6) { strategyScores[0] += 0.3; // Aggressive cars go direct } else { strategyScores[1] += 0.2; // Less aggressive cars intimidate first } } // Select strategy with highest score (with some randomness based on adaptability) var maxScore = Math.max(strategyScores[0], strategyScores[1], strategyScores[2]); var candidateStrategies = []; for (var s = 0; s < 3; s++) { if (strategyScores[s] >= maxScore - personality.adaptability * 0.2) { candidateStrategies.push(s); } } enemyCar.followStrategy = candidateStrategies[Math.floor(Math.random() * candidateStrategies.length)]; // Individual strategy duration based on personality var baseDuration = personality.minStrategyDuration + (personality.maxStrategyDuration - personality.minStrategyDuration) * personality.patience; var strategyMultiplier = 1.0; switch (enemyCar.followStrategy) { case 0: // Direct - duration affected by aggressiveness (aggressive = shorter bursts) strategyMultiplier = 1.2 - personality.aggressiveness * 0.4; break; case 1: // Intimidation - duration affected by patience (patient = longer intimidation) strategyMultiplier = 0.8 + personality.patience * 0.6; break; case 2: // Ambush - duration affected by adaptability (adaptive = longer planning) strategyMultiplier = 1.0 + personality.adaptability * 0.5; break; } enemyCar.followStrategyDuration = Math.floor(baseDuration * strategyMultiplier); enemyCar.followStrategyTimer = 0; } // Calculate base values using current target instead of always player (optimized) if (enemyCar.currentTarget) { aiDeltaX = enemyCar.currentTarget.object.x - enemyCar.x; aiDeltaY = enemyCar.currentTarget.object.y - enemyCar.y; // Cache distance calculation var aiDeltaXSq = aiDeltaX * aiDeltaX; var aiDeltaYSq = aiDeltaY * aiDeltaY; aiDistance = Math.sqrt(aiDeltaXSq + aiDeltaYSq); // Calculate relative velocity and approach angle for smarter pursuit var aiVelX = enemyCar.aiVelocityX || 0; var aiVelY = enemyCar.aiVelocityY || 0; var relativeVelX = enemyCar.currentTarget.velocityX - aiVelX; var relativeVelY = enemyCar.currentTarget.velocityY - aiVelY; } else { // Fallback to player if no target selected aiDeltaX = carPlayer.x - enemyCar.x; aiDeltaY = carPlayer.y - enemyCar.y; // Cache distance calculation var aiDeltaXSq = aiDeltaX * aiDeltaX; var aiDeltaYSq = aiDeltaY * aiDeltaY; aiDistance = Math.sqrt(aiDeltaXSq + aiDeltaYSq); var aiVelX = enemyCar.aiVelocityX || 0; var aiVelY = enemyCar.aiVelocityY || 0; var relativeVelX = velocityX - aiVelX; var relativeVelY = velocityY - aiVelY; } var approachAngle = Math.atan2(aiDeltaX, -aiDeltaY); var enemyCurrentAngle = enemyCar.rotation; var angleDiff = approachAngle - enemyCurrentAngle; while (angleDiff > Math.PI) angleDiff -= 2 * Math.PI; while (angleDiff < -Math.PI) angleDiff += 2 * Math.PI; // Anti-circling detection: check if we're in a circular pattern if (typeof enemyCar.lastPositions === "undefined") { enemyCar.lastPositions = []; enemyCar.circlingDetected = false; enemyCar.circlingCooldown = 0; } // Store position history for circling detection enemyCar.lastPositions.push({ x: enemyCar.x, y: enemyCar.y, frame: LK.ticks }); if (enemyCar.lastPositions.length > 60) { // Keep 1 second of history enemyCar.lastPositions.shift(); } // Detect circling by checking if we've been in similar positions recently var circlingThreshold = 80; // Distance threshold for considering positions "similar" var circlingCount = 0; for (var i = 0; i < enemyCar.lastPositions.length - 20; i++) { var oldPos = enemyCar.lastPositions[i]; var dist = Math.sqrt((enemyCar.x - oldPos.x) * (enemyCar.x - oldPos.x) + (enemyCar.y - oldPos.y) * (enemyCar.y - oldPos.y)); if (dist < circlingThreshold) { circlingCount++; } } enemyCar.circlingDetected = circlingCount > 3 && aiDistance < 200; // Circling if close to player and repeating positions // Reduce circling cooldown if (enemyCar.circlingCooldown > 0) enemyCar.circlingCooldown--; // Initialize failure detection system for direct pursuit if (typeof enemyCar.directPursuitTimer === "undefined") { enemyCar.directPursuitTimer = 0; enemyCar.lastPlayerDistance = aiDistance; enemyCar.nearMissCount = 0; enemyCar.totalPursuitTime = 0; } // Apply different follow strategies with distance awareness switch (enemyCar.followStrategy) { case 0: // Direct pursuit - straight chase with distance management // Track pursuit effectiveness enemyCar.directPursuitTimer++; enemyCar.totalPursuitTime++; // Detect near misses - when AI gets very close but doesn't hit if (aiDistance < 80 && enemyCar.lastPlayerDistance > 80) { // Just entered close range enemyCar.directPursuitTimer = 0; // Reset timer on new approach } if (enemyCar.lastPlayerDistance < 80 && aiDistance > 120) { // Just left close range without collision - potential near miss enemyCar.nearMissCount++; } // Check for failure conditions in direct pursuit var pursuitFailed = false; if (enemyCar.nearMissCount >= 2) { // Failed after 2 near misses pursuitFailed = true; } else if (enemyCar.totalPursuitTime > 300 && !currentColliding) { // Failed after 5 seconds without collision pursuitFailed = true; } else if (enemyCar.directPursuitTimer > 120 && aiDistance > 200) { // Failed if stuck at medium distance for 2 seconds pursuitFailed = true; } // Switch strategy if direct pursuit failed if (pursuitFailed) { // Reset failure tracking enemyCar.nearMissCount = 0; enemyCar.totalPursuitTime = 0; enemyCar.directPursuitTimer = 0; // Calculate target speed for strategy selection var currentTargetSpeed = 0; if (enemyCar.currentTarget) { currentTargetSpeed = Math.sqrt(enemyCar.currentTarget.velocityX * enemyCar.currentTarget.velocityX + enemyCar.currentTarget.velocityY * enemyCar.currentTarget.velocityY); } var targetSpeedRatio = currentTargetSpeed / maxSpeed; // Choose new strategy based on current situation if (targetSpeedRatio > 0.5 || aiDistance > 300) { // Fast target or far away - try ambush enemyCar.followStrategy = 2; enemyCar.followStrategyDuration = 90 + Math.floor(Math.random() * 60); } else { // Slow target or close - try varied approach enemyCar.followStrategy = 1; enemyCar.followStrategyDuration = 60 + Math.floor(Math.random() * 60); } enemyCar.followStrategyTimer = 0; } // Check current AI speed to prioritize acceleration building var currentAISpeed = Math.sqrt((enemyCar.aiVelocityX || 0) * (enemyCar.aiVelocityX || 0) + (enemyCar.aiVelocityY || 0) * (enemyCar.aiVelocityY || 0)); var aiSpeedRatio = currentAISpeed / maxSpeed; var needsAcceleration = aiSpeedRatio < 0.4; // Less than 40% of max speed // Check if AI can go straight to target for speed optimization var currentEnemyAngle = enemyCar.rotation; var straightLineAngle = Math.atan2(aiDeltaX, -aiDeltaY); var angleToTarget = straightLineAngle - currentEnemyAngle; while (angleToTarget > Math.PI) angleToTarget -= 2 * Math.PI; while (angleToTarget < -Math.PI) angleToTarget += 2 * Math.PI; // AI is considered "aligned" if facing within 15 degrees of target var isAlignedWithTarget = Math.abs(angleToTarget) < Math.PI / 12; // 15 degrees // Prioritize acceleration building when speed is low if (needsAcceleration && aiDistance > 150) { // Focus on building speed when far enough from target if (isAlignedWithTarget) { // Perfect alignment - accelerate in straight line toward target aiTargetRotation = straightLineAngle; aiPower = 1.0; // Maximum acceleration aiTargetVelocity = maxSpeed * aiPower * 1.0; } else if (Math.abs(angleToTarget) < Math.PI / 6) { // Close to aligned (within 30 degrees) - minor correction while accelerating var correctionFactor = Math.abs(angleToTarget) / (Math.PI / 6); // 0 to 1 aiTargetRotation = enemyCar.rotation + angleToTarget * 0.3; // Gentle steering aiPower = 0.9 + correctionFactor * 0.1; // Maintain high acceleration aiTargetVelocity = maxSpeed * aiPower * (0.95 + correctionFactor * 0.05); } else { // Need significant turn - prioritize getting aligned for acceleration aiTargetRotation = straightLineAngle; aiPower = 0.7; // Moderate acceleration while turning aiTargetVelocity = maxSpeed * aiPower * 0.8; } } else if (aiDistance < enemyCar.tacticalPersonality.preferredAttackDistance * 0.6 && !enemyCar.circlingDetected) { // Too close for this car's preferred attack style - back off based on aggressiveness var backoffIntensity = 0.3 + (1 - enemyCar.tacticalPersonality.aggressiveness) * 0.4; // Less aggressive = more backoff var backoffAngle = approachAngle + Math.PI + (Math.random() - 0.5) * Math.PI * backoffIntensity; aiTargetRotation = backoffAngle; aiPower = 0.2 + enemyCar.tacticalPersonality.aggressiveness * 0.2; aiTargetVelocity = maxSpeed * aiPower * 0.6; } else if (aiDistance > enemyCar.tacticalPersonality.preferredAttackDistance * 2) { // Far away - check for straight line opportunity if (isAlignedWithTarget) { // Can go straight - use maximum speed for efficiency aiTargetRotation = straightLineAngle; aiPower = Math.min(1, aiDistance / 600); aiTargetVelocity = maxSpeed * aiPower * 1.0; // Full speed when aligned } else { // Need to turn first - direct approach at normal speed aiTargetRotation = approachAngle; aiPower = Math.min(1, aiDistance / 600); aiTargetVelocity = maxSpeed * aiPower * 0.95; } } else { // Medium distance - check alignment for speed boost, but prioritize acceleration if needed if (needsAcceleration) { // Still building speed - maintain acceleration focus if (isAlignedWithTarget) { aiTargetRotation = straightLineAngle; aiPower = 0.9; aiTargetVelocity = maxSpeed * aiPower * 0.95; } else { // Get aligned while building speed aiTargetRotation = approachAngle; aiPower = 0.8; aiTargetVelocity = maxSpeed * aiPower * 0.9; } } else if (isAlignedWithTarget && aiDistance > 200) { // Aligned and far enough - go straight at higher speed aiTargetRotation = straightLineAngle; aiPower = Math.min(1, aiDistance / 500); aiTargetVelocity = maxSpeed * aiPower * 0.98; // Near full speed when aligned } else { // Not aligned or too close - slight offset to avoid head-on collision var offsetAngle = (Math.random() - 0.5) * Math.PI * 0.2; // ±18 degrees aiTargetRotation = approachAngle + offsetAngle; aiPower = Math.min(1, aiDistance / 500); aiTargetVelocity = maxSpeed * aiPower * 0.92; } } // Update last distance for next frame enemyCar.lastPlayerDistance = aiDistance; break; case 1: // Varied pursuit - intimidation mode: follows like direct but maintains safe distance to scare // Check current AI speed for acceleration prioritization var currentAISpeedIntimidation = Math.sqrt((enemyCar.aiVelocityX || 0) * (enemyCar.aiVelocityX || 0) + (enemyCar.aiVelocityY || 0) * (enemyCar.aiVelocityY || 0)); var aiSpeedRatioIntimidation = currentAISpeedIntimidation / maxSpeed; var needsAccelerationIntimidation = aiSpeedRatioIntimidation < 0.35; // Less than 35% of max speed if (enemyCar.circlingDetected && enemyCar.circlingCooldown <= 0) { // Break out of circling pattern var escapeAngle = approachAngle + Math.PI * 0.75 * (Math.random() < 0.5 ? 1 : -1); aiTargetRotation = escapeAngle; aiPower = 0.8; aiTargetVelocity = maxSpeed * aiPower * 0.85; enemyCar.circlingCooldown = 120; // 2 second cooldown } else if (needsAccelerationIntimidation && aiDistance > 200) { // Build speed before attempting intimidation tactics var intimidationAngle = Math.atan2(aiDeltaX, -aiDeltaY); var currentAngle = enemyCar.rotation; var angleToIntimidationTarget = intimidationAngle - currentAngle; while (angleToIntimidationTarget > Math.PI) angleToIntimidationTarget -= 2 * Math.PI; while (angleToIntimidationTarget < -Math.PI) angleToIntimidationTarget += 2 * Math.PI; if (Math.abs(angleToIntimidationTarget) < Math.PI / 8) { // Well aligned - accelerate directly toward intimidation position aiTargetRotation = intimidationAngle; aiPower = 0.9; aiTargetVelocity = maxSpeed * aiPower * 0.95; } else { // Need to align - gentle steering while building speed aiTargetRotation = currentAngle + angleToIntimidationTarget * 0.4; aiPower = 0.8; aiTargetVelocity = maxSpeed * aiPower * 0.9; } } else if (aiDistance < enemyCar.tacticalPersonality.preferredIntimidationDistance * 1.2) { // Individual intimidation range based on car's personality var intimidationDistance = enemyCar.tacticalPersonality.preferredIntimidationDistance; var distanceError = aiDistance - intimidationDistance; var toleranceRange = 20 + enemyCar.tacticalPersonality.patience * 20; // 20-40 pixel tolerance based on patience if (Math.abs(distanceError) < toleranceRange) { // Perfect intimidation distance - behavior based on individual aggressiveness and speed var aggressiveness = enemyCar.tacticalPersonality.aggressiveness; var weavingIntensity = Math.PI * 0.1 + aggressiveness * Math.PI * 0.1; // More aggressive = more weaving var intimidationOffset = Math.sin(LK.ticks * (0.03 + aggressiveness * 0.04)) * weavingIntensity; aiTargetRotation = approachAngle + intimidationOffset; // Speed-dependent intimidation - more aggressive when faster var speedBonus = Math.min(0.2, aiSpeedRatioIntimidation * 0.3); aiPower = 0.5 + aggressiveness * 0.3 + speedBonus; aiTargetVelocity = maxSpeed * aiPower * (0.8 + aggressiveness * 0.1 + speedBonus); // Check if target is stationary and we're intimidating var currentTargetSpeed = 0; if (enemyCar.currentTarget) { currentTargetSpeed = Math.sqrt(enemyCar.currentTarget.velocityX * enemyCar.currentTarget.velocityX + enemyCar.currentTarget.velocityY * enemyCar.currentTarget.velocityY); } if (currentTargetSpeed < maxSpeed * 0.05 && typeof enemyCar.intimidationCount !== "undefined") { // Mark intimidation as successful - duration based on patience if (typeof enemyCar.intimidationTimer === "undefined") enemyCar.intimidationTimer = 0; enemyCar.intimidationTimer++; // Patient cars intimidate longer, impatient cars intimidate briefly var intimidationDuration = 60 + enemyCar.tacticalPersonality.patience * 120; // 1-3 seconds based on patience if (enemyCar.intimidationTimer > intimidationDuration) { enemyCar.intimidationCount++; enemyCar.intimidationTimer = 0; // Force strategy recalculation enemyCar.followStrategyTimer = enemyCar.followStrategyDuration + 1; } } } else if (distanceError < 0) { // Too close - back off behavior based on aggressiveness var backoffIntensity = 0.2 + (1 - enemyCar.tacticalPersonality.aggressiveness) * 0.2; // Less aggressive = back off more aiTargetRotation = approachAngle + Math.PI + (Math.random() - 0.5) * Math.PI * backoffIntensity; aiPower = 0.3 + enemyCar.tacticalPersonality.aggressiveness * 0.2; aiTargetVelocity = maxSpeed * aiPower * 0.6; } else { // Too far - approach for intimidation with individual aggressiveness aiTargetRotation = approachAngle; aiPower = 0.6 + enemyCar.tacticalPersonality.aggressiveness * 0.3; aiTargetVelocity = maxSpeed * aiPower * (0.85 + enemyCar.tacticalPersonality.aggressiveness * 0.1); } } else { // Long range - approach based on individual preferred attack distance var approachIntensity = Math.min(1, aiDistance / (enemyCar.tacticalPersonality.preferredAttackDistance * 2)); aiTargetRotation = approachAngle; aiPower = approachIntensity * (0.7 + enemyCar.tacticalPersonality.aggressiveness * 0.3); aiTargetVelocity = maxSpeed * aiPower * (0.9 + enemyCar.tacticalPersonality.aggressiveness * 0.05); } break; case 2: // Ambush - predict target's future position with smarter interception var targetVelX = 0; var targetVelY = 0; var targetPosX = enemyCar.x; // fallback position var targetPosY = enemyCar.y; if (enemyCar.currentTarget) { targetVelX = enemyCar.currentTarget.velocityX; targetVelY = enemyCar.currentTarget.velocityY; targetPosX = enemyCar.currentTarget.object.x; targetPosY = enemyCar.currentTarget.object.y; } var targetSpeed = Math.sqrt(targetVelX * targetVelX + targetVelY * targetVelY); // Check current AI speed for acceleration prioritization in ambush var currentAISpeedAmbush = Math.sqrt((enemyCar.aiVelocityX || 0) * (enemyCar.aiVelocityX || 0) + (enemyCar.aiVelocityY || 0) * (enemyCar.aiVelocityY || 0)); var aiSpeedRatioAmbush = currentAISpeedAmbush / maxSpeed; var needsAccelerationAmbush = aiSpeedRatioAmbush < 0.5; // Less than 50% of max speed for ambush if (enemyCar.circlingDetected && enemyCar.circlingCooldown <= 0) { // Break circling with wide flanking maneuver var flankAngle = approachAngle + Math.PI * 0.8 * (Math.random() < 0.5 ? 1 : -1); aiTargetRotation = flankAngle; aiPower = 0.9; aiTargetVelocity = maxSpeed * aiPower * 0.9; enemyCar.circlingCooldown = 180; // 3 second cooldown } else if (needsAccelerationAmbush && aiDistance > 250) { // Build speed before attempting interception - find clear acceleration path var accelerationAngle; // Try to find a path that doesn't lead directly to target (for ambush positioning) if (aiDistance > 400) { // Far from target - accelerate in a flanking direction var flankingOffset = Math.PI * 0.3 * (Math.random() < 0.5 ? 1 : -1); // ±54 degrees accelerationAngle = approachAngle + flankingOffset; } else { // Medium distance - position for future interception while building speed var leadAngle = Math.atan2(targetVelX, -targetVelY); // Target's movement direction var interceptAngle = leadAngle + Math.PI * 0.6 * (Math.random() < 0.5 ? 1 : -1); // Offset for ambush accelerationAngle = interceptAngle; } aiTargetRotation = accelerationAngle; aiPower = 0.95; // High acceleration priority aiTargetVelocity = maxSpeed * aiPower * 0.98; } else if (aiDistance < 150) { // Close range - position for optimal angle of attack, but only if we have good speed if (aiSpeedRatioAmbush > 0.4) { // Have enough speed for attack positioning var attackAngle = approachAngle + Math.PI * 0.4 * (angleDiff > 0 ? 1 : -1); aiTargetRotation = attackAngle; aiPower = 0.7 + aiSpeedRatioAmbush * 0.2; // More power when faster aiTargetVelocity = maxSpeed * aiPower * (0.8 + aiSpeedRatioAmbush * 0.15); } else { // Need more speed - create space while accelerating var retreatAngle = approachAngle + Math.PI + (Math.random() - 0.5) * Math.PI * 0.4; aiTargetRotation = retreatAngle; aiPower = 0.8; aiTargetVelocity = maxSpeed * aiPower * 0.85; } } else { // Long range interception var predictionTime = Math.min(2.5, aiDistance / maxSpeed * 0.9); var interceptX = targetPosX + targetVelX * predictionTime; var interceptY = targetPosY + targetVelY * predictionTime; // Keep predicted position within bounds interceptX = Math.max(80, Math.min(1968, interceptX)); interceptY = Math.max(80, Math.min(2106, interceptY)); // Calculate optimal interception approach var interceptDeltaX = interceptX - enemyCar.x; var interceptDeltaY = interceptY - enemyCar.y; var interceptDistance = Math.sqrt(interceptDeltaX * interceptDeltaX + interceptDeltaY * interceptDeltaY); // Adjust interception based on current speed capabilities var speedRatio = (enemyCar.aiCurrentVelocity || 0) / Math.max(0.1, targetSpeed); var leadAdjustment = (1 - speedRatio) * 0.3; // Speed-dependent interception strategy if (needsAccelerationAmbush) { // Focus on acceleration toward interception point aiTargetRotation = Math.atan2(interceptDeltaX, -interceptDeltaY); aiPower = 0.9; // High acceleration var speedBoost = Math.min(0.25, targetSpeed / maxSpeed * 0.25); aiTargetVelocity = maxSpeed * aiPower * (0.9 + speedBoost); } else { // Have good speed - execute precise interception aiTargetRotation = Math.atan2(interceptDeltaX, -interceptDeltaY); aiPower = Math.min(1, interceptDistance / 700); var speedBoost = Math.min(0.2, targetSpeed / maxSpeed * 0.2); aiTargetVelocity = maxSpeed * aiPower * (0.85 + speedBoost + leadAdjustment); } } break; } // Anti-clustering avoidance - check for nearby friendly cars to avoid grouping var clusterAvoidanceX = 0; var clusterAvoidanceY = 0; var clusterAvoidanceRadius = enemyCar.tacticalPersonality.clusterAvoidanceRadius; var clusterAvoidanceStrength = enemyCar.tacticalPersonality.antiGroupingPreference * 0.4; // Count and avoid nearby friendly cars var nearbyCarsCount = 0; for (var avoidIdx = 0; avoidIdx < enemyCars.length; avoidIdx++) { if (avoidIdx !== aiCarIdx) { var otherEnemyCar = enemyCars[avoidIdx]; var distToOther = Math.sqrt((enemyCar.x - otherEnemyCar.x) * (enemyCar.x - otherEnemyCar.x) + (enemyCar.y - otherEnemyCar.y) * (enemyCar.y - otherEnemyCar.y)); if (distToOther < clusterAvoidanceRadius && distToOther > 0) { nearbyCarsCount++; // Calculate avoidance force (push away from other car) var avoidForceX = (enemyCar.x - otherEnemyCar.x) / distToOther; var avoidForceY = (enemyCar.y - otherEnemyCar.y) / distToOther; // Stronger avoidance when closer var proximityFactor = Math.max(0, (clusterAvoidanceRadius - distToOther) / clusterAvoidanceRadius); clusterAvoidanceX += avoidForceX * proximityFactor * clusterAvoidanceStrength; clusterAvoidanceY += avoidForceY * proximityFactor * clusterAvoidanceStrength; } } } // Apply cluster penalty to movement if too many cars nearby var clusterPenalty = 1.0; if (nearbyCarsCount > enemyCar.tacticalPersonality.maxClusterTolerance) { // Reduce speed and add more aggressive avoidance when clustered clusterPenalty = 0.7 - (nearbyCarsCount - enemyCar.tacticalPersonality.maxClusterTolerance) * 0.15; clusterPenalty = Math.max(0.3, clusterPenalty); // Increase avoidance strength when over tolerance clusterAvoidanceStrength *= 1.5; } // Blend in edge avoidance and cluster avoidance with follow behavior var totalAvoidanceX = edgeAvoidanceX + clusterAvoidanceX; var totalAvoidanceY = edgeAvoidanceY + clusterAvoidanceY; if (totalAvoidanceX !== 0 || totalAvoidanceY !== 0) { // Calculate combined avoidance angle var avoidanceAngle = Math.atan2(totalAvoidanceX, -totalAvoidanceY); // Blend the target rotation with avoidance (stronger when closer to edges or clusters) var avoidanceWeight = Math.min(0.8, Math.abs(totalAvoidanceX) + Math.abs(totalAvoidanceY)); var followWeight = 1 - avoidanceWeight; // Convert angles to vectors for blending var followVecX = Math.sin(aiTargetRotation) * followWeight; var followVecY = -Math.cos(aiTargetRotation) * followWeight; var avoidVecX = Math.sin(avoidanceAngle) * avoidanceWeight; var avoidVecY = -Math.cos(avoidanceAngle) * avoidanceWeight; // Combine and convert back to angle var blendedVecX = followVecX + avoidVecX; var blendedVecY = followVecY + avoidVecY; aiTargetRotation = Math.atan2(blendedVecX, -blendedVecY); // Apply cluster penalty to target velocity aiTargetVelocity *= clusterPenalty; } } else if (enemyCar.aiMode === "evade") { // Evasion mode - flee from pursuers with different patterns if (typeof enemyCar.evasionState === "undefined" || enemyCar.aiModeTimer === 0) { // Initialize evasion variables enemyCar.evasionState = { baseAngle: Math.random() * Math.PI * 2, escapeTimer: 0, zigzagDirection: Math.random() < 0.5 ? 1 : -1, circleCenter: { x: enemyCar.x, y: enemyCar.y }, circleRadius: 150 + Math.random() * 100, circleAngle: 0, lastPursuerAngle: 0 }; } enemyCar.evasionState.escapeTimer++; // Calculate average pursuer position for evasion reference var avgPursuerX = 0, avgPursuerY = 0; var pursuerCount = enemyCar.pursuitDetection.pursuers.length; if (pursuerCount > 0) { for (var ep = 0; ep < pursuerCount; ep++) { avgPursuerX += enemyCar.pursuitDetection.pursuers[ep].object.x; avgPursuerY += enemyCar.pursuitDetection.pursuers[ep].object.y; } avgPursuerX /= pursuerCount; avgPursuerY /= pursuerCount; } else { // Fallback if no pursuers detected avgPursuerX = enemyCar.x - 100; avgPursuerY = enemyCar.y - 100; } // Calculate escape direction (away from average pursuer position) var escapeFromX = avgPursuerX - enemyCar.x; var escapeFromY = avgPursuerY - enemyCar.y; var escapeBaseAngle = Math.atan2(-escapeFromX, escapeFromY); // Opposite direction // Apply different evasion strategies with individual preferences var flightPrefs = enemyCar.tacticalPersonality; switch (enemyCar.pursuitDetection.evasionStrategy) { case 0: // Speed focus - straight line escape with individual variations var escapeEfficiency = 0.8 + flightPrefs.aggressiveness * 0.2; // Aggressive cars escape more efficiently aiTargetRotation = escapeBaseAngle; // Look for clear path ahead with individual distance preferences var lookAheadDist = 150 + flightPrefs.riskTolerance * 100; // Risk-tolerant cars look further var pathClearX = enemyCar.x + Math.sin(escapeBaseAngle) * lookAheadDist; var pathClearY = enemyCar.y - Math.cos(escapeBaseAngle) * lookAheadDist; // Individual wall avoidance based on risk tolerance var wallBuffer = 80 + (1 - flightPrefs.riskTolerance) * 60; // Risk-averse cars avoid walls more if (pathClearX < wallBuffer || pathClearX > 2048 - wallBuffer || pathClearY < wallBuffer || pathClearY > 2186 - wallBuffer) { // Escape route selection based on individual preferences if (flightPrefs.adaptability > 0.5) { // Adaptive cars find better escape routes var toCenterX = 1024 - enemyCar.x; var toCenterY = 1093 - enemyCar.y; var toCenterAngle = Math.atan2(toCenterX, -toCenterY); // Blend escape with center direction based on adaptability var blendFactor = flightPrefs.adaptability; var escapeVecX = Math.sin(escapeBaseAngle) * (1 - blendFactor); var escapeVecY = -Math.cos(escapeBaseAngle) * (1 - blendFactor); var centerVecX = Math.sin(toCenterAngle) * blendFactor; var centerVecY = -Math.cos(toCenterAngle) * blendFactor; aiTargetRotation = Math.atan2(escapeVecX + centerVecX, -(escapeVecY + centerVecY)); } else { // Less adaptive cars just turn toward center var toCenterX = 1024 - enemyCar.x; var toCenterY = 1093 - enemyCar.y; aiTargetRotation = Math.atan2(toCenterX, -toCenterY); } } // Individual speed based on aggressiveness and bravery aiPower = 0.9 + flightPrefs.aggressiveness * 0.1; // More aggressive = faster escape var speedMultiplier = escapeEfficiency * (0.95 + flightPrefs.bravery * 0.05); // Brave cars maintain higher speed aiTargetVelocity = maxSpeed * aiPower * speedMultiplier; break; case 1: // Zigzag pattern with individual variations var baseFrequency = 0.08 + flightPrefs.adaptability * 0.08; // Adaptive cars zigzag more frequently var pursuitFrequency = enemyCar.pursuitDetection.pursuitIntensity * 0.06; var zigzagFrequency = baseFrequency + pursuitFrequency; // Individual zigzag amplitude based on risk tolerance var baseAmplitude = Math.PI * (0.3 + flightPrefs.riskTolerance * 0.2); // Risk-tolerant cars make wider turns var panicAmplitude = flightPrefs.panicThreshold < 0.5 ? Math.PI * 0.1 : 0; // Panicky cars add erratic movement var zigzagAmplitude = baseAmplitude + panicAmplitude; var zigzagOffset = Math.sin(enemyCar.evasionState.escapeTimer * zigzagFrequency) * zigzagAmplitude; // Add individual unpredictability if (flightPrefs.adaptability > 0.7 && Math.random() < 0.05) { // Highly adaptive cars occasionally reverse zigzag direction enemyCar.evasionState.zigzagDirection *= -1; } aiTargetRotation = escapeBaseAngle + zigzagOffset * (enemyCar.evasionState.zigzagDirection || 1); // Individual power and speed based on personality var basePower = 0.75 + flightPrefs.aggressiveness * 0.15; var pursuitBonus = enemyCar.pursuitDetection.pursuitIntensity * 0.15; aiPower = basePower + pursuitBonus; var speedMultiplier = 0.85 + flightPrefs.bravery * 0.1; // Brave cars maintain higher speed while evading aiTargetVelocity = maxSpeed * aiPower * speedMultiplier; break; case 2: // Circular escape pattern with individual preferences // Individual circle updating frequency based on adaptability var updateFrequency = flightPrefs.adaptability > 0.6 ? 45 : 75; // Adaptive cars update circle more often if (enemyCar.evasionState.escapeTimer % updateFrequency === 0) { enemyCar.evasionState.circleCenter.x = enemyCar.x; enemyCar.evasionState.circleCenter.y = enemyCar.y; // Individual circle radius based on personality var baseRadius = 120 + flightPrefs.riskTolerance * 80; // Risk-tolerant cars use larger circles var panicRadius = flightPrefs.panicThreshold < 0.4 ? 40 : 0; // Panicky cars use smaller circles enemyCar.evasionState.circleRadius = baseRadius + panicRadius; } // Individual circle speed based on aggressiveness and pursuit intensity var baseCircleSpeed = 0.06 + flightPrefs.aggressiveness * 0.04; var pursuitSpeedBonus = enemyCar.pursuitDetection.pursuitIntensity * 0.04; var circleSpeed = baseCircleSpeed + pursuitSpeedBonus; // Add individual direction changes for unpredictability if (flightPrefs.adaptability > 0.8 && Math.random() < 0.02) { // Highly adaptive cars occasionally reverse circle direction circleSpeed *= -1; } enemyCar.evasionState.circleAngle += circleSpeed; var targetCircleX = enemyCar.evasionState.circleCenter.x + Math.cos(enemyCar.evasionState.circleAngle) * enemyCar.evasionState.circleRadius; var targetCircleY = enemyCar.evasionState.circleCenter.y + Math.sin(enemyCar.evasionState.circleAngle) * enemyCar.evasionState.circleRadius; // Individual boundary handling based on risk tolerance var boundary = 80 + (1 - flightPrefs.riskTolerance) * 40; // Risk-averse cars stay further from edges targetCircleX = Math.max(boundary, Math.min(2048 - boundary, targetCircleX)); targetCircleY = Math.max(boundary, Math.min(2186 - boundary, targetCircleY)); var toCircleX = targetCircleX - enemyCar.x; var toCircleY = targetCircleY - enemyCar.y; aiTargetRotation = Math.atan2(toCircleX, -toCircleY); // Individual power and speed var basePower = 0.65 + flightPrefs.patience * 0.15; // Patient cars better at sustained circular movement var pursuitBonus = enemyCar.pursuitDetection.pursuitIntensity * 0.15; aiPower = basePower + pursuitBonus; var speedMultiplier = 0.8 + flightPrefs.bravery * 0.1; // Brave cars circle at higher speeds aiTargetVelocity = maxSpeed * aiPower * speedMultiplier; break; } // Apply strong edge avoidance during evasion if (edgeAvoidanceX !== 0 || edgeAvoidanceY !== 0) { var avoidanceAngle = Math.atan2(edgeAvoidanceX, -edgeAvoidanceY); var avoidanceWeight = Math.min(0.8, (Math.abs(edgeAvoidanceX) + Math.abs(edgeAvoidanceY)) * 2); var evasionWeight = 1 - avoidanceWeight; var evasionVecX = Math.sin(aiTargetRotation) * evasionWeight; var evasionVecY = -Math.cos(aiTargetRotation) * evasionWeight; var avoidVecX = Math.sin(avoidanceAngle) * avoidanceWeight; var avoidVecY = -Math.cos(avoidanceAngle) * avoidanceWeight; var blendedVecX = evasionVecX + avoidVecX; var blendedVecY = evasionVecY + avoidVecY; aiTargetRotation = Math.atan2(blendedVecX, -blendedVecY); } } else if (enemyCar.aiMode === "camp") { // Camping mode - position strategically and wait for opportunities if (typeof enemyCar.campingState === "undefined" || enemyCar.aiModeTimer === 0) { // Initialize camping variables enemyCar.campingState = { campingPosition: { x: 1024 + (Math.random() - 0.5) * 800, // Random position near center y: 1093 + (Math.random() - 0.5) * 600 }, repositionTimer: 0, repositionInterval: 180 + Math.floor(Math.random() * 240), // 3-7 seconds opportunityTimer: 0, lastOpportunityCheck: 0, preferredDistance: enemyCar.tacticalPersonality.campingDistance, isPositioned: false, patrolAngle: Math.random() * Math.PI * 2, patrolRadius: 80 + Math.random() * 40 // 80-120 patrol radius }; } enemyCar.campingState.repositionTimer++; enemyCar.campingState.opportunityTimer++; // Check for opportunities every 30 frames (0.5 seconds) if (enemyCar.campingState.opportunityTimer - enemyCar.campingState.lastOpportunityCheck > 30) { enemyCar.campingState.lastOpportunityCheck = enemyCar.campingState.opportunityTimer; // Scan for weakened targets var bestOpportunity = null; var bestOpportunityScore = 0; // Check all potential targets for (var campScanIdx = 0; campScanIdx < enemyCars.length + 1; campScanIdx++) { var scanTarget = null; var scanTargetHealth = 100; if (campScanIdx < enemyCars.length && campScanIdx !== aiCarIdx) { scanTarget = enemyCars[campScanIdx]; scanTargetHealth = scanTarget.health; } else if (campScanIdx === enemyCars.length) { scanTarget = carPlayer; scanTargetHealth = playerHealth; } if (scanTarget) { var scanDistance = Math.sqrt((scanTarget.x - enemyCar.x) * (scanTarget.x - enemyCar.x) + (scanTarget.y - enemyCar.y) * (scanTarget.y - enemyCar.y)); var scanHealthRatio = scanTargetHealth / maxHealth; var myHealthRatio = enemyCar.health / maxHealth; // Calculate opportunity score var opportunityScore = 0; if (scanHealthRatio < 0.5) { opportunityScore += (0.5 - scanHealthRatio) * 100; // Up to 50 points for very weak targets } if (scanDistance < 300) { opportunityScore += (300 - scanDistance) / 300 * 30; // Up to 30 points for close targets } if (myHealthRatio > scanHealthRatio + 0.2) { opportunityScore += 20; // 20 points for significant health advantage } // Camper-specific bonuses if (scanHealthRatio < 0.3 && myHealthRatio > 0.6) { opportunityScore += enemyCar.tacticalPersonality.opportunismLevel * 40; // Major opportunity bonus } if (opportunityScore > bestOpportunityScore && opportunityScore > 30) { bestOpportunityScore = opportunityScore; bestOpportunity = scanTarget; } } } // If excellent opportunity found, switch to hunting mode if (bestOpportunity && bestOpportunityScore > 70) { enemyCar.aiMode = "follow"; // Override target to the opportunity enemyCar.currentTarget = { object: bestOpportunity, velocityX: bestOpportunity === carPlayer ? velocityX : (bestOpportunity.aiVelocityX || 0) + (bestOpportunity.velocityX || 0), velocityY: bestOpportunity === carPlayer ? velocityY : (bestOpportunity.aiVelocityY || 0) + (bestOpportunity.velocityY || 0), weight: bestOpportunity === carPlayer ? playerCarWeight : bestOpportunity.weight, isPlayer: bestOpportunity === carPlayer }; enemyCar.aiModeTimer = 0; // Reset mode timer for immediate action } } // Camping positioning logic var toCampX = enemyCar.campingState.campingPosition.x - enemyCar.x; var toCampY = enemyCar.campingState.campingPosition.y - enemyCar.y; var distanceToCamp = Math.sqrt(toCampX * toCampX + toCampY * toCampY); // Check if we need to reposition the camping spot if (enemyCar.campingState.repositionTimer > enemyCar.campingState.repositionInterval || distanceToCamp < 50) { enemyCar.campingState.repositionTimer = 0; enemyCar.campingState.repositionInterval = 180 + Math.floor(Math.random() * 240); // Find new strategic camping position var bestCampX = 1024; var bestCampY = 1093; var maxDistance = 0; // Try to find position that maximizes distance to all targets while staying in bounds for (var campTry = 0; campTry < 8; campTry++) { var tryX = 200 + Math.random() * 1648; // Stay within bounds var tryY = 200 + Math.random() * 1786; var minDistanceToTargets = Infinity; // Check distance to all other cars for (var distCheck = 0; distCheck < enemyCars.length + 1; distCheck++) { var checkX, checkY; if (distCheck < enemyCars.length && distCheck !== aiCarIdx) { checkX = enemyCars[distCheck].x; checkY = enemyCars[distCheck].y; } else if (distCheck === enemyCars.length) { checkX = carPlayer.x; checkY = carPlayer.y; } else { continue; } var distToTarget = Math.sqrt((tryX - checkX) * (tryX - checkX) + (tryY - checkY) * (tryY - checkY)); minDistanceToTargets = Math.min(minDistanceToTargets, distToTarget); } if (minDistanceToTargets > maxDistance && minDistanceToTargets > enemyCar.campingState.preferredDistance * 0.8) { maxDistance = minDistanceToTargets; bestCampX = tryX; bestCampY = tryY; } } enemyCar.campingState.campingPosition.x = bestCampX; enemyCar.campingState.campingPosition.y = bestCampY; toCampX = bestCampX - enemyCar.x; toCampY = bestCampY - enemyCar.y; distanceToCamp = Math.sqrt(toCampX * toCampX + toCampY * toCampY); } // Movement logic for camping if (distanceToCamp > 100) { // Move to camping position aiTargetRotation = Math.atan2(toCampX, -toCampY); aiPower = Math.min(0.7, distanceToCamp / 300); aiTargetVelocity = maxSpeed * aiPower * 0.8; enemyCar.campingState.isPositioned = false; } else { // At camping position - patrol slowly while watching enemyCar.campingState.isPositioned = true; enemyCar.campingState.patrolAngle += 0.02 + Math.random() * 0.02; // Slow patrol var patrolX = enemyCar.campingState.campingPosition.x + Math.cos(enemyCar.campingState.patrolAngle) * enemyCar.campingState.patrolRadius; var patrolY = enemyCar.campingState.campingPosition.y + Math.sin(enemyCar.campingState.patrolAngle) * enemyCar.campingState.patrolRadius; // Keep patrol within bounds patrolX = Math.max(100, Math.min(1948, patrolX)); patrolY = Math.max(100, Math.min(2086, patrolY)); var toPatrolX = patrolX - enemyCar.x; var toPatrolY = patrolY - enemyCar.y; aiTargetRotation = Math.atan2(toPatrolX, -toPatrolY); aiPower = 0.3 + Math.random() * 0.2; // Very slow, patient movement aiTargetVelocity = maxSpeed * aiPower * 0.5; } // Apply edge avoidance to camping mode if (edgeAvoidanceX !== 0 || edgeAvoidanceY !== 0) { var avoidanceAngle = Math.atan2(edgeAvoidanceX, -edgeAvoidanceY); var avoidanceWeight = Math.min(0.7, (Math.abs(edgeAvoidanceX) + Math.abs(edgeAvoidanceY)) * 1.2); var campWeight = 1 - avoidanceWeight; var campVecX = Math.sin(aiTargetRotation) * campWeight; var campVecY = -Math.cos(aiTargetRotation) * campWeight; var avoidVecX = Math.sin(avoidanceAngle) * avoidanceWeight; var avoidVecY = -Math.cos(avoidanceAngle) * avoidanceWeight; var blendedVecX = campVecX + avoidVecX; var blendedVecY = campVecY + avoidVecY; aiTargetRotation = Math.atan2(blendedVecX, -blendedVecY); } } else { // Enhanced Free roam logic with dynamic behavior patterns if (typeof enemyCar.freeRoamAngle === "undefined" || enemyCar.aiModeTimer === 0) { // Initialize free roam variables enemyCar.freeRoamAngle = Math.random() * Math.PI * 2; enemyCar.freeRoamSpeed = maxSpeed * (0.4 + Math.random() * 0.4); // 40%-80% of max speed enemyCar.freeRoamTimer = 0; enemyCar.freeRoamDirectionTimer = 0; enemyCar.freeRoamPattern = Math.floor(Math.random() * 3); // 0=wander, 1=circular, 2=aggressive } // Dynamic free roam behavior - change direction periodically for more interesting movement enemyCar.freeRoamTimer++; enemyCar.freeRoamDirectionTimer++; // Change direction every 1-3 seconds based on pattern var directionChangeInterval; switch (enemyCar.freeRoamPattern) { case 0: // Wander pattern - gentle direction changes directionChangeInterval = 90 + Math.floor(Math.random() * 60); // 1.5-2.5 seconds break; case 1: // Circular pattern - more frequent turns directionChangeInterval = 45 + Math.floor(Math.random() * 30); // 0.75-1.25 seconds break; case 2: // Aggressive pattern - quick direction changes directionChangeInterval = 30 + Math.floor(Math.random() * 40); // 0.5-1.17 seconds break; } if (enemyCar.freeRoamDirectionTimer > directionChangeInterval) { enemyCar.freeRoamDirectionTimer = 0; // Different behavior patterns for more variety switch (enemyCar.freeRoamPattern) { case 0: // Wander - small random direction changes var angleChange = (Math.random() - 0.5) * Math.PI * 0.6; // ±54 degrees enemyCar.freeRoamAngle += angleChange; enemyCar.freeRoamSpeed = maxSpeed * (0.3 + Math.random() * 0.4); break; case 1: // Circular - tends to turn in one direction if (typeof enemyCar.circularDirection === "undefined") { enemyCar.circularDirection = Math.random() < 0.5 ? -1 : 1; } var circularTurn = enemyCar.circularDirection * (Math.PI * 0.3 + Math.random() * Math.PI * 0.4); // 54-126 degrees enemyCar.freeRoamAngle += circularTurn; enemyCar.freeRoamSpeed = maxSpeed * (0.5 + Math.random() * 0.3); // Occasionally reverse direction if (Math.random() < 0.15) enemyCar.circularDirection *= -1; break; case 2: // Aggressive - sharp turns and speed changes var aggressiveTurn = (Math.random() - 0.5) * Math.PI * 1.2; // ±108 degrees enemyCar.freeRoamAngle += aggressiveTurn; enemyCar.freeRoamSpeed = maxSpeed * (0.6 + Math.random() * 0.3); break; } } // Add some target awareness even in free roam - occasionally look towards closest target var closestTarget = null; var closestDistance = Infinity; // Check distance to player var playerDistance = Math.sqrt((carPlayer.x - enemyCar.x) * (carPlayer.x - enemyCar.x) + (carPlayer.y - enemyCar.y) * (carPlayer.y - enemyCar.y)); if (playerDistance < closestDistance) { closestDistance = playerDistance; closestTarget = carPlayer; } // Check distance to other enemy cars for (var nearIdx = 0; nearIdx < enemyCars.length; nearIdx++) { if (nearIdx !== aiCarIdx) { var otherCar = enemyCars[nearIdx]; var otherDistance = Math.sqrt((otherCar.x - enemyCar.x) * (otherCar.x - enemyCar.x) + (otherCar.y - enemyCar.y) * (otherCar.y - enemyCar.y)); if (otherDistance < closestDistance) { closestDistance = otherDistance; closestTarget = otherCar; } } } var targetInfluence = 0; if (closestTarget && closestDistance < 400) { // Within 400 pixels // Closer target = more influence on direction targetInfluence = Math.max(0, (400 - closestDistance) / 400 * 0.3); // Up to 30% influence if (Math.random() < 0.02) { // 2% chance per frame to look at closest target var targetAngle = Math.atan2(closestTarget.x - enemyCar.x, -(closestTarget.y - enemyCar.y)); // Blend current angle with target angle var currentVecX = Math.sin(enemyCar.freeRoamAngle); var currentVecY = -Math.cos(enemyCar.freeRoamAngle); var targetVecX = Math.sin(targetAngle) * targetInfluence; var targetVecY = -Math.cos(targetAngle) * targetInfluence; var blendedVecX = currentVecX * (1 - targetInfluence) + targetVecX; var blendedVecY = currentVecY * (1 - targetInfluence) + targetVecY; enemyCar.freeRoamAngle = Math.atan2(blendedVecX, -blendedVecY); } } aiTargetRotation = enemyCar.freeRoamAngle; aiPower = 1; aiTargetVelocity = enemyCar.freeRoamSpeed; // Anti-clustering avoidance for free roam mode var freeRoamClusterAvoidanceX = 0; var freeRoamClusterAvoidanceY = 0; var freeRoamClusterRadius = enemyCar.tacticalPersonality.clusterAvoidanceRadius * 1.2; // Slightly larger radius in free roam var freeRoamAvoidanceStrength = enemyCar.tacticalPersonality.antiGroupingPreference * 0.3; // Check for nearby cars in free roam var freeRoamNearbyCars = 0; for (var freeRoamAvoidIdx = 0; freeRoamAvoidIdx < enemyCars.length; freeRoamAvoidIdx++) { if (freeRoamAvoidIdx !== aiCarIdx) { var otherFreeRoamCar = enemyCars[freeRoamAvoidIdx]; var distToFreeRoamOther = Math.sqrt((enemyCar.x - otherFreeRoamCar.x) * (enemyCar.x - otherFreeRoamCar.x) + (enemyCar.y - otherFreeRoamCar.y) * (enemyCar.y - otherFreeRoamCar.y)); if (distToFreeRoamOther < freeRoamClusterRadius && distToFreeRoamOther > 0) { freeRoamNearbyCars++; // Calculate avoidance force var freeRoamAvoidForceX = (enemyCar.x - otherFreeRoamCar.x) / distToFreeRoamOther; var freeRoamAvoidForceY = (enemyCar.y - otherFreeRoamCar.y) / distToFreeRoamOther; var freeRoamProximityFactor = Math.max(0, (freeRoamClusterRadius - distToFreeRoamOther) / freeRoamClusterRadius); freeRoamClusterAvoidanceX += freeRoamAvoidForceX * freeRoamProximityFactor * freeRoamAvoidanceStrength; freeRoamClusterAvoidanceY += freeRoamAvoidForceY * freeRoamProximityFactor * freeRoamAvoidanceStrength; } } } // Encourage spread-out behavior in free roam if too clustered if (freeRoamNearbyCars > enemyCar.tacticalPersonality.maxClusterTolerance) { // Bias free roam movement away from center to spread cars out var toCenterX = 1024 - enemyCar.x; var toCenterY = 1093 - enemyCar.y; var distanceFromCenter = Math.sqrt(toCenterX * toCenterX + toCenterY * toCenterY); if (distanceFromCenter < 300) { // Too close to center and clustered - move toward edges var spreadOutX = -toCenterX / distanceFromCenter * 0.4; var spreadOutY = -toCenterY / distanceFromCenter * 0.4; freeRoamClusterAvoidanceX += spreadOutX; freeRoamClusterAvoidanceY += spreadOutY; } } // Apply combined edge and cluster avoidance to free roam mode var totalFreeRoamAvoidanceX = edgeAvoidanceX + freeRoamClusterAvoidanceX; var totalFreeRoamAvoidanceY = edgeAvoidanceY + freeRoamClusterAvoidanceY; if (totalFreeRoamAvoidanceX !== 0 || totalFreeRoamAvoidanceY !== 0) { // Calculate combined avoidance angle var avoidanceAngle = Math.atan2(totalFreeRoamAvoidanceX, -totalFreeRoamAvoidanceY); // Much stronger avoidance in free roam mode var avoidanceWeight = Math.min(0.85, (Math.abs(totalFreeRoamAvoidanceX) + Math.abs(totalFreeRoamAvoidanceY)) * 1.5); var roamWeight = 1 - avoidanceWeight; // Convert angles to vectors for blending var roamVecX = Math.sin(aiTargetRotation) * roamWeight; var roamVecY = -Math.cos(aiTargetRotation) * roamWeight; var avoidVecX = Math.sin(avoidanceAngle) * avoidanceWeight; var avoidVecY = -Math.cos(avoidanceAngle) * avoidanceWeight; // Combine and convert back to angle var blendedVecX = roamVecX + avoidVecX; var blendedVecY = roamVecY + avoidVecY; aiTargetRotation = Math.atan2(blendedVecX, -blendedVecY); // Update the free roam angle to new direction after avoidance enemyCar.freeRoamAngle = aiTargetRotation; } } // AI: Smoothly rotate enemy car towards target if (typeof enemyCar.aiRotation === "undefined") enemyCar.aiRotation = enemyCar.rotation; var aiRotationDelta = aiTargetRotation - enemyCar.aiRotation; while (aiRotationDelta > Math.PI) aiRotationDelta -= 2 * Math.PI; while (aiRotationDelta < -Math.PI) aiRotationDelta += 2 * Math.PI; var aiRotationSpeed = baseRotationSpeed * 0.7; // Slightly slower turning for AI enemyCar.aiRotation += aiRotationDelta * aiRotationSpeed; enemyCar.rotation = enemyCar.aiRotation; // AI: Velocity logic if (typeof enemyCar.aiCurrentVelocity === "undefined") enemyCar.aiCurrentVelocity = 0; if (aiPower > 0.1) { // Accelerate var aiVelocityDiff = aiTargetVelocity - enemyCar.aiCurrentVelocity; var aiAccelerationRate = 0.003; enemyCar.aiCurrentVelocity += aiVelocityDiff * aiAccelerationRate; } else { // Decelerate var aiDecelerationRate = 0.045; enemyCar.aiCurrentVelocity *= 1 - aiDecelerationRate; if (Math.abs(enemyCar.aiCurrentVelocity) < 0.1) enemyCar.aiCurrentVelocity = 0; } enemyCar.aiCurrentVelocity = Math.min(enemyCar.aiCurrentVelocity, maxSpeed * 0.92); // AI: Intended movement direction var aiIntendedMoveX = Math.sin(enemyCar.aiRotation) * enemyCar.aiCurrentVelocity; var aiIntendedMoveY = -Math.cos(enemyCar.aiRotation) * enemyCar.aiCurrentVelocity; // AI: Drift physics for enemy car if (typeof enemyCar.aiVelocityX === "undefined") enemyCar.aiVelocityX = 0; if (typeof enemyCar.aiVelocityY === "undefined") enemyCar.aiVelocityY = 0; enemyCar.aiVelocityX = enemyCar.aiVelocityX * driftFactor + aiIntendedMoveX * gripFactor; enemyCar.aiVelocityY = enemyCar.aiVelocityY * driftFactor + aiIntendedMoveY * gripFactor; // AI: Apply friction and update position enemyCar.aiVelocityX *= 0.98; enemyCar.aiVelocityY *= 0.98; enemyCar.x += enemyCar.aiVelocityX + enemyCar.velocityX; enemyCar.y += enemyCar.aiVelocityY + enemyCar.velocityY; // --- End Enemy Car AI Movement Logic --- // Apply smooth braking to enemy car after collision for more natural deceleration if (!enemyCar.smoothBraking) enemyCar.smoothBraking = false; if (!enemyCar.brakeFrames) enemyCar.brakeFrames = 0; var currentEnemySpeed = Math.sqrt(enemyCar.velocityX * enemyCar.velocityX + enemyCar.velocityY * enemyCar.velocityY); // If enemy car was just launched (high velocity), enable smooth braking if (currentEnemySpeed > maxSpeed * 0.6 && !enemyCar.smoothBraking) { enemyCar.smoothBraking = true; enemyCar.brakeFrames = 0; } // Smooth braking logic: apply a gentle, progressive friction for a short period after being launched if (enemyCar.smoothBraking) { // Braking lasts for 18 frames (~0.3s at 60fps) var brakeDuration = 18; var brakeProgress = Math.min(1, enemyCar.brakeFrames / brakeDuration); // Start with gentle friction, increase to normal friction var minFriction = 0.96; var maxFriction = 0.92 - currentEnemySpeed / maxSpeed * 0.05; var frictionRate = minFriction + (maxFriction - minFriction) * brakeProgress; enemyCar.velocityX *= frictionRate; enemyCar.velocityY *= frictionRate; enemyCar.brakeFrames++; if (enemyCar.brakeFrames >= brakeDuration) { enemyCar.smoothBraking = false; } } else if (currentEnemySpeed > 0.1) { // Normal progressive friction var frictionRate = 0.92 - currentEnemySpeed / maxSpeed * 0.05; // More friction at higher speeds enemyCar.velocityX *= frictionRate; enemyCar.velocityY *= frictionRate; } else { // Stop very slow movement to prevent endless drift enemyCar.velocityX = 0; enemyCar.velocityY = 0; } // Keep enemy car within bounds var enemyHalfWidth = 32; var enemyHalfHeight = 47; if (enemyCar.x < enemyHalfWidth) { enemyCar.x = enemyHalfWidth; enemyCar.velocityX = -enemyCar.velocityX * 0.6; } if (enemyCar.x > 2048 - enemyHalfWidth) { enemyCar.x = 2048 - enemyHalfWidth; enemyCar.velocityX = -enemyCar.velocityX * 0.6; } if (enemyCar.y < enemyHalfHeight) { enemyCar.y = enemyHalfHeight; enemyCar.velocityY = -enemyCar.velocityY * 0.6; } if (enemyCar.y > 2186 - enemyHalfHeight) { enemyCar.y = 2186 - enemyHalfHeight; enemyCar.velocityY = -enemyCar.velocityY * 0.6; } } // --- End Enemy Cars AI Movement Logic --- // Add collision detection between enemy cars (optimized - only check every 3rd frame) if (LK.ticks % 3 === 0) { for (var carA = 0; carA < enemyCars.length; carA++) { for (var carB = carA + 1; carB < enemyCars.length; carB++) { var enemyCarA = enemyCars[carA]; var enemyCarB = enemyCars[carB]; // Initialize collision tracking if not exists if (!enemyCarA.enemyColliding) enemyCarA.enemyColliding = []; if (!enemyCarB.enemyColliding) enemyCarB.enemyColliding = []; while (enemyCarA.enemyColliding.length <= carB) enemyCarA.enemyColliding.push(false); while (enemyCarB.enemyColliding.length <= carA) enemyCarB.enemyColliding.push(false); // Calculate collision boxes for both cars var carALeft = enemyCarA.x - enemyCollisionWidth / 2; var carARight = enemyCarA.x + enemyCollisionWidth / 2; var carATop = enemyCarA.y - enemyCollisionHeight / 2; var carABottom = enemyCarA.y + enemyCollisionHeight / 2; var carBLeft = enemyCarB.x - enemyCollisionWidth / 2; var carBRight = enemyCarB.x + enemyCollisionWidth / 2; var carBTop = enemyCarB.y - enemyCollisionHeight / 2; var carBBottom = enemyCarB.y + enemyCollisionHeight / 2; // Check for collision var currentColliding = !(carARight < carBLeft || carALeft > carBRight || carABottom < carBTop || carATop > carBBottom); if (!enemyCarA.enemyColliding[carB] && currentColliding) { // Collision just started - determine collision responsibility for enemy cars var carASpeedX = (enemyCarA.aiVelocityX || 0) + (enemyCarA.velocityX || 0); var carASpeedY = (enemyCarA.aiVelocityY || 0) + (enemyCarA.velocityY || 0); var carBSpeedX = (enemyCarB.aiVelocityX || 0) + (enemyCarB.velocityX || 0); var carBSpeedY = (enemyCarB.aiVelocityY || 0) + (enemyCarB.velocityY || 0); var carASpeed = Math.sqrt(carASpeedX * carASpeedX + carASpeedY * carASpeedY); var carBSpeed = Math.sqrt(carBSpeedX * carBSpeedX + carBSpeedY * carBSpeedY); // Calculate collision direction var collisionDeltaX = enemyCarB.x - enemyCarA.x; var collisionDeltaY = enemyCarB.y - enemyCarA.y; var collisionDistance = Math.sqrt(collisionDeltaX * collisionDeltaX + collisionDeltaY * collisionDeltaY); // Normalize collision direction if (collisionDistance > 0) { collisionDeltaX /= collisionDistance; collisionDeltaY /= collisionDistance; } // Determine collision responsibility for enemy cars var carACrasher = false; var carBCrasher = false; var bothEnemiesCrashed = false; // Calculate approach vectors var carAApproachX = 0, carAApproachY = 0; var carBApproachX = 0, carBApproachY = 0; if (carASpeed > 0.1) { carAApproachX = carASpeedX / carASpeed; carAApproachY = carASpeedY / carASpeed; } if (carBSpeed > 0.1) { carBApproachX = carBSpeedX / carBSpeed; carBApproachY = carBSpeedY / carBSpeed; } // Calculate how much each car is moving toward the collision var carATowardCollision = 0; var carBTowardCollision = 0; if (carASpeed > 0.1) { carATowardCollision = carAApproachX * collisionDeltaX + carAApproachY * collisionDeltaY; } if (carBSpeed > 0.1) { carBTowardCollision = carBApproachX * -collisionDeltaX + carBApproachY * -collisionDeltaY; } // Speed thresholds for determining crasher responsibility var minCrasherSpeed = maxSpeed * 0.15; var dominantCrasherThreshold = 0.3; // Determine responsibility if (carASpeed > minCrasherSpeed && carBSpeed > minCrasherSpeed) { if (carATowardCollision > dominantCrasherThreshold && carBTowardCollision > dominantCrasherThreshold) { bothEnemiesCrashed = true; } else if (carATowardCollision > carBTowardCollision + dominantCrasherThreshold) { carACrasher = true; } else if (carBTowardCollision > carATowardCollision + dominantCrasherThreshold) { carBCrasher = true; } else { bothEnemiesCrashed = true; } } else if (carASpeed > minCrasherSpeed && carBSpeed <= minCrasherSpeed) { carACrasher = true; } else if (carBSpeed > minCrasherSpeed && carASpeed <= minCrasherSpeed) { carBCrasher = true; } else { bothEnemiesCrashed = true; } // Calculate momentum transfer using conservation of momentum var totalMass = enemyCarA.weight + enemyCarB.weight; var massRatioA1 = (enemyCarA.weight - enemyCarB.weight) / totalMass; var massRatioA2 = 2 * enemyCarB.weight / totalMass; var massRatioB1 = 2 * enemyCarA.weight / totalMass; var massRatioB2 = (enemyCarB.weight - enemyCarA.weight) / totalMass; // Apply different energy loss based on collision responsibility var relativeSpeed = Math.max(carASpeed, carBSpeed); var minLoss, maxLoss; var speedNorm = Math.min(1, relativeSpeed / maxSpeed); if (bothEnemiesCrashed) { minLoss = 0.18; maxLoss = 0.60; } else { minLoss = 0.12; maxLoss = 0.50; } var energyLoss = minLoss + (maxLoss - minLoss) * speedNorm; var restitution = 1 - energyLoss; // Apply different restitution based on responsibility var carARestitution = restitution; var carBRestitution = restitution; if (carACrasher) { carARestitution = restitution * 1.15; // Car A is crasher - less penalty carBRestitution = restitution * 0.85; // Car B crashed into - more penalty } else if (carBCrasher) { carBRestitution = restitution * 1.15; // Car B is crasher - less penalty carARestitution = restitution * 0.85; // Car A crashed into - more penalty } // Separate cars to prevent overlap var separationDistance = 75; var halfSeparation = separationDistance / 2; enemyCarA.x = enemyCarA.x - collisionDeltaX * halfSeparation; enemyCarA.y = enemyCarA.y - collisionDeltaY * halfSeparation; enemyCarB.x = enemyCarB.x + collisionDeltaX * halfSeparation; enemyCarB.y = enemyCarB.y + collisionDeltaY * halfSeparation; // Calculate new velocities for car A var carAImpactX = carASpeedX * massRatioA1 + carBSpeedX * massRatioA2; var carAImpactY = carASpeedY * massRatioA1 + carBSpeedY * massRatioA2; enemyCarA.velocityX = carAImpactX * carARestitution; enemyCarA.velocityY = carAImpactY * carARestitution; // Calculate new velocities for car B var carBImpactX = carASpeedX * massRatioB1 + carBSpeedX * massRatioB2; var carBImpactY = carASpeedY * massRatioB1 + carBSpeedY * massRatioB2; enemyCarB.velocityX = carBImpactX * carBRestitution; enemyCarB.velocityY = carBImpactY * carBRestitution; // Add directional push based on collision var pushIntensity = (carASpeed + carBSpeed) * (0.4 + 0.4 * speedNorm); var carAPushRatio = enemyCarB.weight / totalMass; var carBPushRatio = enemyCarA.weight / totalMass; enemyCarA.velocityX += -collisionDeltaX * pushIntensity * carAPushRatio * 0.6; enemyCarA.velocityY += -collisionDeltaY * pushIntensity * carAPushRatio * 0.6; enemyCarB.velocityX += collisionDeltaX * pushIntensity * carBPushRatio * 0.6; enemyCarB.velocityY += collisionDeltaY * pushIntensity * carBPushRatio * 0.6; // Reset acceleration state based on responsibility if (carACrasher) { if (typeof enemyCarA.aiCurrentVelocity !== "undefined") { enemyCarA.aiCurrentVelocity *= 0.3; // Car A is crasher - smaller penalty } } else { if (typeof enemyCarA.aiCurrentVelocity !== "undefined") { enemyCarA.aiCurrentVelocity = 0; // Car A crashed into or mutual - full reset } } if (carBCrasher) { if (typeof enemyCarB.aiCurrentVelocity !== "undefined") { enemyCarB.aiCurrentVelocity *= 0.3; // Car B is crasher - smaller penalty } } else { if (typeof enemyCarB.aiCurrentVelocity !== "undefined") { enemyCarB.aiCurrentVelocity = 0; // Car B crashed into or mutual - full reset } } // Calculate speed-based damage for enemy collision var enemyCollisionSpeed = Math.max(carASpeed, carBSpeed); var baseEnemyDamage = calculateSpeedDamage(enemyCollisionSpeed); // Apply damage with responsibility-based penalties var carADamage = 0; var carBDamage = 0; if (bothEnemiesCrashed) { // Both crashed - equal damage carADamage = baseEnemyDamage; carBDamage = baseEnemyDamage; } else if (carACrasher) { // Car A is crasher - takes less damage carADamage = Math.round(baseEnemyDamage * 0.7); // 30% less damage carBDamage = Math.round(baseEnemyDamage * 1.3); // 30% more damage } else if (carBCrasher) { // Car B is crasher - takes less damage carBDamage = Math.round(baseEnemyDamage * 0.7); // 30% less damage carADamage = Math.round(baseEnemyDamage * 1.3); // 30% more damage } else { // Fallback - equal damage carADamage = baseEnemyDamage; carBDamage = baseEnemyDamage; } // Apply damage to car A if (carADamage > 0) { enemyCarA.health = Math.max(0, enemyCarA.health - carADamage); } // Apply damage to car B if (carBDamage > 0) { enemyCarB.health = Math.max(0, enemyCarB.health - carBDamage); } // Check for destroyed cars and remove them var carsToRemove = []; if (enemyCarA.health <= 0) { carsToRemove.push({ car: enemyCarA, index: carA }); } if (enemyCarB.health <= 0) { carsToRemove.push({ car: enemyCarB, index: carB }); } // Remove destroyed cars (in reverse order to maintain indices) for (var removeIdx = carsToRemove.length - 1; removeIdx >= 0; removeIdx--) { var carToRemove = carsToRemove[removeIdx]; carToRemove.car.destroy(); enemyCars.splice(carToRemove.index, 1); // Remove corresponding health bar if (carToRemove.index < enemyHealthBars.length) { enemyHealthBars[carToRemove.index].destroy(); enemyHealthBarBgs[carToRemove.index].destroy(); enemyHealthBars.splice(carToRemove.index, 1); enemyHealthBarBgs.splice(carToRemove.index, 1); // Reposition remaining health bars for (var repositionIdx = carToRemove.index; repositionIdx < enemyHealthBars.length; repositionIdx++) { enemyHealthBars[repositionIdx].y = 80 + repositionIdx * 35; enemyHealthBarBgs[repositionIdx].y = 80 + repositionIdx * 35; } } // Adjust collision tracking arrays for all remaining cars for (var adjustIdx = 0; adjustIdx < enemyCars.length; adjustIdx++) { if (enemyCars[adjustIdx].enemyColliding && enemyCars[adjustIdx].enemyColliding.length > carToRemove.index) { enemyCars[adjustIdx].enemyColliding.splice(carToRemove.index, 1); } } // Adjust player collision tracking if (carPlayer.lastColliding && carPlayer.lastColliding.length > carToRemove.index) { carPlayer.lastColliding.splice(carToRemove.index, 1); } } // Check for victory condition if (enemyCars.length === 0) { LK.showYouWin(); return; // Exit update loop } // Skip visual feedback if cars were destroyed if (carsToRemove.length > 0) { continue; } // Visual feedback based on collision responsibility if (bothEnemiesCrashed) { // Both crashed - flash both in orange LK.effects.flashObject(enemyCarA, 0xff8800, 300); LK.effects.flashObject(enemyCarB, 0xff8800, 300); } else if (carACrasher) { // Car A crashed into car B - flash A red, B yellow LK.effects.flashObject(enemyCarA, 0xff6600, 250); LK.effects.flashObject(enemyCarB, 0xffdd00, 350); } else if (carBCrasher) { // Car B crashed into car A - flash B red, A yellow LK.effects.flashObject(enemyCarB, 0xff6600, 250); LK.effects.flashObject(enemyCarA, 0xffdd00, 350); } else { // Default fallback LK.effects.flashObject(enemyCarA, 0xffaa00, 300); LK.effects.flashObject(enemyCarB, 0xffaa00, 300); } } // Update collision tracking enemyCarA.enemyColliding[carB] = currentColliding; enemyCarB.enemyColliding[carA] = currentColliding; } } } // Update player health bar scale var healthPercentage = playerHealth / maxHealth; healthBar.scaleX = 4 * healthPercentage; // Update enemy health bars for (var healthIdx = 0; healthIdx < enemyCars.length && healthIdx < enemyHealthBars.length; healthIdx++) { var enemyHealthPercentage = enemyCars[healthIdx].health / maxHealth; enemyHealthBars[healthIdx].scaleX = 4 * enemyHealthPercentage; } // Remove excess health bars if we have more bars than cars while (enemyHealthBars.length > enemyCars.length) { var removedBar = enemyHealthBars.pop(); var removedBg = enemyHealthBarBgs.pop(); if (removedBar) removedBar.destroy(); if (removedBg) removedBg.destroy(); } // Add health bars if we have more cars than bars while (enemyHealthBars.length < enemyCars.length) { var newCarIndex = enemyHealthBars.length; var newEnemyCarGraphics = enemyCars[newCarIndex].children[0]; var newEnemyColor = newEnemyCarGraphics.tint || 0xffffff; createEnemyHealthBar(newCarIndex, newEnemyColor); } // Update speed display var totalSpeed = Math.sqrt(velocityX * velocityX + velocityY * velocityY); speedText.setText('Speed: ' + Math.round(totalSpeed)); };
===================================================================
--- original.js
+++ change.js
@@ -263,8 +263,13 @@
campingDistance: 200 + Math.random() * 150,
// 200-350 preferred camping distance
opportunismLevel: 0.7 + Math.random() * 0.3 // 0.7-1.0 how opportunistic the camper is
};
+ // Add anti-grouping preferences for smarter car distribution
+ enemyCar.tacticalPersonality.antiGroupingPreference = 0.4 + Math.random() * 0.5; // 0.4-0.9 preference for avoiding groups
+ enemyCar.tacticalPersonality.isolationSeekingLevel = 0.3 + Math.random() * 0.6; // 0.3-0.9 how much to seek isolated targets
+ enemyCar.tacticalPersonality.clusterAvoidanceRadius = 120 + Math.random() * 80; // 120-200 pixels to consider as "clustered"
+ enemyCar.tacticalPersonality.maxClusterTolerance = 2 + Math.floor(Math.random() * 2); // Max 2-3 cars in nearby area before avoiding
// Add to array and scene
enemyCars.push(enemyCar);
gameplayBackground.addChild(enemyCar);
}
@@ -1251,8 +1256,44 @@
targetHealth = target.object.health;
} else if (target.isPlayer && typeof playerHealth !== "undefined") {
targetHealth = playerHealth;
}
+ // --- New: Anti-grouping and isolation preference system ---
+ var isolationScore = 0;
+ var clusterPenalty = 0;
+ // Count nearby cars around this target
+ var nearbyCarCount = 0;
+ var clusterRadius = enemyCar.tacticalPersonality.clusterAvoidanceRadius;
+ // Check distance to all other potential targets around this target
+ for (var clusterCheck = 0; clusterCheck < potentialTargets.length; clusterCheck++) {
+ if (clusterCheck !== evalIdx) {
+ var otherTarget = potentialTargets[clusterCheck];
+ var distanceToOther = Math.sqrt((target.object.x - otherTarget.object.x) * (target.object.x - otherTarget.object.x) + (target.object.y - otherTarget.object.y) * (target.object.y - otherTarget.object.y));
+ if (distanceToOther < clusterRadius) {
+ nearbyCarCount++;
+ }
+ }
+ }
+ // Calculate isolation bonus - prefer targets that are alone
+ if (nearbyCarCount === 0) {
+ // Target is completely isolated - major bonus
+ isolationScore = enemyCar.tacticalPersonality.isolationSeekingLevel * 25;
+ } else if (nearbyCarCount === 1) {
+ // Target has one nearby car - moderate bonus
+ isolationScore = enemyCar.tacticalPersonality.isolationSeekingLevel * 15;
+ } else {
+ // Target is in a cluster - potential penalty
+ isolationScore = 0;
+ }
+ // Calculate cluster avoidance penalty
+ if (nearbyCarCount > enemyCar.tacticalPersonality.maxClusterTolerance) {
+ // Too many cars clustered around target - apply penalty
+ var excessCars = nearbyCarCount - enemyCar.tacticalPersonality.maxClusterTolerance;
+ clusterPenalty = enemyCar.tacticalPersonality.antiGroupingPreference * excessCars * 20;
+ }
+ // Apply isolation bonus and cluster penalty
+ score += isolationScore;
+ score -= clusterPenalty;
// Base health scoring - prefer weaker targets
var healthScore = Math.max(0, 30 - targetHealth / maxHealth * 30); // 0-30 points, lower health = higher score
// Camper behavior modification
if (enemyCar.tacticalPersonality.isCamper) {
@@ -1284,56 +1325,8 @@
healthScore *= 1.5; // Increase health preference
}
}
score += healthScore;
- // --- NEW: Anti-grouping preference system - prefer isolated targets ---
- var isolationScore = 0;
- var isolationRange = 180; // Range to check for other cars around target
- var otherCarsNearTarget = 0;
- var totalNearbyDistance = 0;
- // Count how many other cars are near this target
- for (var isolationCheck = 0; isolationCheck < enemyCars.length + 1; isolationCheck++) {
- var checkCar = null;
- if (isolationCheck < enemyCars.length && isolationCheck !== aiCarIdx) {
- checkCar = enemyCars[isolationCheck];
- } else if (isolationCheck === enemyCars.length && !target.isPlayer) {
- checkCar = carPlayer; // Don't count player when checking player isolation
- }
- if (checkCar && checkCar !== target.object) {
- var distanceToTarget = Math.sqrt((checkCar.x - target.object.x) * (checkCar.x - target.object.x) + (checkCar.y - target.object.y) * (checkCar.y - target.object.y));
- if (distanceToTarget < isolationRange) {
- otherCarsNearTarget++;
- totalNearbyDistance += distanceToTarget;
- }
- }
- }
- // Calculate isolation bonus - prefer targets that are alone
- if (otherCarsNearTarget === 0) {
- // Perfect isolation - major bonus
- isolationScore = 25;
- } else if (otherCarsNearTarget === 1) {
- // One other car nearby - moderate bonus based on distance
- var avgDistance = totalNearbyDistance / otherCarsNearTarget;
- isolationScore = Math.max(5, 20 - avgDistance / isolationRange * 15); // 5-20 points
- } else {
- // Multiple cars nearby - penalty for crowded targets
- isolationScore = -10 * (otherCarsNearTarget - 1); // -10 points per extra car
- }
- // Additional bonus for targets that would separate us from current groups
- var myCurrentGroupSize = 0;
- for (var myGroupCheck = 0; myGroupCheck < enemyCars.length; myGroupCheck++) {
- if (myGroupCheck !== aiCarIdx) {
- var distanceToMe = Math.sqrt((enemyCars[myGroupCheck].x - enemyCar.x) * (enemyCars[myGroupCheck].x - enemyCar.x) + (enemyCars[myGroupCheck].y - enemyCar.y) * (enemyCars[myGroupCheck].y - enemyCar.y));
- if (distanceToMe < isolationRange) {
- myCurrentGroupSize++;
- }
- }
- }
- // If we're currently in a group, prefer targets that lead us away
- if (myCurrentGroupSize > 0) {
- isolationScore += myCurrentGroupSize * 8; // Bonus for breaking away from groups
- }
- score += isolationScore;
// Distance factor - prefer closer targets (0-30 points, slightly reduced to balance health)
var distanceScore = Math.max(0, 30 - targetDistance / 1000 * 30);
score += distanceScore;
// Speed factor - prefer faster targets for more exciting gameplay (0-20 points)
@@ -2026,49 +2019,47 @@
}
}
break;
}
- // --- NEW: Anti-grouping behavior - avoid clustering with other cars ---
- var antigroupingX = 0;
- var antigroupingY = 0;
- var groupingAvoidanceRange = 120; // Distance to start avoiding other cars
- var groupingAvoidanceStrength = 0.4; // How strongly to avoid clustering
- // Check distance from other enemy cars and calculate repulsion force
- for (var antigroupIdx = 0; antigroupIdx < enemyCars.length; antigroupIdx++) {
- if (antigroupIdx !== aiCarIdx) {
- var otherCar = enemyCars[antigroupIdx];
- var distanceToOther = Math.sqrt((otherCar.x - enemyCar.x) * (otherCar.x - enemyCar.x) + (otherCar.y - enemyCar.y) * (otherCar.y - enemyCar.y));
- if (distanceToOther < groupingAvoidanceRange && distanceToOther > 10) {
- // Too close to another car - calculate repulsion force
- var repulsionStrength = (groupingAvoidanceRange - distanceToOther) / groupingAvoidanceRange;
- var repulsionForce = repulsionStrength * groupingAvoidanceStrength;
- // Calculate direction away from other car
- var awayX = (enemyCar.x - otherCar.x) / distanceToOther;
- var awayY = (enemyCar.y - otherCar.y) / distanceToOther;
- antigroupingX += awayX * repulsionForce;
- antigroupingY += awayY * repulsionForce;
+ // Anti-clustering avoidance - check for nearby friendly cars to avoid grouping
+ var clusterAvoidanceX = 0;
+ var clusterAvoidanceY = 0;
+ var clusterAvoidanceRadius = enemyCar.tacticalPersonality.clusterAvoidanceRadius;
+ var clusterAvoidanceStrength = enemyCar.tacticalPersonality.antiGroupingPreference * 0.4;
+ // Count and avoid nearby friendly cars
+ var nearbyCarsCount = 0;
+ for (var avoidIdx = 0; avoidIdx < enemyCars.length; avoidIdx++) {
+ if (avoidIdx !== aiCarIdx) {
+ var otherEnemyCar = enemyCars[avoidIdx];
+ var distToOther = Math.sqrt((enemyCar.x - otherEnemyCar.x) * (enemyCar.x - otherEnemyCar.x) + (enemyCar.y - otherEnemyCar.y) * (enemyCar.y - otherEnemyCar.y));
+ if (distToOther < clusterAvoidanceRadius && distToOther > 0) {
+ nearbyCarsCount++;
+ // Calculate avoidance force (push away from other car)
+ var avoidForceX = (enemyCar.x - otherEnemyCar.x) / distToOther;
+ var avoidForceY = (enemyCar.y - otherEnemyCar.y) / distToOther;
+ // Stronger avoidance when closer
+ var proximityFactor = Math.max(0, (clusterAvoidanceRadius - distToOther) / clusterAvoidanceRadius);
+ clusterAvoidanceX += avoidForceX * proximityFactor * clusterAvoidanceStrength;
+ clusterAvoidanceY += avoidForceY * proximityFactor * clusterAvoidanceStrength;
}
}
}
- // Also avoid getting too close to player when not specifically targeting them
- if (enemyCar.currentTarget && !enemyCar.currentTarget.isPlayer) {
- var distanceToPlayer = Math.sqrt((carPlayer.x - enemyCar.x) * (carPlayer.x - enemyCar.x) + (carPlayer.y - enemyCar.y) * (carPlayer.y - enemyCar.y));
- if (distanceToPlayer < groupingAvoidanceRange * 0.8 && distanceToPlayer > 10) {
- var playerRepulsionStrength = (groupingAvoidanceRange * 0.8 - distanceToPlayer) / (groupingAvoidanceRange * 0.8);
- var playerRepulsionForce = playerRepulsionStrength * groupingAvoidanceStrength * 0.6; // Weaker than car-to-car avoidance
- var awayFromPlayerX = (enemyCar.x - carPlayer.x) / distanceToPlayer;
- var awayFromPlayerY = (enemyCar.y - carPlayer.y) / distanceToPlayer;
- antigroupingX += awayFromPlayerX * playerRepulsionForce;
- antigroupingY += awayFromPlayerY * playerRepulsionForce;
- }
+ // Apply cluster penalty to movement if too many cars nearby
+ var clusterPenalty = 1.0;
+ if (nearbyCarsCount > enemyCar.tacticalPersonality.maxClusterTolerance) {
+ // Reduce speed and add more aggressive avoidance when clustered
+ clusterPenalty = 0.7 - (nearbyCarsCount - enemyCar.tacticalPersonality.maxClusterTolerance) * 0.15;
+ clusterPenalty = Math.max(0.3, clusterPenalty);
+ // Increase avoidance strength when over tolerance
+ clusterAvoidanceStrength *= 1.5;
}
- // Blend in edge avoidance and anti-grouping with follow behavior
- var totalAvoidanceX = edgeAvoidanceX + antigroupingX;
- var totalAvoidanceY = edgeAvoidanceY + antigroupingY;
+ // Blend in edge avoidance and cluster avoidance with follow behavior
+ var totalAvoidanceX = edgeAvoidanceX + clusterAvoidanceX;
+ var totalAvoidanceY = edgeAvoidanceY + clusterAvoidanceY;
if (totalAvoidanceX !== 0 || totalAvoidanceY !== 0) {
// Calculate combined avoidance angle
var avoidanceAngle = Math.atan2(totalAvoidanceX, -totalAvoidanceY);
- // Blend the target rotation with avoidance (stronger when closer to edges/other cars)
+ // Blend the target rotation with avoidance (stronger when closer to edges or clusters)
var avoidanceWeight = Math.min(0.8, Math.abs(totalAvoidanceX) + Math.abs(totalAvoidanceY));
var followWeight = 1 - avoidanceWeight;
// Convert angles to vectors for blending
var followVecX = Math.sin(aiTargetRotation) * followWeight;
@@ -2078,8 +2069,10 @@
// Combine and convert back to angle
var blendedVecX = followVecX + avoidVecX;
var blendedVecY = followVecY + avoidVecY;
aiTargetRotation = Math.atan2(blendedVecX, -blendedVecY);
+ // Apply cluster penalty to target velocity
+ aiTargetVelocity *= clusterPenalty;
}
} else if (enemyCar.aiMode === "evade") {
// Evasion mode - flee from pursuers with different patterns
if (typeof enemyCar.evasionState === "undefined" || enemyCar.aiModeTimer === 0) {
@@ -2488,14 +2481,52 @@
}
aiTargetRotation = enemyCar.freeRoamAngle;
aiPower = 1;
aiTargetVelocity = enemyCar.freeRoamSpeed;
- // Apply edge avoidance to free roam mode with stronger influence
- if (edgeAvoidanceX !== 0 || edgeAvoidanceY !== 0) {
- // Calculate edge avoidance angle
- var avoidanceAngle = Math.atan2(edgeAvoidanceX, -edgeAvoidanceY);
+ // Anti-clustering avoidance for free roam mode
+ var freeRoamClusterAvoidanceX = 0;
+ var freeRoamClusterAvoidanceY = 0;
+ var freeRoamClusterRadius = enemyCar.tacticalPersonality.clusterAvoidanceRadius * 1.2; // Slightly larger radius in free roam
+ var freeRoamAvoidanceStrength = enemyCar.tacticalPersonality.antiGroupingPreference * 0.3;
+ // Check for nearby cars in free roam
+ var freeRoamNearbyCars = 0;
+ for (var freeRoamAvoidIdx = 0; freeRoamAvoidIdx < enemyCars.length; freeRoamAvoidIdx++) {
+ if (freeRoamAvoidIdx !== aiCarIdx) {
+ var otherFreeRoamCar = enemyCars[freeRoamAvoidIdx];
+ var distToFreeRoamOther = Math.sqrt((enemyCar.x - otherFreeRoamCar.x) * (enemyCar.x - otherFreeRoamCar.x) + (enemyCar.y - otherFreeRoamCar.y) * (enemyCar.y - otherFreeRoamCar.y));
+ if (distToFreeRoamOther < freeRoamClusterRadius && distToFreeRoamOther > 0) {
+ freeRoamNearbyCars++;
+ // Calculate avoidance force
+ var freeRoamAvoidForceX = (enemyCar.x - otherFreeRoamCar.x) / distToFreeRoamOther;
+ var freeRoamAvoidForceY = (enemyCar.y - otherFreeRoamCar.y) / distToFreeRoamOther;
+ var freeRoamProximityFactor = Math.max(0, (freeRoamClusterRadius - distToFreeRoamOther) / freeRoamClusterRadius);
+ freeRoamClusterAvoidanceX += freeRoamAvoidForceX * freeRoamProximityFactor * freeRoamAvoidanceStrength;
+ freeRoamClusterAvoidanceY += freeRoamAvoidForceY * freeRoamProximityFactor * freeRoamAvoidanceStrength;
+ }
+ }
+ }
+ // Encourage spread-out behavior in free roam if too clustered
+ if (freeRoamNearbyCars > enemyCar.tacticalPersonality.maxClusterTolerance) {
+ // Bias free roam movement away from center to spread cars out
+ var toCenterX = 1024 - enemyCar.x;
+ var toCenterY = 1093 - enemyCar.y;
+ var distanceFromCenter = Math.sqrt(toCenterX * toCenterX + toCenterY * toCenterY);
+ if (distanceFromCenter < 300) {
+ // Too close to center and clustered - move toward edges
+ var spreadOutX = -toCenterX / distanceFromCenter * 0.4;
+ var spreadOutY = -toCenterY / distanceFromCenter * 0.4;
+ freeRoamClusterAvoidanceX += spreadOutX;
+ freeRoamClusterAvoidanceY += spreadOutY;
+ }
+ }
+ // Apply combined edge and cluster avoidance to free roam mode
+ var totalFreeRoamAvoidanceX = edgeAvoidanceX + freeRoamClusterAvoidanceX;
+ var totalFreeRoamAvoidanceY = edgeAvoidanceY + freeRoamClusterAvoidanceY;
+ if (totalFreeRoamAvoidanceX !== 0 || totalFreeRoamAvoidanceY !== 0) {
+ // Calculate combined avoidance angle
+ var avoidanceAngle = Math.atan2(totalFreeRoamAvoidanceX, -totalFreeRoamAvoidanceY);
// Much stronger avoidance in free roam mode
- var avoidanceWeight = Math.min(0.85, (Math.abs(edgeAvoidanceX) + Math.abs(edgeAvoidanceY)) * 1.5);
+ var avoidanceWeight = Math.min(0.85, (Math.abs(totalFreeRoamAvoidanceX) + Math.abs(totalFreeRoamAvoidanceY)) * 1.5);
var roamWeight = 1 - avoidanceWeight;
// Convert angles to vectors for blending
var roamVecX = Math.sin(aiTargetRotation) * roamWeight;
var roamVecY = -Math.cos(aiTargetRotation) * roamWeight;