JavaScript · 28937 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 // AI Rotation constants
20 const AI_ROTATION_UPDATE_RATE = 150; // How often AI reconsiders rotation (ms)
21 const AI_ROTATION_SMOOTHING = 0.08; // How smoothly AI rotates
22 const AI_DEFENSIVE_ANGLE = 0.2; // Slight angle for defensive shots
23 const AI_OFFENSIVE_ANGLE = 0.4; // Larger angle for offensive shots
24 const AI_TRICK_SHOT_ANGLE = 0.6; // Maximum angle for trick shots
25 const AI_ROTATION_PREDICTION = 1.2; // How far ahead AI predicts for rotation
26
27 // ============= AI SETTINGS =============
28 const AI_SETTINGS = {
29 easy: {
30 reactionTime: 350,
31 accuracy: 0.7,
32 speed: 0.8,
33 prediction: 0.3,
34 aggression: 0.4,
35 oscillation: 0.3,
36 bopChance: 0.25,
37 windupSpeed: 0.1,
38 windupRadius: 30,
39 comboBopChance: 0.1,
40 circularMotion: 0.4,
41 phaseSpeed: 0.06,
42 idleMovement: 0.3,
43 trackingAggression: 0.1,
44 // Rotation settings
45 rotationUse: 0.2, // How often AI uses rotation
46 rotationAccuracy: 0.5, // How accurately AI calculates angle
47 rotationSpeed: 0.6, // How fast AI rotates
48 rotationAnticipation: 0.3 // How well AI predicts needed angle
49 },
50 medium: {
51 reactionTime: 200,
52 accuracy: 0.85,
53 speed: 1.0,
54 prediction: 0.6,
55 aggression: 0.7,
56 oscillation: 0.7,
57 bopChance: 0.55,
58 windupSpeed: 0.15,
59 windupRadius: 40,
60 comboBopChance: 0.3,
61 circularMotion: 0.6,
62 phaseSpeed: 0.08,
63 idleMovement: 0.5,
64 trackingAggression: 0.15,
65 // Rotation settings
66 rotationUse: 0.5,
67 rotationAccuracy: 0.7,
68 rotationSpeed: 0.8,
69 rotationAnticipation: 0.6
70 },
71 hard: {
72 reactionTime: 100,
73 accuracy: 0.95,
74 speed: 1.5,
75 prediction: 0.85,
76 aggression: 1.0,
77 oscillation: 1.0,
78 bopChance: 0.85,
79 windupSpeed: 0.2,
80 windupRadius: 60,
81 comboBopChance: 0.5,
82 circularMotion: 0.8,
83 phaseSpeed: 0.12,
84 idleMovement: 0.8,
85 trackingAggression: 0.25,
86 // Rotation settings
87 rotationUse: 0.8,
88 rotationAccuracy: 0.9,
89 rotationSpeed: 1.0,
90 rotationAnticipation: 0.85
91 }
92 };
93
94 // ============= AI STATE =============
95 let aiState = {
96 targetY: 200,
97 reactionDelay: 0,
98 difficulty: 'medium',
99 lastBallX: 0,
100 lastUpdateTime: 0,
101
102 // Advanced AI state machine
103 mode: 'TRACKING',
104 windupStartTime: 0,
105 swingStartTime: 0,
106 interceptY: 200,
107 windupDirection: 1,
108 aggressionLevel: 0.5,
109 lastHitTime: 0,
110
111 // Enhanced windup system
112 windupPhase: 0, // 0 to 2π for circular motion
113 windupVelocity: 0, // Current oscillation speed
114 windupMomentum: {x: 0, y: 0}, // Momentum vector
115 windupCenter: 200, // Center point of circular motion
116 peakReached: false, // Track if we hit peak velocity
117 comboBop: false, // Planning windup+bop combo
118 maxVelocityPhase: 0, // Phase where max velocity occurs
119
120 // Motion tracking
121 lastPositions: [], // Track last N positions for smoothing
122 currentVelocity: 0, // Actual paddle velocity
123 targetVelocity: 0, // Desired paddle velocity
124 smoothedTarget: 200, // Smoothed target position
125
126 // Original oscillation parameters (keeping for compatibility)
127 windupDistance: 120,
128 swingPower: 1.05,
129 timingWindow: 40,
130 windupProgress: 0,
131
132 // Lifelike movement
133 idleTarget: 200,
134 microAdjustment: 0,
135 breathingOffset: 0,
136 lastMicroTime: 0,
137
138 // AI Bop system
139 consideringBop: false,
140 bopDecisionTime: 0,
141 bopTiming: 200,
142
143 // AI Rotation system
144 targetRotation: 0, // Desired rotation angle
145 rotationMode: 'NEUTRAL', // NEUTRAL, OFFENSIVE, DEFENSIVE, TRICK_SHOT
146 lastRotationUpdate: 0, // Time of last rotation decision
147 plannedShotAngle: 0, // Angle AI wants to send ball
148 rotationConfidence: 0 // How confident AI is in its rotation choice
149 };
150
151 // ============= MAIN AI HANDLER =============
152 function handleAI(currentTime, ball, rightPaddle, rightSupport,
153 leftScore, rightScore, width, height,
154 bopState, activateBop, engine, particles) {
155 let ballPos = ball.position;
156 let ballVel = ball.velocity;
157 let aiSettings = AI_SETTINGS[aiState.difficulty];
158
159 updateAIAggression(leftScore, rightScore);
160 updateAILifelikeBehavior(currentTime, height);
161 updateAIRotation(currentTime, ball, rightPaddle, width, height, aiSettings);
162
163 switch (aiState.mode) {
164 case 'TRACKING':
165 handleAITracking(currentTime, ballPos, ballVel, aiSettings,
166 rightPaddle, rightSupport, width, height,
167 bopState, activateBop, engine, particles);
168 break;
169 case 'WINDING_UP':
170 handleAIWindup(currentTime, ballPos, ballVel, aiSettings,
171 rightPaddle, rightSupport, height, width);
172 break;
173 case 'SWINGING':
174 handleAISwing(currentTime, ball, rightPaddle, rightSupport);
175 break;
176 case 'RECOVERING':
177 handleAIRecovery(currentTime);
178 break;
179 case 'ANTICIPATING':
180 handleAIAnticipation(currentTime, ballPos, ballVel,
181 rightPaddle, rightSupport, height);
182 break;
183 }
184
185 executeAIMovement(aiSettings, rightSupport);
186 }
187
188 // ============= AI BEHAVIORS =============
189 function updateAILifelikeBehavior(currentTime, height) {
190 let aiSettings = AI_SETTINGS[aiState.difficulty];
191
192 // More pronounced breathing motion
193 aiState.breathingOffset = Math.sin(currentTime * 0.005) * 5 * aiSettings.idleMovement;
194
195 // More frequent micro-adjustments
196 if (currentTime - aiState.lastMicroTime > 1000 + Math.random() * 500) {
197 aiState.microAdjustment = (Math.random() - 0.5) * 30 * aiSettings.idleMovement;
198 aiState.lastMicroTime = currentTime;
199 }
200
201 // Slower decay for more persistent movement
202 aiState.microAdjustment *= 0.95;
203
204 // Add "nervous energy" - small random movements
205 let nervousEnergy = (Math.random() - 0.5) * AI_NERVOUS_ENERGY * aiSettings.aggression;
206 aiState.microAdjustment += nervousEnergy;
207
208 if (aiState.mode === 'ANTICIPATING' || aiState.mode === 'RECOVERING') {
209 let centerY = height / 2;
210 let wanderRadius = 40 * aiSettings.idleMovement; // Larger wander radius
211 aiState.idleTarget = centerY + Math.sin(currentTime * 0.003) * wanderRadius;
212
213 // Add vertical patrol behavior
214 let patrolOffset = Math.sin(currentTime * 0.002) * 30 * aiSettings.idleMovement;
215 aiState.idleTarget += patrolOffset;
216 }
217 }
218
219 function updateAIAggression(leftScore, rightScore) {
220 let scoreDiff = leftScore - rightScore;
221 let baseAggression = AI_SETTINGS[aiState.difficulty].aggression;
222
223 if (scoreDiff >= 2) {
224 aiState.aggressionLevel = Math.min(1.0, baseAggression + 0.3);
225 } else if (scoreDiff >= 1) {
226 aiState.aggressionLevel = Math.min(1.0, baseAggression + 0.15);
227 } else {
228 aiState.aggressionLevel = baseAggression;
229 }
230 }
231
232 function handleAITracking(currentTime, ballPos, ballVel, aiSettings,
233 rightPaddle, rightSupport, width, height,
234 bopState, activateBop, engine, particles) {
235 let ballApproaching = ballVel.x > 0;
236 let ballDistance = width - ballPos.x;
237 let ballSpeed = Math.sqrt(ballVel.x * ballVel.x + ballVel.y * ballVel.y);
238
239 let paddlePos = rightPaddle.position;
240 let anchorPos = rightSupport.position;
241
242 // More aggressive tracking based on difficulty
243 let trackingIntensity = ballApproaching ?
244 (0.08 + aiSettings.trackingAggression) :
245 (0.03 + aiSettings.trackingAggression * 0.5);
246
247 // Predict where ball will be and move preemptively
248 let futureTime = 0.5; // Look ahead 0.5 seconds
249 let futureBallY = ballPos.y + ballVel.y * futureTime * 60; // 60 fps assumption
250
251 // Blend current and future position based on difficulty
252 let targetBallY = lerp(ballPos.y, futureBallY, aiSettings.prediction);
253
254 let desiredPaddleY = targetBallY + aiState.microAdjustment + aiState.breathingOffset;
255 desiredPaddleY = Math.max(80, Math.min(height - 80, desiredPaddleY));
256
257 // Add aggressive positioning - AI tries to "cut off" the ball
258 if (ballApproaching && aiSettings.aggression > 0.5) {
259 let aggressiveOffset = (ballVel.y > 0 ? 1 : -1) * 20 * aiSettings.aggression;
260 desiredPaddleY += aggressiveOffset;
261 }
262
263 let anchorOffsetNeeded = calculateAnchorOffset(desiredPaddleY, paddlePos, anchorPos);
264 let targetAnchorY = desiredPaddleY + anchorOffsetNeeded;
265
266 // Faster interpolation for more responsive movement
267 aiState.targetY = lerp(aiState.targetY, targetAnchorY, trackingIntensity * 1.5);
268
269
270 // AI Bop decision logic
271 if (ballApproaching && !aiState.consideringBop && !bopState.right.active) {
272 let timeToReach = ballDistance / Math.abs(ballVel.x);
273 let predictedBallY = ballPos.y + ballVel.y * timeToReach;
274
275 if (predictedBallY < 50) {
276 predictedBallY = 100 - predictedBallY;
277 } else if (predictedBallY > height - 50) {
278 predictedBallY = 2 * (height - 50) - predictedBallY;
279 }
280
281 let paddleY = rightPaddle.position.y;
282 let distanceToIntercept = Math.abs(predictedBallY - paddleY);
283
284 let bopEffectiveRange = PADDLE_HEIGHT / 2 + 30;
285
286 let shouldConsiderBop = ballSpeed > 5 &&
287 distanceToIntercept < bopEffectiveRange &&
288 ballDistance > 80 && ballDistance < 200 &&
289 currentTime - bopState.right.lastBopTime > BOP_COOLDOWN &&
290 Math.random() < aiSettings.bopChance;
291
292 if (shouldConsiderBop) {
293 aiState.consideringBop = true;
294 aiState.bopDecisionTime = currentTime;
295 aiState.bopTiming = Math.max(50, Math.min(200, ballDistance * 2 - ballSpeed * 10));
296 }
297 }
298
299 // Execute bop at the right moment
300 if (aiState.consideringBop && ballApproaching) {
301 let timeToBop = currentTime - aiState.bopDecisionTime;
302 let paddleY = rightPaddle.position.y;
303 let distanceToBall = Math.abs(ballPos.y - paddleY);
304
305 // Refined bop execution conditions
306 let shouldBop = timeToBop > aiState.bopTiming &&
307 ballDistance < 150 && // Close enough
308 distanceToBall < PADDLE_HEIGHT / 2 + 20 && // Paddle can reach ball
309 !bopState.right.active;
310
311 // Special handling for combo bops during windup
312 if (aiState.comboBop && aiState.mode === 'WINDING_UP') {
313 // Execute bop at peak velocity during windup
314 shouldBop = shouldBop || (aiState.peakReached &&
315 ballDistance < 180 &&
316 distanceToBall < PADDLE_HEIGHT / 2 + 30);
317 }
318
319 if (shouldBop) {
320 activateBop('right', currentTime, rightPaddle, rightSupport, engine, particles);
321 aiState.consideringBop = false;
322
323 if (aiState.comboBop) {
324 console.log(`AI COMBO BOP! Phase: ${(aiState.windupPhase % (Math.PI * 2)).toFixed(2)}, Velocity: ${aiState.currentVelocity.toFixed(1)}`);
325 aiState.comboBop = false;
326
327 // Transition to swing after combo
328 if (aiState.mode === 'WINDING_UP') {
329 aiState.mode = 'SWINGING';
330 aiState.swingStartTime = currentTime;
331 }
332 } else {
333 console.log(`AI BOP! Difficulty: ${aiState.difficulty}, Speed: ${ballSpeed.toFixed(1)}`);
334 }
335 }
336
337 // Cancel bop if opportunity missed
338 if (ballDistance > 200 || ballDistance < 50) {
339 aiState.consideringBop = false;
340 aiState.comboBop = false;
341 }
342 }
343
344 // Advanced prediction and windup logic
345 if (currentTime - aiState.lastUpdateTime > aiSettings.reactionTime) {
346 if (ballApproaching && ballDistance < 300) {
347 let timeToReach = ballDistance / Math.abs(ballVel.x);
348 let predictedBallY = ballPos.y + ballVel.y * timeToReach;
349
350 if (predictedBallY < 50) {
351 predictedBallY = 100 - predictedBallY;
352 } else if (predictedBallY > height - 50) {
353 predictedBallY = 2 * (height - 50) - predictedBallY;
354 }
355
356 let error = (Math.random() - 0.5) * 35 * (1 - aiSettings.accuracy);
357 predictedBallY += error;
358
359 aiState.interceptY = predictedBallY;
360
361 let interceptAnchorOffset = calculateAnchorOffset(aiState.interceptY, paddlePos, anchorPos);
362 let targetAnchorForIntercept = aiState.interceptY + interceptAnchorOffset;
363
364 let shouldWindUp = ballSpeed < 4.5 &&
365 ballDistance > 200 &&
366 Math.abs(ballVel.y) < 3 &&
367 Math.abs(ballVel.x) > 1 &&
368 !aiState.consideringBop &&
369 Math.random() < aiSettings.oscillation * aiState.aggressionLevel * 0.3;
370
371 if (shouldWindUp) {
372 // Start winding up for power shot
373 aiState.mode = 'WINDING_UP';
374 aiState.windupStartTime = currentTime;
375
376 // Initialize circular windup
377 aiState.windupCenter = paddlePos.y; // Start from current position
378 aiState.windupPhase = 0;
379 aiState.windupMomentum = {x: 0, y: 0};
380 aiState.smoothedTarget = paddlePos.y;
381 aiState.lastPositions = [paddlePos.y];
382 aiState.peakReached = false;
383 aiState.comboBop = false;
384
385 // Determine initial direction based on intercept position
386 aiState.windupDirection = aiState.interceptY > paddlePos.y ? -1 : 1;
387 } else {
388 aiState.targetY = lerp(aiState.targetY, targetAnchorForIntercept, 0.3);
389 }
390
391 aiState.lastUpdateTime = currentTime;
392 }
393 }
394 }
395
396 function handleAIWindup(currentTime, ballPos, ballVel, aiSettings,
397 rightPaddle, rightSupport, height, width) {
398 let windupTime = currentTime - aiState.windupStartTime;
399 let maxWindupTime = AI_WINDUP_MIN_TIME + (AI_WINDUP_MAX_TIME - AI_WINDUP_MIN_TIME) * aiState.aggressionLevel;
400
401 // Update windup phase for circular motion
402 let phaseSpeed = aiSettings.phaseSpeed * (1 + aiState.aggressionLevel * 0.5);
403 aiState.windupPhase += phaseSpeed;
404
405 // Calculate circular motion with momentum
406 let radius = aiSettings.windupRadius * aiState.aggressionLevel;
407 let circularBlend = aiSettings.circularMotion;
408
409 // Pure circular motion components
410 let circularX = Math.sin(aiState.windupPhase) * radius * 0.3; // Slight X movement
411 let circularY = Math.cos(aiState.windupPhase) * radius;
412
413 // Add momentum for more natural motion
414 let targetDeltaY = circularY - (aiState.smoothedTarget - aiState.windupCenter);
415 aiState.windupMomentum.y = aiState.windupMomentum.y * AI_MOMENTUM_CARRY + targetDeltaY * (1 - AI_MOMENTUM_CARRY);
416
417 // Calculate the target position with smooth circular motion
418 let windupTargetY = aiState.windupCenter + aiState.windupMomentum.y;
419
420 // Smooth the target for more fluid motion
421 aiState.smoothedTarget = aiState.smoothedTarget * AI_WINDUP_SMOOTHNESS +
422 windupTargetY * (1 - AI_WINDUP_SMOOTHNESS);
423
424 // Keep within bounds
425 aiState.smoothedTarget = Math.max(50, Math.min(height - 50, aiState.smoothedTarget));
426
427 // Convert paddle target to anchor target
428 let anchorOffsetNeeded = calculateAnchorOffset(aiState.smoothedTarget, rightPaddle.position, rightSupport.position);
429 aiState.targetY = aiState.smoothedTarget + anchorOffsetNeeded;
430
431 // Track velocity for combo detection
432 if (aiState.lastPositions.length > 5) {
433 aiState.lastPositions.shift();
434 }
435 aiState.lastPositions.push(aiState.smoothedTarget);
436
437 // Calculate current velocity
438 if (aiState.lastPositions.length > 1) {
439 let recentDelta = aiState.lastPositions[aiState.lastPositions.length - 1] -
440 aiState.lastPositions[aiState.lastPositions.length - 2];
441 aiState.currentVelocity = Math.abs(recentDelta);
442
443 // Check if we're at peak velocity (good time for combo bop)
444 if (aiState.currentVelocity > radius * phaseSpeed * 0.8 && !aiState.peakReached) {
445 aiState.peakReached = true;
446 aiState.maxVelocityPhase = aiState.windupPhase;
447
448 // Consider combo bop at peak
449 if (!aiState.comboBop && !aiState.consideringBop && !bopState.right.active &&
450 Math.random() < aiSettings.comboBopChance * aiState.aggressionLevel) {
451 aiState.comboBop = true;
452 aiState.consideringBop = true;
453 aiState.bopDecisionTime = currentTime;
454 aiState.bopTiming = 50; // Quick bop at peak
455 console.log("AI planning COMBO: Windup + Bop!");
456 }
457 }
458 }
459
460 // Check if it's time to swing
461 let ballDistance = width - ballPos.x;
462 let ballSpeed = Math.sqrt(ballVel.x * ballVel.x + ballVel.y * ballVel.y);
463
464 let shouldSwing = windupTime > maxWindupTime ||
465 ballDistance < 120 ||
466 ballSpeed > 6 ||
467 (aiState.windupPhase > Math.PI * 2 && ballDistance < 200);
468
469 if (shouldSwing) {
470 aiState.mode = 'SWINGING';
471 aiState.swingStartTime = currentTime;
472 aiState.windupPhase = 0;
473 aiState.peakReached = false;
474 aiState.comboBop = false;
475 aiState.lastPositions = [];
476
477 // Carry momentum into swing
478 aiState.targetVelocity = aiState.currentVelocity * 2;
479 }
480 }
481
482 function handleAISwing(currentTime, ball, rightPaddle, rightSupport) {
483 let paddlePos = rightPaddle.position;
484
485 let anchorOffsetNeeded = calculateAnchorOffset(aiState.interceptY, paddlePos, rightSupport.position);
486 aiState.targetY = aiState.interceptY + anchorOffsetNeeded;
487
488 let swingTime = currentTime - aiState.swingStartTime;
489 let maxSwingTime = aiState.timingWindow;
490
491 if (swingTime > maxSwingTime || Math.abs(ball.velocity.x) < 2) {
492 aiState.mode = 'RECOVERING';
493 aiState.lastHitTime = currentTime;
494 }
495 }
496
497 function handleAIRecovery(currentTime) {
498 aiState.targetY = aiState.idleTarget + aiState.breathingOffset;
499
500 let recoveryTime = currentTime - aiState.lastHitTime;
501 if (recoveryTime > 400) {
502 aiState.mode = 'ANTICIPATING';
503 }
504 }
505
506 function handleAIAnticipation(currentTime, ballPos, ballVel,
507 rightPaddle, rightSupport, height) {
508 let baseTarget = aiState.idleTarget + aiState.breathingOffset + aiState.microAdjustment;
509 let ballTrackingTarget = ballPos.y;
510
511 let desiredPaddleY = lerp(baseTarget, ballTrackingTarget, 0.15);
512
513 let paddlePos = rightPaddle.position;
514 let anchorPos = rightSupport.position;
515 let anchorOffsetNeeded = calculateAnchorOffset(desiredPaddleY, paddlePos, anchorPos);
516 aiState.targetY = desiredPaddleY + anchorOffsetNeeded;
517
518 if (ballVel.x > 0) {
519 aiState.mode = 'TRACKING';
520 }
521 }
522
523 // ============= HELPER FUNCTIONS =============
524 function calculateAnchorOffset(targetPaddleY, currentPaddlePos, currentAnchorPos) {
525 let springVectorY = currentPaddlePos.y - currentAnchorPos.y;
526 let estimatedPaddleOffset = springVectorY * 0.8;
527 return -estimatedPaddleOffset;
528 }
529
530 function executeAIMovement(aiSettings, rightSupport) {
531 let currentY = rightSupport.position.y;
532 let deltaY = aiState.targetY - currentY;
533
534 // Lower threshold for more constant movement
535 if (Math.abs(deltaY) > 0.5) { // Reduced from 1
536 let baseSpeed = 0.15 * aiSettings.speed; // Increased from 0.12
537
538 // Apply swing power during swing phase
539 if (aiState.mode === 'SWINGING') {
540 baseSpeed *= aiState.swingPower * (1 + aiState.aggressionLevel * 0.5); // Increased multiplier
541
542 // Add momentum from windup if available
543 if (aiState.targetVelocity > 0) {
544 baseSpeed *= (1 + aiState.targetVelocity * 0.15); // Increased from 0.1
545 aiState.targetVelocity *= 0.85; // Slower decay
546 }
547 } else if (aiState.mode === 'WINDING_UP') {
548 // Enhanced windup speed based on phase and settings
549 let windupSpeedMultiplier = aiSettings.windupSpeed / AI_WINDUP_SPEED;
550 baseSpeed *= (2.0 + windupSpeedMultiplier); // Increased from 1.5
551
552 // Add extra speed at peak velocity points
553 if (aiState.peakReached) {
554 baseSpeed *= 1.5; // Increased from 1.3
555 }
556 } else if (aiState.mode === 'TRACKING' || aiState.mode === 'ANTICIPATING') {
557 // Add movement urgency based on ball position
558 let ball = window.ball; // Access global ball
559 if (ball && ball.velocity.x > 0) {
560 let urgency = 1 + (1 - (ball.position.x / window.width)) * aiSettings.aggression;
561 baseSpeed *= urgency;
562 }
563 }
564
565 // Apply aggression multiplier with higher impact
566 baseSpeed *= (1 + aiState.aggressionLevel * 0.5); // Increased from 0.3
567
568 let movement = deltaY * baseSpeed;
569
570 // Allow faster movement for hard AI
571 let maxSpeed = SUPPORT_SPEED * (1.2 + aiSettings.aggression * 0.3);
572 movement = Math.max(-maxSpeed, Math.min(maxSpeed, movement));
573
574 // Import moveSupportEnhanced from game-systems
575 const moveSupportEnhanced = window.moveSupportEnhanced;
576 moveSupportEnhanced(rightSupport, movement, window.height);
577
578 // Update input buffer for visual effects
579 window.inputBuffer.right = movement / maxSpeed;
580 } else {
581 // Even when close to target, add small movements for liveliness
582 if (aiSettings.idleMovement > 0.5) {
583 let tinyMovement = (Math.random() - 0.5) * aiSettings.idleMovement;
584 const moveSupportEnhanced = window.moveSupportEnhanced;
585 moveSupportEnhanced(rightSupport, tinyMovement, window.height);
586 window.inputBuffer.right = tinyMovement / SUPPORT_SPEED;
587 } else {
588 // Gradually reduce input buffer when AI is not moving
589 window.inputBuffer.right *= 0.9; // Slower decay for more visible movement
590 }
591 }
592 }
593
594 // ============= AI ROTATION SYSTEM =============
595 function updateAIRotation(currentTime, ball, rightPaddle, width, height, aiSettings) {
596 // Only update rotation decision periodically
597 if (currentTime - aiState.lastRotationUpdate < AI_ROTATION_UPDATE_RATE) {
598 // Still apply rotation smoothly even if not updating decision
599 applyAIRotation(aiSettings);
600 return;
601 }
602
603 aiState.lastRotationUpdate = currentTime;
604
605 let ballPos = ball.position;
606 let ballVel = ball.velocity;
607 let paddlePos = rightPaddle.position;
608
609 // Check if AI should use rotation
610 if (Math.random() > aiSettings.rotationUse) {
611 aiState.rotationMode = 'NEUTRAL';
612 aiState.targetRotation = 0;
613 applyAIRotation(aiSettings);
614 return;
615 }
616
617 // Calculate ball approach
618 let ballApproaching = ballVel.x > 0;
619 let ballDistance = width - ballPos.x;
620 let timeToReach = ballDistance / Math.abs(ballVel.x);
621
622 // Predict where ball will be
623 let predictedBallY = ballPos.y + ballVel.y * timeToReach * AI_ROTATION_PREDICTION * aiSettings.rotationAnticipation;
624
625 // Bounce prediction
626 if (predictedBallY < 50) {
627 predictedBallY = 100 - predictedBallY;
628 } else if (predictedBallY > height - 50) {
629 predictedBallY = 2 * (height - 50) - predictedBallY;
630 }
631
632 // Decide rotation strategy
633 if (!ballApproaching || ballDistance > 300) {
634 // Return to neutral when ball is far
635 aiState.rotationMode = 'NEUTRAL';
636 aiState.targetRotation = 0;
637 } else if (aiState.mode === 'WINDING_UP' || aiState.mode === 'SWINGING') {
638 // Offensive rotation during power shots
639 aiState.rotationMode = 'OFFENSIVE';
640
641 // Calculate desired shot angle
642 let targetY = height / 2; // Aim for center by default
643
644 // Try to aim away from player
645 if (paddlePos.y < height / 2) {
646 targetY = height - 80; // Aim down
647 } else {
648 targetY = 80; // Aim up
649 }
650
651 // Calculate required angle
652 let deltaY = targetY - paddlePos.y;
653 let desiredAngle = Math.atan2(deltaY, width - paddlePos.x) * AI_OFFENSIVE_ANGLE;
654
655 // Add some inaccuracy based on difficulty
656 let error = (Math.random() - 0.5) * (1 - aiSettings.rotationAccuracy) * 0.5;
657 desiredAngle += error;
658
659 // Clamp angle
660 aiState.targetRotation = Math.max(-ROTATION_MAX_ANGLE, Math.min(ROTATION_MAX_ANGLE, desiredAngle));
661
662 } else if (aiState.consideringBop || bopState.right.active) {
663 // Trick shot rotation during bop
664 aiState.rotationMode = 'TRICK_SHOT';
665
666 // More extreme angles for bop shots
667 let trickDirection = (paddlePos.y < height / 2) ? 1 : -1;
668 aiState.targetRotation = trickDirection * AI_TRICK_SHOT_ANGLE * aiSettings.rotationAccuracy;
669
670 } else if (Math.abs(predictedBallY - paddlePos.y) < 30) {
671 // Defensive slight angle when ball is coming straight
672 aiState.rotationMode = 'DEFENSIVE';
673
674 // Slight angle to control return
675 let defensiveDirection = (predictedBallY < height / 2) ? -1 : 1;
676 aiState.targetRotation = defensiveDirection * AI_DEFENSIVE_ANGLE * aiSettings.rotationAccuracy;
677
678 } else {
679 // Normal tracking
680 aiState.rotationMode = 'NEUTRAL';
681 aiState.targetRotation = 0;
682 }
683
684 // Apply rotation confidence based on AI state
685 aiState.rotationConfidence = aiSettings.rotationAccuracy;
686 if (aiState.mode === 'RECOVERING') {
687 aiState.rotationConfidence *= 0.5; // Less confident when recovering
688 }
689
690 applyAIRotation(aiSettings);
691 }
692
693 function applyAIRotation(aiSettings) {
694 // Get current rotation state
695 let rotState = window.rotationState.right;
696
697 // Calculate rotation input needed
698 let angleDiff = aiState.targetRotation - rotState.currentAngle;
699 let rotationInput = 0;
700
701 if (Math.abs(angleDiff) > 0.05) {
702 // Determine rotation direction
703 rotationInput = Math.sign(angleDiff);
704
705 // Scale by AI rotation speed and confidence
706 rotationInput *= aiSettings.rotationSpeed * aiState.rotationConfidence;
707
708 // Add some imperfection for easier difficulties
709 if (aiSettings.rotationAccuracy < 0.8) {
710 rotationInput += (Math.random() - 0.5) * 0.2 * (1 - aiSettings.rotationAccuracy);
711 }
712 }
713
714 // Update rotation physics for AI paddle
715 updateRotationPhysics('right', rotationInput, window.rightPaddle);
716 }