@@ -16,6 +16,13 @@ const AI_NERVOUS_ENERGY = 0.5; // Random fidgeting energy |
| 16 | 16 | const AI_PREDICTIVE_MOVEMENT = 0.7; // How much AI moves based on prediction |
| 17 | 17 | const AI_TRACKING_AGGRESSION = 0.15; // How aggressively AI tracks the ball |
| 18 | 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 |
| 19 | 26 | |
| 20 | 27 | // ============= AI SETTINGS ============= |
| 21 | 28 | const AI_SETTINGS = { |
@@ -33,7 +40,12 @@ const AI_SETTINGS = { |
| 33 | 40 | circularMotion: 0.4, |
| 34 | 41 | phaseSpeed: 0.06, |
| 35 | 42 | idleMovement: 0.3, |
| 36 | | - trackingAggression: 0.1 |
| 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 |
| 37 | 49 | }, |
| 38 | 50 | medium: { |
| 39 | 51 | reactionTime: 200, |
@@ -49,7 +61,12 @@ const AI_SETTINGS = { |
| 49 | 61 | circularMotion: 0.6, |
| 50 | 62 | phaseSpeed: 0.08, |
| 51 | 63 | idleMovement: 0.5, |
| 52 | | - trackingAggression: 0.15 |
| 64 | + trackingAggression: 0.15, |
| 65 | + // Rotation settings |
| 66 | + rotationUse: 0.5, |
| 67 | + rotationAccuracy: 0.7, |
| 68 | + rotationSpeed: 0.8, |
| 69 | + rotationAnticipation: 0.6 |
| 53 | 70 | }, |
| 54 | 71 | hard: { |
| 55 | 72 | reactionTime: 100, |
@@ -65,7 +82,12 @@ const AI_SETTINGS = { |
| 65 | 82 | circularMotion: 0.8, |
| 66 | 83 | phaseSpeed: 0.12, |
| 67 | 84 | idleMovement: 0.8, |
| 68 | | - trackingAggression: 0.25 |
| 85 | + trackingAggression: 0.25, |
| 86 | + // Rotation settings |
| 87 | + rotationUse: 0.8, |
| 88 | + rotationAccuracy: 0.9, |
| 89 | + rotationSpeed: 1.0, |
| 90 | + rotationAnticipation: 0.85 |
| 69 | 91 | } |
| 70 | 92 | }; |
| 71 | 93 | |
@@ -116,7 +138,14 @@ let aiState = { |
| 116 | 138 | // AI Bop system |
| 117 | 139 | consideringBop: false, |
| 118 | 140 | bopDecisionTime: 0, |
| 119 | | - bopTiming: 200 |
| 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 |
| 120 | 149 | }; |
| 121 | 150 | |
| 122 | 151 | // ============= MAIN AI HANDLER ============= |
@@ -129,6 +158,7 @@ function handleAI(currentTime, ball, rightPaddle, rightSupport, |
| 129 | 158 | |
| 130 | 159 | updateAIAggression(leftScore, rightScore); |
| 131 | 160 | updateAILifelikeBehavior(currentTime, height); |
| 161 | + updateAIRotation(currentTime, ball, rightPaddle, width, height, aiSettings); |
| 132 | 162 | |
| 133 | 163 | switch (aiState.mode) { |
| 134 | 164 | case 'TRACKING': |
@@ -559,4 +589,128 @@ function executeAIMovement(aiSettings, rightSupport) { |
| 559 | 589 | window.inputBuffer.right *= 0.9; // Slower decay for more visible movement |
| 560 | 590 | } |
| 561 | 591 | } |
| 562 | | -} |
| 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 | +} |