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) var enemyColors = [0x0066ff, // Blue 0x00ff66, // Green 0xffff00, // Yellow 0xff8800, // Orange 0x8800ff, // Purple 0x00ffff, // Cyan 0xff00ff, // Magenta 0x888888, // Gray 0xffffff // White ]; 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 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; } // 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; } // Blend in edge avoidance with follow behavior if (edgeAvoidanceX !== 0 || edgeAvoidanceY !== 0) { // Calculate edge avoidance angle var avoidanceAngle = Math.atan2(edgeAvoidanceX, -edgeAvoidanceY); // Blend the target rotation with avoidance (stronger when closer to edges) var avoidanceWeight = Math.min(0.7, Math.abs(edgeAvoidanceX) + Math.abs(edgeAvoidanceY)); 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); } } 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; // 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); // Much stronger avoidance in free roam mode var avoidanceWeight = Math.min(0.85, (Math.abs(edgeAvoidanceX) + Math.abs(edgeAvoidanceY)) * 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)); };
/****
* 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)
var enemyColors = [0x0066ff,
// Blue
0x00ff66,
// Green
0xffff00,
// Yellow
0xff8800,
// Orange
0x8800ff,
// Purple
0x00ffff,
// Cyan
0xff00ff,
// Magenta
0x888888,
// Gray
0xffffff // White
];
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 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;
}
// 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;
}
// Blend in edge avoidance with follow behavior
if (edgeAvoidanceX !== 0 || edgeAvoidanceY !== 0) {
// Calculate edge avoidance angle
var avoidanceAngle = Math.atan2(edgeAvoidanceX, -edgeAvoidanceY);
// Blend the target rotation with avoidance (stronger when closer to edges)
var avoidanceWeight = Math.min(0.7, Math.abs(edgeAvoidanceX) + Math.abs(edgeAvoidanceY));
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);
}
} 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;
// 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);
// Much stronger avoidance in free roam mode
var avoidanceWeight = Math.min(0.85, (Math.abs(edgeAvoidanceX) + Math.abs(edgeAvoidanceY)) * 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));
};