JavaScript · 22927 bytes Raw Blame History
1 // ai.js - AI logic and behavior
2
3 // ============= AI TECHNIQUE CONSTANTS =============
4 const AI_WINDUP_SPEED = 0.15; // Base oscillation speed
5 const AI_WINDUP_SMOOTHNESS = 1.00; // Smooth transitions
6 const AI_WINDUP_RADIUS = 240; // Circular motion radius
7 const AI_WINDUP_MIN_TIME = 300; // Minimum windup duration
8 const AI_WINDUP_MAX_TIME = 1200; // Maximum windup duration
9 const AI_BOP_AT_PEAK_CHANCE = 0.4; // Chance to bop at windup peak
10 const AI_CIRCULAR_MOTION = 0.9; // How circular vs linear the motion is
11 const AI_MOMENTUM_CARRY = 0.85; // How much momentum carries between moves
12 const AI_PHASE_SPEED = 0.1; // Speed of phase progression (radians per frame)
13
14 const AI_IDLE_MOVEMENT = 0.8; // How much AI moves when idle
15 const AI_NERVOUS_ENERGY = 0.5; // Random fidgeting energy
16 const AI_PREDICTIVE_MOVEMENT = 0.7; // How much AI moves based on prediction
17 const AI_TRACKING_AGGRESSION = 0.15; // How aggressively AI tracks the ball
18
19
20 // ============= AI SETTINGS =============
21 const AI_SETTINGS = {
22 easy: {
23 reactionTime: 350,
24 accuracy: 0.7,
25 speed: 0.8,
26 prediction: 0.3,
27 aggression: 0.4,
28 oscillation: 0.3,
29 bopChance: 0.25,
30 windupSpeed: 0.1,
31 windupRadius: 30,
32 comboBopChance: 0.1,
33 circularMotion: 0.4,
34 phaseSpeed: 0.06,
35 idleMovement: 0.3,
36 trackingAggression: 0.1
37 },
38 medium: {
39 reactionTime: 200,
40 accuracy: 0.85,
41 speed: 1.0,
42 prediction: 0.6,
43 aggression: 0.7,
44 oscillation: 0.7,
45 bopChance: 0.55,
46 windupSpeed: 0.15,
47 windupRadius: 40,
48 comboBopChance: 0.3,
49 circularMotion: 0.6,
50 phaseSpeed: 0.08,
51 idleMovement: 0.5,
52 trackingAggression: 0.15
53 },
54 hard: {
55 reactionTime: 100,
56 accuracy: 0.95,
57 speed: 1.5,
58 prediction: 0.85,
59 aggression: 1.0,
60 oscillation: 1.0,
61 bopChance: 0.85,
62 windupSpeed: 0.2,
63 windupRadius: 60,
64 comboBopChance: 0.5,
65 circularMotion: 0.8,
66 phaseSpeed: 0.12,
67 idleMovement: 0.8,
68 trackingAggression: 0.25
69 }
70 };
71
72 // ============= AI STATE =============
73 let aiState = {
74 targetY: 200,
75 reactionDelay: 0,
76 difficulty: 'medium',
77 lastBallX: 0,
78 lastUpdateTime: 0,
79
80 // Advanced AI state machine
81 mode: 'TRACKING',
82 windupStartTime: 0,
83 swingStartTime: 0,
84 interceptY: 200,
85 windupDirection: 1,
86 aggressionLevel: 0.5,
87 lastHitTime: 0,
88
89 // Enhanced windup system
90 windupPhase: 0, // 0 to 2π for circular motion
91 windupVelocity: 0, // Current oscillation speed
92 windupMomentum: {x: 0, y: 0}, // Momentum vector
93 windupCenter: 200, // Center point of circular motion
94 peakReached: false, // Track if we hit peak velocity
95 comboBop: false, // Planning windup+bop combo
96 maxVelocityPhase: 0, // Phase where max velocity occurs
97
98 // Motion tracking
99 lastPositions: [], // Track last N positions for smoothing
100 currentVelocity: 0, // Actual paddle velocity
101 targetVelocity: 0, // Desired paddle velocity
102 smoothedTarget: 200, // Smoothed target position
103
104 // Original oscillation parameters (keeping for compatibility)
105 windupDistance: 120,
106 swingPower: 1.05,
107 timingWindow: 40,
108 windupProgress: 0,
109
110 // Lifelike movement
111 idleTarget: 200,
112 microAdjustment: 0,
113 breathingOffset: 0,
114 lastMicroTime: 0,
115
116 // AI Bop system
117 consideringBop: false,
118 bopDecisionTime: 0,
119 bopTiming: 200
120 };
121
122 // ============= MAIN AI HANDLER =============
123 function handleAI(currentTime, ball, rightPaddle, rightSupport,
124 leftScore, rightScore, width, height,
125 bopState, activateBop, engine, particles) {
126 let ballPos = ball.position;
127 let ballVel = ball.velocity;
128 let aiSettings = AI_SETTINGS[aiState.difficulty];
129
130 updateAIAggression(leftScore, rightScore);
131 updateAILifelikeBehavior(currentTime, height);
132
133 switch (aiState.mode) {
134 case 'TRACKING':
135 handleAITracking(currentTime, ballPos, ballVel, aiSettings,
136 rightPaddle, rightSupport, width, height,
137 bopState, activateBop, engine, particles);
138 break;
139 case 'WINDING_UP':
140 handleAIWindup(currentTime, ballPos, ballVel, aiSettings,
141 rightPaddle, rightSupport, height, width);
142 break;
143 case 'SWINGING':
144 handleAISwing(currentTime, ball, rightPaddle, rightSupport);
145 break;
146 case 'RECOVERING':
147 handleAIRecovery(currentTime);
148 break;
149 case 'ANTICIPATING':
150 handleAIAnticipation(currentTime, ballPos, ballVel,
151 rightPaddle, rightSupport, height);
152 break;
153 }
154
155 executeAIMovement(aiSettings, rightSupport);
156 }
157
158 // ============= AI BEHAVIORS =============
159 function updateAILifelikeBehavior(currentTime, height) {
160 let aiSettings = AI_SETTINGS[aiState.difficulty];
161
162 // More pronounced breathing motion
163 aiState.breathingOffset = Math.sin(currentTime * 0.005) * 5 * aiSettings.idleMovement;
164
165 // More frequent micro-adjustments
166 if (currentTime - aiState.lastMicroTime > 1000 + Math.random() * 500) {
167 aiState.microAdjustment = (Math.random() - 0.5) * 30 * aiSettings.idleMovement;
168 aiState.lastMicroTime = currentTime;
169 }
170
171 // Slower decay for more persistent movement
172 aiState.microAdjustment *= 0.95;
173
174 // Add "nervous energy" - small random movements
175 let nervousEnergy = (Math.random() - 0.5) * AI_NERVOUS_ENERGY * aiSettings.aggression;
176 aiState.microAdjustment += nervousEnergy;
177
178 if (aiState.mode === 'ANTICIPATING' || aiState.mode === 'RECOVERING') {
179 let centerY = height / 2;
180 let wanderRadius = 40 * aiSettings.idleMovement; // Larger wander radius
181 aiState.idleTarget = centerY + Math.sin(currentTime * 0.003) * wanderRadius;
182
183 // Add vertical patrol behavior
184 let patrolOffset = Math.sin(currentTime * 0.002) * 30 * aiSettings.idleMovement;
185 aiState.idleTarget += patrolOffset;
186 }
187 }
188
189 function updateAIAggression(leftScore, rightScore) {
190 let scoreDiff = leftScore - rightScore;
191 let baseAggression = AI_SETTINGS[aiState.difficulty].aggression;
192
193 if (scoreDiff >= 2) {
194 aiState.aggressionLevel = Math.min(1.0, baseAggression + 0.3);
195 } else if (scoreDiff >= 1) {
196 aiState.aggressionLevel = Math.min(1.0, baseAggression + 0.15);
197 } else {
198 aiState.aggressionLevel = baseAggression;
199 }
200 }
201
202 function handleAITracking(currentTime, ballPos, ballVel, aiSettings,
203 rightPaddle, rightSupport, width, height,
204 bopState, activateBop, engine, particles) {
205 let ballApproaching = ballVel.x > 0;
206 let ballDistance = width - ballPos.x;
207 let ballSpeed = Math.sqrt(ballVel.x * ballVel.x + ballVel.y * ballVel.y);
208
209 let paddlePos = rightPaddle.position;
210 let anchorPos = rightSupport.position;
211
212 // More aggressive tracking based on difficulty
213 let trackingIntensity = ballApproaching ?
214 (0.08 + aiSettings.trackingAggression) :
215 (0.03 + aiSettings.trackingAggression * 0.5);
216
217 // Predict where ball will be and move preemptively
218 let futureTime = 0.5; // Look ahead 0.5 seconds
219 let futureBallY = ballPos.y + ballVel.y * futureTime * 60; // 60 fps assumption
220
221 // Blend current and future position based on difficulty
222 let targetBallY = lerp(ballPos.y, futureBallY, aiSettings.prediction);
223
224 let desiredPaddleY = targetBallY + aiState.microAdjustment + aiState.breathingOffset;
225 desiredPaddleY = Math.max(80, Math.min(height - 80, desiredPaddleY));
226
227 // Add aggressive positioning - AI tries to "cut off" the ball
228 if (ballApproaching && aiSettings.aggression > 0.5) {
229 let aggressiveOffset = (ballVel.y > 0 ? 1 : -1) * 20 * aiSettings.aggression;
230 desiredPaddleY += aggressiveOffset;
231 }
232
233 let anchorOffsetNeeded = calculateAnchorOffset(desiredPaddleY, paddlePos, anchorPos);
234 let targetAnchorY = desiredPaddleY + anchorOffsetNeeded;
235
236 // Faster interpolation for more responsive movement
237 aiState.targetY = lerp(aiState.targetY, targetAnchorY, trackingIntensity * 1.5);
238
239
240 // AI Bop decision logic
241 if (ballApproaching && !aiState.consideringBop && !bopState.right.active) {
242 let timeToReach = ballDistance / Math.abs(ballVel.x);
243 let predictedBallY = ballPos.y + ballVel.y * timeToReach;
244
245 if (predictedBallY < 50) {
246 predictedBallY = 100 - predictedBallY;
247 } else if (predictedBallY > height - 50) {
248 predictedBallY = 2 * (height - 50) - predictedBallY;
249 }
250
251 let paddleY = rightPaddle.position.y;
252 let distanceToIntercept = Math.abs(predictedBallY - paddleY);
253
254 let bopEffectiveRange = PADDLE_HEIGHT / 2 + 30;
255
256 let shouldConsiderBop = ballSpeed > 5 &&
257 distanceToIntercept < bopEffectiveRange &&
258 ballDistance > 80 && ballDistance < 200 &&
259 currentTime - bopState.right.lastBopTime > BOP_COOLDOWN &&
260 Math.random() < aiSettings.bopChance;
261
262 if (shouldConsiderBop) {
263 aiState.consideringBop = true;
264 aiState.bopDecisionTime = currentTime;
265 aiState.bopTiming = Math.max(50, Math.min(200, ballDistance * 2 - ballSpeed * 10));
266 }
267 }
268
269 // Execute bop at the right moment
270 if (aiState.consideringBop && ballApproaching) {
271 let timeToBop = currentTime - aiState.bopDecisionTime;
272 let paddleY = rightPaddle.position.y;
273 let distanceToBall = Math.abs(ballPos.y - paddleY);
274
275 // Refined bop execution conditions
276 let shouldBop = timeToBop > aiState.bopTiming &&
277 ballDistance < 150 && // Close enough
278 distanceToBall < PADDLE_HEIGHT / 2 + 20 && // Paddle can reach ball
279 !bopState.right.active;
280
281 // Special handling for combo bops during windup
282 if (aiState.comboBop && aiState.mode === 'WINDING_UP') {
283 // Execute bop at peak velocity during windup
284 shouldBop = shouldBop || (aiState.peakReached &&
285 ballDistance < 180 &&
286 distanceToBall < PADDLE_HEIGHT / 2 + 30);
287 }
288
289 if (shouldBop) {
290 activateBop('right', currentTime, rightPaddle, rightSupport, engine, particles);
291 aiState.consideringBop = false;
292
293 if (aiState.comboBop) {
294 console.log(`AI COMBO BOP! Phase: ${(aiState.windupPhase % (Math.PI * 2)).toFixed(2)}, Velocity: ${aiState.currentVelocity.toFixed(1)}`);
295 aiState.comboBop = false;
296
297 // Transition to swing after combo
298 if (aiState.mode === 'WINDING_UP') {
299 aiState.mode = 'SWINGING';
300 aiState.swingStartTime = currentTime;
301 }
302 } else {
303 console.log(`AI BOP! Difficulty: ${aiState.difficulty}, Speed: ${ballSpeed.toFixed(1)}`);
304 }
305 }
306
307 // Cancel bop if opportunity missed
308 if (ballDistance > 200 || ballDistance < 50) {
309 aiState.consideringBop = false;
310 aiState.comboBop = false;
311 }
312 }
313
314 // Advanced prediction and windup logic
315 if (currentTime - aiState.lastUpdateTime > aiSettings.reactionTime) {
316 if (ballApproaching && ballDistance < 300) {
317 let timeToReach = ballDistance / Math.abs(ballVel.x);
318 let predictedBallY = ballPos.y + ballVel.y * timeToReach;
319
320 if (predictedBallY < 50) {
321 predictedBallY = 100 - predictedBallY;
322 } else if (predictedBallY > height - 50) {
323 predictedBallY = 2 * (height - 50) - predictedBallY;
324 }
325
326 let error = (Math.random() - 0.5) * 35 * (1 - aiSettings.accuracy);
327 predictedBallY += error;
328
329 aiState.interceptY = predictedBallY;
330
331 let interceptAnchorOffset = calculateAnchorOffset(aiState.interceptY, paddlePos, anchorPos);
332 let targetAnchorForIntercept = aiState.interceptY + interceptAnchorOffset;
333
334 let shouldWindUp = ballSpeed < 4.5 &&
335 ballDistance > 200 &&
336 Math.abs(ballVel.y) < 3 &&
337 Math.abs(ballVel.x) > 1 &&
338 !aiState.consideringBop &&
339 Math.random() < aiSettings.oscillation * aiState.aggressionLevel * 0.3;
340
341 if (shouldWindUp) {
342 // Start winding up for power shot
343 aiState.mode = 'WINDING_UP';
344 aiState.windupStartTime = currentTime;
345
346 // Initialize circular windup
347 aiState.windupCenter = paddlePos.y; // Start from current position
348 aiState.windupPhase = 0;
349 aiState.windupMomentum = {x: 0, y: 0};
350 aiState.smoothedTarget = paddlePos.y;
351 aiState.lastPositions = [paddlePos.y];
352 aiState.peakReached = false;
353 aiState.comboBop = false;
354
355 // Determine initial direction based on intercept position
356 aiState.windupDirection = aiState.interceptY > paddlePos.y ? -1 : 1;
357 } else {
358 aiState.targetY = lerp(aiState.targetY, targetAnchorForIntercept, 0.3);
359 }
360
361 aiState.lastUpdateTime = currentTime;
362 }
363 }
364 }
365
366 function handleAIWindup(currentTime, ballPos, ballVel, aiSettings,
367 rightPaddle, rightSupport, height, width) {
368 let windupTime = currentTime - aiState.windupStartTime;
369 let maxWindupTime = AI_WINDUP_MIN_TIME + (AI_WINDUP_MAX_TIME - AI_WINDUP_MIN_TIME) * aiState.aggressionLevel;
370
371 // Update windup phase for circular motion
372 let phaseSpeed = aiSettings.phaseSpeed * (1 + aiState.aggressionLevel * 0.5);
373 aiState.windupPhase += phaseSpeed;
374
375 // Calculate circular motion with momentum
376 let radius = aiSettings.windupRadius * aiState.aggressionLevel;
377 let circularBlend = aiSettings.circularMotion;
378
379 // Pure circular motion components
380 let circularX = Math.sin(aiState.windupPhase) * radius * 0.3; // Slight X movement
381 let circularY = Math.cos(aiState.windupPhase) * radius;
382
383 // Add momentum for more natural motion
384 let targetDeltaY = circularY - (aiState.smoothedTarget - aiState.windupCenter);
385 aiState.windupMomentum.y = aiState.windupMomentum.y * AI_MOMENTUM_CARRY + targetDeltaY * (1 - AI_MOMENTUM_CARRY);
386
387 // Calculate the target position with smooth circular motion
388 let windupTargetY = aiState.windupCenter + aiState.windupMomentum.y;
389
390 // Smooth the target for more fluid motion
391 aiState.smoothedTarget = aiState.smoothedTarget * AI_WINDUP_SMOOTHNESS +
392 windupTargetY * (1 - AI_WINDUP_SMOOTHNESS);
393
394 // Keep within bounds
395 aiState.smoothedTarget = Math.max(50, Math.min(height - 50, aiState.smoothedTarget));
396
397 // Convert paddle target to anchor target
398 let anchorOffsetNeeded = calculateAnchorOffset(aiState.smoothedTarget, rightPaddle.position, rightSupport.position);
399 aiState.targetY = aiState.smoothedTarget + anchorOffsetNeeded;
400
401 // Track velocity for combo detection
402 if (aiState.lastPositions.length > 5) {
403 aiState.lastPositions.shift();
404 }
405 aiState.lastPositions.push(aiState.smoothedTarget);
406
407 // Calculate current velocity
408 if (aiState.lastPositions.length > 1) {
409 let recentDelta = aiState.lastPositions[aiState.lastPositions.length - 1] -
410 aiState.lastPositions[aiState.lastPositions.length - 2];
411 aiState.currentVelocity = Math.abs(recentDelta);
412
413 // Check if we're at peak velocity (good time for combo bop)
414 if (aiState.currentVelocity > radius * phaseSpeed * 0.8 && !aiState.peakReached) {
415 aiState.peakReached = true;
416 aiState.maxVelocityPhase = aiState.windupPhase;
417
418 // Consider combo bop at peak
419 if (!aiState.comboBop && !aiState.consideringBop && !bopState.right.active &&
420 Math.random() < aiSettings.comboBopChance * aiState.aggressionLevel) {
421 aiState.comboBop = true;
422 aiState.consideringBop = true;
423 aiState.bopDecisionTime = currentTime;
424 aiState.bopTiming = 50; // Quick bop at peak
425 console.log("AI planning COMBO: Windup + Bop!");
426 }
427 }
428 }
429
430 // Check if it's time to swing
431 let ballDistance = width - ballPos.x;
432 let ballSpeed = Math.sqrt(ballVel.x * ballVel.x + ballVel.y * ballVel.y);
433
434 let shouldSwing = windupTime > maxWindupTime ||
435 ballDistance < 120 ||
436 ballSpeed > 6 ||
437 (aiState.windupPhase > Math.PI * 2 && ballDistance < 200);
438
439 if (shouldSwing) {
440 aiState.mode = 'SWINGING';
441 aiState.swingStartTime = currentTime;
442 aiState.windupPhase = 0;
443 aiState.peakReached = false;
444 aiState.comboBop = false;
445 aiState.lastPositions = [];
446
447 // Carry momentum into swing
448 aiState.targetVelocity = aiState.currentVelocity * 2;
449 }
450 }
451
452 function handleAISwing(currentTime, ball, rightPaddle, rightSupport) {
453 let paddlePos = rightPaddle.position;
454
455 let anchorOffsetNeeded = calculateAnchorOffset(aiState.interceptY, paddlePos, rightSupport.position);
456 aiState.targetY = aiState.interceptY + anchorOffsetNeeded;
457
458 let swingTime = currentTime - aiState.swingStartTime;
459 let maxSwingTime = aiState.timingWindow;
460
461 if (swingTime > maxSwingTime || Math.abs(ball.velocity.x) < 2) {
462 aiState.mode = 'RECOVERING';
463 aiState.lastHitTime = currentTime;
464 }
465 }
466
467 function handleAIRecovery(currentTime) {
468 aiState.targetY = aiState.idleTarget + aiState.breathingOffset;
469
470 let recoveryTime = currentTime - aiState.lastHitTime;
471 if (recoveryTime > 400) {
472 aiState.mode = 'ANTICIPATING';
473 }
474 }
475
476 function handleAIAnticipation(currentTime, ballPos, ballVel,
477 rightPaddle, rightSupport, height) {
478 let baseTarget = aiState.idleTarget + aiState.breathingOffset + aiState.microAdjustment;
479 let ballTrackingTarget = ballPos.y;
480
481 let desiredPaddleY = lerp(baseTarget, ballTrackingTarget, 0.15);
482
483 let paddlePos = rightPaddle.position;
484 let anchorPos = rightSupport.position;
485 let anchorOffsetNeeded = calculateAnchorOffset(desiredPaddleY, paddlePos, anchorPos);
486 aiState.targetY = desiredPaddleY + anchorOffsetNeeded;
487
488 if (ballVel.x > 0) {
489 aiState.mode = 'TRACKING';
490 }
491 }
492
493 // ============= HELPER FUNCTIONS =============
494 function calculateAnchorOffset(targetPaddleY, currentPaddlePos, currentAnchorPos) {
495 let springVectorY = currentPaddlePos.y - currentAnchorPos.y;
496 let estimatedPaddleOffset = springVectorY * 0.8;
497 return -estimatedPaddleOffset;
498 }
499
500 function executeAIMovement(aiSettings, rightSupport) {
501 let currentY = rightSupport.position.y;
502 let deltaY = aiState.targetY - currentY;
503
504 // Lower threshold for more constant movement
505 if (Math.abs(deltaY) > 0.5) { // Reduced from 1
506 let baseSpeed = 0.15 * aiSettings.speed; // Increased from 0.12
507
508 // Apply swing power during swing phase
509 if (aiState.mode === 'SWINGING') {
510 baseSpeed *= aiState.swingPower * (1 + aiState.aggressionLevel * 0.5); // Increased multiplier
511
512 // Add momentum from windup if available
513 if (aiState.targetVelocity > 0) {
514 baseSpeed *= (1 + aiState.targetVelocity * 0.15); // Increased from 0.1
515 aiState.targetVelocity *= 0.85; // Slower decay
516 }
517 } else if (aiState.mode === 'WINDING_UP') {
518 // Enhanced windup speed based on phase and settings
519 let windupSpeedMultiplier = aiSettings.windupSpeed / AI_WINDUP_SPEED;
520 baseSpeed *= (2.0 + windupSpeedMultiplier); // Increased from 1.5
521
522 // Add extra speed at peak velocity points
523 if (aiState.peakReached) {
524 baseSpeed *= 1.5; // Increased from 1.3
525 }
526 } else if (aiState.mode === 'TRACKING' || aiState.mode === 'ANTICIPATING') {
527 // Add movement urgency based on ball position
528 let ball = window.ball; // Access global ball
529 if (ball && ball.velocity.x > 0) {
530 let urgency = 1 + (1 - (ball.position.x / window.width)) * aiSettings.aggression;
531 baseSpeed *= urgency;
532 }
533 }
534
535 // Apply aggression multiplier with higher impact
536 baseSpeed *= (1 + aiState.aggressionLevel * 0.5); // Increased from 0.3
537
538 let movement = deltaY * baseSpeed;
539
540 // Allow faster movement for hard AI
541 let maxSpeed = SUPPORT_SPEED * (1.2 + aiSettings.aggression * 0.3);
542 movement = Math.max(-maxSpeed, Math.min(maxSpeed, movement));
543
544 // Import moveSupportEnhanced from game-systems
545 const moveSupportEnhanced = window.moveSupportEnhanced;
546 moveSupportEnhanced(rightSupport, movement, window.height);
547
548 // Update input buffer for visual effects
549 window.inputBuffer.right = movement / maxSpeed;
550 } else {
551 // Even when close to target, add small movements for liveliness
552 if (aiSettings.idleMovement > 0.5) {
553 let tinyMovement = (Math.random() - 0.5) * aiSettings.idleMovement;
554 const moveSupportEnhanced = window.moveSupportEnhanced;
555 moveSupportEnhanced(rightSupport, tinyMovement, window.height);
556 window.inputBuffer.right = tinyMovement / SUPPORT_SPEED;
557 } else {
558 // Gradually reduce input buffer when AI is not moving
559 window.inputBuffer.right *= 0.9; // Slower decay for more visible movement
560 }
561 }
562 }
563
564 // Utility function - should match p5.js lerp
565 function lerp(start, stop, amt) {
566 return amt * (stop - start) + start;
567 }