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