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