@@ -1,5 +1,5 @@ |
| 1 | 1 | // Matter.js module aliases |
| 2 | | -const Body = Matter.Body; |
| 2 | +const Body = Matter.Body; |
| 3 | 3 | const World = Matter.World; |
| 4 | 4 | const Engine = Matter.Engine; |
| 5 | 5 | const Bodies = Matter.Bodies; |
@@ -17,18 +17,18 @@ let leftSupport, leftPaddle, leftSpring; |
| 17 | 17 | let rightSupport, rightPaddle, rightSpring; |
| 18 | 18 | |
| 19 | 19 | // Game state |
| 20 | | -let gameMode = 'vs-cpu'; // 'vs-cpu' or 'vs-human' |
| 21 | | -let leftScore = 0; |
| 22 | | -let aiEnabled = true; |
| 23 | | -let gameState = 'menu'; // 'menu', 'playing', 'paused' |
| 24 | | -let rightScore = 0; |
| 20 | +let leftScore = 0; |
| 21 | +let rightScore = 0; |
| 25 | 22 | let gameStarted = false; |
| 23 | +let gameState = 'menu'; // 'menu', 'playing', 'paused' |
| 24 | +let gameMode = 'vs-cpu'; // 'vs-cpu' or 'vs-human' |
| 25 | +let aiEnabled = true; |
| 26 | 26 | |
| 27 | 27 | // Menu state |
| 28 | 28 | let menuState = { |
| 29 | | - selectedOption: 0, // 0 = 1 Player, 1 = 2 Player |
| 29 | + selectedOption: 0, // 0 = 1 Player, 1 = 2 Player |
| 30 | 30 | options: ['1 Player vs CPU', '2 Player'], |
| 31 | | - difficultySelected: 1, // 0 = Easy, 1 = Medium, 2 = Hard |
| 31 | + difficultySelected: 1, // 0 = Easy, 1 = Medium, 2 = Hard |
| 32 | 32 | difficulties: ['Easy', 'Medium', 'Hard'], |
| 33 | 33 | showDifficulty: true |
| 34 | 34 | }; |
@@ -50,8 +50,8 @@ let aiState = { |
| 50 | 50 | aggressionLevel: 0.5, // 0 = defensive, 1 = maximum aggression |
| 51 | 51 | lastHitTime: 0, |
| 52 | 52 | |
| 53 | | - // Oscillation parameters (tuned for smoother movement) |
| 54 | | - windupDistance: 40, // Slightly increased from 35 with longer spring |
| 53 | + // Oscillation parameters (increased for better windup) |
| 54 | + windupDistance: 120, // Much bigger - about half canvas height |
| 55 | 55 | swingPower: 1.05, // Reduced from 1.1 for more control |
| 56 | 56 | timingWindow: 40, // Slightly longer execution window |
| 57 | 57 | |
@@ -91,21 +91,21 @@ const PADDLE_WIDTH = 20; |
| 91 | 91 | const PADDLE_HEIGHT = 80; |
| 92 | 92 | |
| 93 | 93 | // Enhanced movement constants (tuned for faster response) |
| 94 | | -const SUPPORT_SPEED = 6.5; // Bumped up from 4.5 |
| 95 | | -const SUPPORT_ACCEL = 1.2; // Increased acceleration |
| 96 | | -const INPUT_SMOOTHING = 0.25; // More responsive |
| 97 | | -const SUPPORT_MAX_SPEED = 8; // Higher max speed |
| 94 | +const SUPPORT_SPEED = 6.5; // Bumped up from 4.5 |
| 95 | +const SUPPORT_ACCEL = 1.2; // Increased acceleration |
| 96 | +const INPUT_SMOOTHING = 0.25; // More responsive |
| 97 | +const SUPPORT_MAX_SPEED = 8; // Higher max speed |
| 98 | 98 | |
| 99 | 99 | // Touch/mouse control constants |
| 100 | | -const MOUSE_SPEED_LIMIT = 4; // Max speed for mouse movement |
| 101 | | -const MOUSE_LAG_FACTOR = 0.12; // How much lag in mouse following |
| 102 | | -const TOUCH_SENSITIVITY = 1.2; // Touch movement multiplier |
| 100 | +const MOUSE_LAG_FACTOR = 0.12; // How much lag in mouse following |
| 101 | +const MOUSE_SPEED_LIMIT = 4; // Max speed for mouse movement |
| 102 | +const TOUCH_SENSITIVITY = 1.2; // Touch movement multiplier |
| 103 | 103 | |
| 104 | 104 | // Spring physics constants (adjusted for better oscillation) |
| 105 | | -const PADDLE_MASS = 0.8; // Back to more stable value |
| 106 | | -const SPRING_LENGTH = 50; // Increased from 40 for more room |
| 107 | | -const SPRING_DAMPING = 0.6; // More damping = less erratic |
| 108 | | -const SPRING_STIFFNESS = 0.025; // Slightly lower for smoother motion |
| 105 | +const PADDLE_MASS = 0.8; // Back to more stable value |
| 106 | +const SPRING_LENGTH = 50; // Increased from 40 for more room |
| 107 | +const SPRING_DAMPING = 0.6; // More damping = less erratic |
| 108 | +const SPRING_STIFFNESS = 0.025; // Slightly lower for smoother motion |
| 109 | 109 | |
| 110 | 110 | // AI difficulty settings |
| 111 | 111 | const AI_SETTINGS = { |
@@ -178,6 +178,11 @@ function setup() { |
| 178 | 178 | leftSupport, leftPaddle, leftSpring, |
| 179 | 179 | rightSupport, rightPaddle, rightSpring |
| 180 | 180 | ]); |
| 181 | + |
| 182 | + console.log("Sprong Phase 5 Complete!"); |
| 183 | + console.log("Particle effects system"); |
| 184 | + console.log("Tuned physics for maximum bounce"); |
| 185 | + console.log("Faster, more responsive paddles"); |
| 181 | 186 | } |
| 182 | 187 | |
| 183 | 188 | function createSpringPaddleSystem(side) { |
@@ -426,77 +431,122 @@ function updateAIAggression() { |
| 426 | 431 | } |
| 427 | 432 | |
| 428 | 433 | function handleAITracking(currentTime, ballPos, ballVel, aiSettings) { |
| 429 | | - // Only react if enough time has passed (reaction delay) |
| 430 | | - if (currentTime - aiState.lastUpdateTime < aiSettings.reactionTime) return; |
| 431 | | - |
| 432 | | - // Check if ball is approaching AI paddle |
| 434 | + // Always track ball position for more responsive movement |
| 433 | 435 | let ballApproaching = ballVel.x > 0; |
| 434 | 436 | let ballDistance = width - ballPos.x; |
| 435 | 437 | let ballSpeed = Math.sqrt(ballVel.x * ballVel.x + ballVel.y * ballVel.y); |
| 436 | 438 | |
| 437 | | - if (ballApproaching && ballDistance < 280) { |
| 438 | | - // Calculate intercept point with advanced prediction |
| 439 | | - let timeToReach = ballDistance / Math.abs(ballVel.x); |
| 440 | | - aiState.interceptY = ballPos.y + ballVel.y * timeToReach; |
| 441 | | - |
| 442 | | - // Account for wall bounces |
| 443 | | - if (aiState.interceptY < 50) { |
| 444 | | - aiState.interceptY = 100 - aiState.interceptY; |
| 445 | | - } else if (aiState.interceptY > height - 50) { |
| 446 | | - aiState.interceptY = 2 * (height - 50) - aiState.interceptY; |
| 447 | | - } |
| 448 | | - |
| 449 | | - // Add accuracy error |
| 450 | | - let error = (random() - 0.5) * 35 * (1 - aiSettings.accuracy); |
| 451 | | - aiState.interceptY += error; |
| 452 | | - |
| 453 | | - // Smart oscillation decision: lower velocity threshold and more distance |
| 454 | | - let shouldWindUp = ballSpeed < 7 && // Reduced from 9 - easier to trigger |
| 455 | | - ballDistance > 160 && // More distance required |
| 456 | | - Math.abs(ballVel.y) < 5 && // Stricter vertical movement limit |
| 457 | | - random() < aiSettings.oscillation * aiState.aggressionLevel; |
| 439 | + // Calculate where the AI paddle currently is (not the anchor) |
| 440 | + let paddlePos = rightPaddle.position; |
| 441 | + let anchorPos = rightSupport.position; |
| 442 | + |
| 443 | + // Continuous ball tracking with paddle awareness |
| 444 | + let trackingIntensity = ballApproaching ? 0.08 : 0.03; |
| 445 | + |
| 446 | + // Calculate where anchor should be to position PADDLE at target Y |
| 447 | + let desiredPaddleY = ballPos.y + aiState.microAdjustment; |
| 448 | + desiredPaddleY = constrain(desiredPaddleY, 80, height - 80); |
| 449 | + |
| 450 | + // Estimate anchor position needed to get paddle to desired position |
| 451 | + // This is tricky because spring physics affects paddle position |
| 452 | + let anchorOffsetNeeded = calculateAnchorOffset(desiredPaddleY, paddlePos, anchorPos); |
| 453 | + let targetAnchorY = desiredPaddleY + anchorOffsetNeeded; |
| 454 | + |
| 455 | + // Always apply some level of paddle-aware tracking |
| 456 | + aiState.targetY = lerp(aiState.targetY, targetAnchorY, trackingIntensity); |
| 457 | + |
| 458 | + // Only do advanced prediction and windup logic if enough time has passed (reaction delay) |
| 459 | + if (currentTime - aiState.lastUpdateTime > aiSettings.reactionTime) { |
| 458 | 460 | |
| 459 | | - if (shouldWindUp) { |
| 460 | | - // Start winding up for power shot |
| 461 | | - aiState.mode = 'WINDING_UP'; |
| 462 | | - aiState.windupStartTime = currentTime; |
| 461 | + if (ballApproaching && ballDistance < 300) { |
| 462 | + // Calculate intercept point with advanced prediction |
| 463 | + let timeToReach = ballDistance / Math.abs(ballVel.x); |
| 464 | + let predictedBallY = ballPos.y + ballVel.y * timeToReach; |
| 465 | + |
| 466 | + // Account for wall bounces |
| 467 | + if (predictedBallY < 50) { |
| 468 | + predictedBallY = 100 - predictedBallY; |
| 469 | + } else if (predictedBallY > height - 50) { |
| 470 | + predictedBallY = 2 * (height - 50) - predictedBallY; |
| 471 | + } |
| 463 | 472 | |
| 464 | | - // Determine windup direction (opposite of intercept) |
| 465 | | - let currentY = rightSupport.position.y; |
| 466 | | - aiState.windupDirection = aiState.interceptY > currentY ? -1 : 1; |
| 473 | + // Add accuracy error |
| 474 | + let error = (random() - 0.5) * 35 * (1 - aiSettings.accuracy); |
| 475 | + predictedBallY += error; |
| 467 | 476 | |
| 468 | | - } else { |
| 469 | | - // Simple tracking without oscillation |
| 470 | | - aiState.targetY = aiState.interceptY; |
| 477 | + // Calculate where PADDLE needs to be to hit the ball |
| 478 | + aiState.interceptY = predictedBallY; |
| 479 | + |
| 480 | + // Calculate where ANCHOR needs to be to position paddle correctly |
| 481 | + let interceptAnchorOffset = calculateAnchorOffset(aiState.interceptY, paddlePos, anchorPos); |
| 482 | + let targetAnchorForIntercept = aiState.interceptY + interceptAnchorOffset; |
| 483 | + |
| 484 | + // VERY selective windup decision: only for very slow balls |
| 485 | + let shouldWindUp = ballSpeed < 4.5 && // Much stricter - very slow balls only |
| 486 | + ballDistance > 200 && // Lots of distance required |
| 487 | + Math.abs(ballVel.y) < 3 && // Very limited vertical movement |
| 488 | + Math.abs(ballVel.x) > 1 && // Ball must be actually moving toward AI |
| 489 | + random() < aiSettings.oscillation * aiState.aggressionLevel * 0.3; // Much lower chance |
| 490 | + |
| 491 | + if (shouldWindUp) { |
| 492 | + // Start winding up for power shot |
| 493 | + aiState.mode = 'WINDING_UP'; |
| 494 | + aiState.windupStartTime = currentTime; |
| 495 | + |
| 496 | + // Determine windup direction (opposite of where paddle needs to be) |
| 497 | + aiState.windupDirection = aiState.interceptY > paddlePos.y ? -1 : 1; |
| 498 | + |
| 499 | + } else { |
| 500 | + // Use paddle-aware intercept positioning |
| 501 | + aiState.targetY = lerp(aiState.targetY, targetAnchorForIntercept, 0.3); |
| 502 | + } |
| 503 | + |
| 504 | + aiState.lastUpdateTime = currentTime; |
| 471 | 505 | } |
| 472 | | - |
| 473 | | - aiState.lastUpdateTime = currentTime; |
| 474 | | - } else { |
| 475 | | - // When ball isn't approaching, do lifelike tracking |
| 476 | | - let trackingY = ballPos.y + aiState.microAdjustment; |
| 477 | | - trackingY = constrain(trackingY, 80, height - 80); |
| 478 | | - aiState.targetY = lerp(aiState.targetY, trackingY, 0.02); // Very gentle tracking |
| 479 | 506 | } |
| 480 | 507 | } |
| 481 | 508 | |
| 509 | +// Helper function to estimate where anchor should be to position paddle at target Y |
| 510 | +function calculateAnchorOffset(targetPaddleY, currentPaddlePos, currentAnchorPos) { |
| 511 | + // Calculate current spring vector |
| 512 | + let springVectorY = currentPaddlePos.y - currentAnchorPos.y; |
| 513 | + |
| 514 | + // The paddle tends to lag behind the anchor due to spring physics |
| 515 | + // We need to account for this offset when positioning |
| 516 | + |
| 517 | + // Simple approximation: if spring is compressed/extended, paddle will be offset |
| 518 | + let currentSpringLength = dist(currentAnchorPos.x, currentAnchorPos.y, |
| 519 | + currentPaddlePos.x, currentPaddlePos.y); |
| 520 | + let springCompression = SPRING_LENGTH - currentSpringLength; |
| 521 | + |
| 522 | + // Estimate the Y offset the paddle will have relative to anchor |
| 523 | + // This is a simplified physics approximation |
| 524 | + let estimatedPaddleOffset = springVectorY * 0.8; // Paddle lags behind anchor |
| 525 | + |
| 526 | + // Return the offset needed for anchor positioning |
| 527 | + return -estimatedPaddleOffset; |
| 528 | +} |
| 529 | + |
| 482 | 530 | function handleAIWindup(currentTime, ballPos, ballVel, aiSettings) { |
| 483 | 531 | let windupTime = currentTime - aiState.windupStartTime; |
| 484 | | - let maxWindupTime = 350 / Math.max(aiState.aggressionLevel, 0.3); // Slower windup, minimum time |
| 532 | + let maxWindupTime = 500 / Math.max(aiState.aggressionLevel, 0.3); // Longer windup for bigger movement |
| 485 | 533 | |
| 486 | | - // Calculate windup target (move away from intercept point) |
| 487 | | - let currentY = rightSupport.position.y; |
| 488 | | - let windupTarget = currentY + aiState.windupDirection * aiState.windupDistance * aiState.aggressionLevel; |
| 489 | | - windupTarget = constrain(windupTarget, 70, height - 70); // More margin from edges |
| 534 | + // Calculate windup target with much bigger distance |
| 535 | + let paddlePos = rightPaddle.position; |
| 536 | + let windupTarget = paddlePos.y + aiState.windupDirection * aiState.windupDistance * aiState.aggressionLevel; |
| 537 | + windupTarget = constrain(windupTarget, 50, height - 50); // Allow closer to edges for big windup |
| 490 | 538 | |
| 491 | | - aiState.targetY = windupTarget; |
| 539 | + // Convert paddle target to anchor target using paddle awareness |
| 540 | + let anchorOffsetNeeded = calculateAnchorOffset(windupTarget, paddlePos, rightSupport.position); |
| 541 | + aiState.targetY = windupTarget + anchorOffsetNeeded; |
| 492 | 542 | |
| 493 | | - // Check if it's time to swing - more conservative timing |
| 543 | + // Check if it's time to swing - allow more time for bigger windups |
| 494 | 544 | let ballDistance = width - ballPos.x; |
| 495 | 545 | let ballSpeed = Math.sqrt(ballVel.x * ballVel.x + ballVel.y * ballVel.y); |
| 496 | 546 | |
| 497 | 547 | let shouldSwing = windupTime > maxWindupTime || |
| 498 | | - ballDistance < 90 || |
| 499 | | - ballSpeed > 8; // Lower threshold - abort if ball speeds up |
| 548 | + ballDistance < 120 || |
| 549 | + ballSpeed > 6; // Abort if ball speeds up even slightly |
| 500 | 550 | |
| 501 | 551 | if (shouldSwing) { |
| 502 | 552 | aiState.mode = 'SWINGING'; |
@@ -505,8 +555,12 @@ function handleAIWindup(currentTime, ballPos, ballVel, aiSettings) { |
| 505 | 555 | } |
| 506 | 556 | |
| 507 | 557 | function handleAISwing(currentTime, ballPos, ballVel, aiSettings) { |
| 508 | | - // Aggressive swing toward intercept point |
| 509 | | - aiState.targetY = aiState.interceptY; |
| 558 | + // Aggressive swing toward intercept point - but position anchor for paddle placement |
| 559 | + let paddlePos = rightPaddle.position; |
| 560 | + |
| 561 | + // Calculate where anchor should be to get paddle to intercept point |
| 562 | + let anchorOffsetNeeded = calculateAnchorOffset(aiState.interceptY, paddlePos, rightSupport.position); |
| 563 | + aiState.targetY = aiState.interceptY + anchorOffsetNeeded; |
| 510 | 564 | |
| 511 | 565 | let swingTime = currentTime - aiState.swingStartTime; |
| 512 | 566 | let maxSwingTime = aiState.timingWindow; |
@@ -529,8 +583,18 @@ function handleAIRecovery(currentTime, ballPos, ballVel, aiSettings) { |
| 529 | 583 | } |
| 530 | 584 | |
| 531 | 585 | function handleAIAnticipation(currentTime, ballPos, ballVel, aiSettings) { |
| 532 | | - // Stay near center with subtle lifelike movements |
| 533 | | - aiState.targetY = aiState.idleTarget + aiState.breathingOffset + aiState.microAdjustment; |
| 586 | + // Stay near center with subtle lifelike movements, but use paddle-aware positioning |
| 587 | + let baseTarget = aiState.idleTarget + aiState.breathingOffset + aiState.microAdjustment; |
| 588 | + let ballTrackingTarget = ballPos.y; |
| 589 | + |
| 590 | + // Blend idle position with gentle ball tracking |
| 591 | + let desiredPaddleY = lerp(baseTarget, ballTrackingTarget, 0.15); |
| 592 | + |
| 593 | + // Convert paddle target to anchor target |
| 594 | + let paddlePos = rightPaddle.position; |
| 595 | + let anchorPos = rightSupport.position; |
| 596 | + let anchorOffsetNeeded = calculateAnchorOffset(desiredPaddleY, paddlePos, anchorPos); |
| 597 | + aiState.targetY = desiredPaddleY + anchorOffsetNeeded; |
| 534 | 598 | |
| 535 | 599 | // Switch back to tracking when ball changes direction |
| 536 | 600 | if (ballVel.x > 0) { |
@@ -543,14 +607,14 @@ function executeAIMovement(aiSettings) { |
| 543 | 607 | let currentY = rightSupport.position.y; |
| 544 | 608 | let deltaY = aiState.targetY - currentY; |
| 545 | 609 | |
| 546 | | - if (Math.abs(deltaY) > 2) { // Smaller dead zone for more responsive micro-movements |
| 547 | | - let baseSpeed = 0.06 * aiSettings.speed; // Slightly slower base movement |
| 610 | + if (Math.abs(deltaY) > 1) { // Very small dead zone for responsive tracking |
| 611 | + let baseSpeed = 0.08 * aiSettings.speed; // Back to more responsive speed |
| 548 | 612 | |
| 549 | 613 | // Apply swing power during swing phase |
| 550 | 614 | if (aiState.mode === 'SWINGING') { |
| 551 | 615 | baseSpeed *= aiState.swingPower * (1 + aiState.aggressionLevel * 0.3); |
| 552 | 616 | } else if (aiState.mode === 'WINDING_UP') { |
| 553 | | - baseSpeed *= 0.7; // Slower during windup for more deliberate movement |
| 617 | + baseSpeed *= 0.6; // Slower during windup for more deliberate movement |
| 554 | 618 | } |
| 555 | 619 | |
| 556 | 620 | // Apply aggression multiplier |
@@ -969,7 +1033,7 @@ function drawDebugInfo() { |
| 969 | 1033 | // Show AI technique indicators |
| 970 | 1034 | if (aiState.mode === 'WINDING_UP') { |
| 971 | 1035 | fill(255, 150, 50, 200); |
| 972 | | - text("AI WINDING UP FOR POWER SHOT", 10, 175); |
| 1036 | + text("🔄 AI WINDING UP FOR POWER SHOT", 10, 175); |
| 973 | 1037 | } else if (aiState.mode === 'SWINGING') { |
| 974 | 1038 | fill(255, 50, 50, 200); |
| 975 | 1039 | text("⚡ AI POWER SWING!", 10, 175); |
@@ -1176,7 +1240,7 @@ function keyPressed() { |
| 1176 | 1240 | if (key === 'm' || key === 'M') { |
| 1177 | 1241 | aiEnabled = !aiEnabled; |
| 1178 | 1242 | gameMode = aiEnabled ? 'vs-cpu' : 'vs-human'; |
| 1179 | | - console.log(`Switched to ${gameMode} mode`); |
| 1243 | + console.log("Switched to " + gameMode + " mode"); |
| 1180 | 1244 | } |
| 1181 | 1245 | |
| 1182 | 1246 | // Change AI difficulty (only during gameplay) |
@@ -1188,7 +1252,7 @@ function keyPressed() { |
| 1188 | 1252 | } else { |
| 1189 | 1253 | aiState.difficulty = 'easy'; |
| 1190 | 1254 | } |
| 1191 | | - console.log(`AI difficulty: ${aiState.difficulty}`); |
| 1255 | + console.log("AI difficulty: " + aiState.difficulty); |
| 1192 | 1256 | } |
| 1193 | 1257 | |
| 1194 | 1258 | // Reset game with spacebar |
@@ -1219,7 +1283,7 @@ function keyPressed() { |
| 1219 | 1283 | } |
| 1220 | 1284 | |
| 1221 | 1285 | // Return to menu with ESC |
| 1222 | | - if (keyCode === 27) { // ESC key |
| 1286 | + if (keyCode === 27) { |
| 1223 | 1287 | gameState = 'menu'; |
| 1224 | 1288 | gameStarted = false; |
| 1225 | 1289 | particles = []; |
@@ -1285,7 +1349,7 @@ function startGameWithSelection() { |
| 1285 | 1349 | // Clear particles |
| 1286 | 1350 | particles = []; |
| 1287 | 1351 | |
| 1288 | | - console.log(`Started ${gameMode} mode${aiEnabled ? ' - Difficulty: ' + aiState.difficulty : ''}`); |
| 1352 | + console.log("Started " + gameMode + " mode" + (aiEnabled ? " - Difficulty: " + aiState.difficulty : "")); |
| 1289 | 1353 | } |
| 1290 | 1354 | |
| 1291 | 1355 | function keyReleased() { |