Comparing changes

Choose two branches to see what's changed or to start a new pull request.

base: trunk
Choose a base ref
compare: rotation
Choose a head ref
Create pull request
Able to merge. These branches can be automatically merged.
2 commits 5 files changed 1 contributor

Commits on rotation

.DS_Storeadded
Binary file changed.
js/ai.jsmodified
@@ -16,6 +16,13 @@ const AI_NERVOUS_ENERGY = 0.5; // Random fidgeting energy
1616
 const AI_PREDICTIVE_MOVEMENT = 0.7;   // How much AI moves based on prediction
1717
 const AI_TRACKING_AGGRESSION = 0.15;  // How aggressively AI tracks the ball
1818
 
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
1926
 
2027
 // ============= AI SETTINGS =============
2128
 const AI_SETTINGS = {
@@ -33,7 +40,12 @@ const AI_SETTINGS = {
3340
         circularMotion: 0.4,
3441
         phaseSpeed: 0.06,
3542
         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
3749
     },
3850
     medium: {
3951
         reactionTime: 200,
@@ -49,7 +61,12 @@ const AI_SETTINGS = {
4961
         circularMotion: 0.6,
5062
         phaseSpeed: 0.08,
5163
         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
5370
     },
5471
     hard: {
5572
         reactionTime: 100,
@@ -65,7 +82,12 @@ const AI_SETTINGS = {
6582
         circularMotion: 0.8,
6683
         phaseSpeed: 0.12,
6784
         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
6991
     }
7092
 };
7193
 
@@ -116,7 +138,14 @@ let aiState = {
116138
     // AI Bop system
117139
     consideringBop: false,
118140
     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
120149
 };
121150
 
122151
 // ============= MAIN AI HANDLER =============
@@ -129,6 +158,7 @@ function handleAI(currentTime, ball, rightPaddle, rightSupport,
129158
     
130159
     updateAIAggression(leftScore, rightScore);
131160
     updateAILifelikeBehavior(currentTime, height);
161
+    updateAIRotation(currentTime, ball, rightPaddle, width, height, aiSettings);
132162
     
133163
     switch (aiState.mode) {
134164
         case 'TRACKING':
@@ -561,7 +591,126 @@ function executeAIMovement(aiSettings, rightSupport) {
561591
     }
562592
 }
563593
 
564
-// Utility function - should match p5.js lerp
565
-function lerp(start, stop, amt) {
566
-    return amt * (stop - start) + start;
567
-}
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
+}
js/game-systems.jsmodified
@@ -1,5 +1,4 @@
11
 // game-systems.js - Core game mechanics and physics
2
-// TODO redocument
32
 
43
 // ============= CONSTANTS =============
54
 // Canvas settings
@@ -48,6 +47,18 @@ const BOP_COOLDOWN = 0; // also also self expl. PULL it.
4847
 const ANCHOR_RECOIL         = 60;   // How far the anchor moves backward during bop
4948
 const BOP_VELOCITY_BOOST    = 5;    // Initial velocity boost for paddle
5049
 
50
+// Rotation control constants
51
+const ROTATION_SPEED        = 0.05;  // Base rotation speed (radians per frame)
52
+const ROTATION_SMOOTHING    = 0.15;  // Input smoothing for rotation (0-1)
53
+const ROTATION_DAMPING      = 0.92;  // Angular velocity damping (0-1)
54
+const ROTATION_MAX_SPEED    = 0.2;   // Maximum angular velocity (radians per frame)
55
+const ROTATION_RESISTANCE   = 3.0;   // How much harder it is to rotate against momentum
56
+const ROTATION_MOMENTUM     = 0.85;  // How much angular momentum is preserved (0-1)
57
+const ROTATION_RETURN_FORCE = 0.02;  // Force returning paddle to neutral position
58
+const ROTATION_MAX_ANGLE    = Math.PI / 4; // Maximum rotation angle (45 degrees)
59
+const ROTATION_WITH_MOMENTUM_BOOST = 1.5;  // Speed multiplier when rotating with momentum
60
+const ROTATION_AGAINST_MOMENTUM_LAG = 0.05; // Lag multiplier when rotating against momentum
61
+
5162
 // ============= BOP SYSTEM =============
5263
 let bopState = {
5364
     left: {
@@ -70,6 +81,26 @@ let bopState = {
7081
     }
7182
 };
7283
 
84
+// ============= ROTATION SYSTEM =============
85
+let rotationState = {
86
+    left: {
87
+        targetAngle: 0,           // Target angle based on input
88
+        currentAngle: 0,          // Current visual angle
89
+        angularVelocity: 0,       // Current angular velocity
90
+        angularMomentum: 0,       // Angular momentum
91
+        inputBuffer: 0,           // Smoothed rotation input (-1 to 1)
92
+        lastDirection: 0          // Last input direction for momentum checks
93
+    },
94
+    right: {
95
+        targetAngle: 0,
96
+        currentAngle: 0,
97
+        angularVelocity: 0,
98
+        angularMomentum: 0,
99
+        inputBuffer: 0,
100
+        lastDirection: 0
101
+    }
102
+};
103
+
73104
 function handleBopInput(keys, aiEnabled, currentTime, leftPaddle, rightPaddle, leftSupport, rightSupport, engine, particles) {
74105
     // Left player bop - use Left Shift for both modes
75106
     let leftBopPressed = keys['Shift'] && !keys['Control'];
@@ -356,7 +387,6 @@ function resetBall(ball, world, width, height) {
356387
     return ball;
357388
 }
358389
 
359
-// ============= COLLISION =============
360390
 // ============= COLLISION =============
361391
 function setupCollisionHandlers(engine, ball, leftPaddle, rightPaddle, particles) {
362392
     const Body = Matter.Body;
@@ -507,4 +537,88 @@ function setupCollisionHandlers(engine, ball, leftPaddle, rightPaddle, particles
507537
             }
508538
         }
509539
     });
540
+}
541
+
542
+// ============= ROTATION SYSTEM =============
543
+function handleRotationInput() {
544
+    // Left paddle rotation (A/D keys)
545
+    let leftRotationInput = 0;
546
+    if (keys['a'] || keys['A']) leftRotationInput -= 1;
547
+    if (keys['d'] || keys['D']) leftRotationInput += 1;
548
+    
549
+    // Right paddle rotation (Left/Right arrows, only if AI disabled)
550
+    let rightRotationInput = 0;
551
+    if (!aiEnabled) {
552
+        if (keys['ArrowLeft']) rightRotationInput -= 1;
553
+        if (keys['ArrowRight']) rightRotationInput += 1;
554
+    }
555
+    
556
+    // Update rotation states
557
+    updateRotationPhysics('left', leftRotationInput, leftPaddle);
558
+    if (!aiEnabled) {
559
+        updateRotationPhysics('right', rightRotationInput, rightPaddle);
560
+    }
561
+}
562
+
563
+function updateRotationPhysics(side, input, paddle) {
564
+    const Body = Matter.Body;
565
+    let state = rotationState[side];
566
+    
567
+    // Smooth the input
568
+    state.inputBuffer = lerp(state.inputBuffer, input, ROTATION_SMOOTHING);
569
+    
570
+    // Calculate resistance based on momentum
571
+    let rotatingWithMomentum = (state.inputBuffer * state.angularVelocity) > 0;
572
+    let effectiveInput = state.inputBuffer;
573
+    
574
+    if (rotatingWithMomentum && Math.abs(state.inputBuffer) > 0.1) {
575
+        // Easier to rotate with momentum
576
+        effectiveInput *= ROTATION_WITH_MOMENTUM_BOOST;
577
+    } else if (!rotatingWithMomentum && Math.abs(state.inputBuffer) > 0.1) {
578
+        // Harder to rotate against momentum
579
+        effectiveInput *= ROTATION_AGAINST_MOMENTUM_LAG;
580
+    }
581
+    
582
+    // Apply torque based on input
583
+    let torque = effectiveInput * ROTATION_SPEED;
584
+    
585
+    // Update angular velocity with torque
586
+    state.angularVelocity += torque;
587
+    
588
+    // Apply damping
589
+    state.angularVelocity *= ROTATION_DAMPING;
590
+    
591
+    // Limit maximum angular velocity
592
+    state.angularVelocity = Math.max(-ROTATION_MAX_SPEED, 
593
+                                    Math.min(ROTATION_MAX_SPEED, state.angularVelocity));
594
+    
595
+    // Apply return-to-center force when no input
596
+    if (Math.abs(state.inputBuffer) < 0.1) {
597
+        let returnForce = -state.currentAngle * ROTATION_RETURN_FORCE;
598
+        state.angularVelocity += returnForce;
599
+    }
600
+    
601
+    // Update current angle
602
+    state.currentAngle += state.angularVelocity;
603
+    
604
+    // Limit maximum rotation angle
605
+    state.currentAngle = Math.max(-ROTATION_MAX_ANGLE, 
606
+                                 Math.min(ROTATION_MAX_ANGLE, state.currentAngle));
607
+    
608
+    // Apply rotation to the paddle body
609
+    Body.setAngle(paddle, state.currentAngle);
610
+    
611
+    // Update angular momentum for next frame
612
+    state.angularMomentum = state.angularMomentum * ROTATION_MOMENTUM + 
613
+                           state.angularVelocity * (1 - ROTATION_MOMENTUM);
614
+    
615
+    // Track last direction for momentum calculations
616
+    if (Math.abs(input) > 0.1) {
617
+        state.lastDirection = Math.sign(input);
618
+    }
619
+}
620
+
621
+// ============= HELPER FUNCTIONS =============
622
+function lerp(start, stop, amt) {
623
+    return amt * (stop - start) + start;
510624
 }
js/rendering.jsmodified
@@ -344,32 +344,44 @@ function drawDebugInfo(ball, leftSupport, leftPaddle, rightSupport, rightPaddle,
344344
     text(`R Spring: ${Math.round(rightSpringLength)}px (${((SPRING_LENGTH/rightSpringLength - 1) * 100).toFixed(0)}%)`, 10, 95);
345345
     text(`Input: L=${inputBuffer.left.toFixed(2)} R=${inputBuffer.right.toFixed(2)}`, 10, 110);
346346
     
347
+    // Rotation info
348
+    if (window.rotationState) {
349
+        text(`L Rot: ${(window.rotationState.left.currentAngle * 180 / Math.PI).toFixed(1)}° (vel: ${window.rotationState.left.angularVelocity.toFixed(3)})`, 10, 125);
350
+        text(`R Rot: ${(window.rotationState.right.currentAngle * 180 / Math.PI).toFixed(1)}° (vel: ${window.rotationState.right.angularVelocity.toFixed(3)})`, 10, 140);
351
+    }
352
+    
347353
     // AI debug info
348354
     if (aiEnabled) {
349
-        text(`AI State: ${aiState.mode} | Aggression: ${aiState.aggressionLevel.toFixed(2)}`, 10, 125);
350
-        text(`Target: ${Math.round(aiState.targetY)} | Intercept: ${Math.round(aiState.interceptY)}`, 10, 140);
351
-        text(`Ball: (${Math.round(ball.position.x)}, ${Math.round(ball.position.y)}) Vel: (${ball.velocity.x.toFixed(1)}, ${ball.velocity.y.toFixed(1)})`, 10, 155);
355
+        text(`AI State: ${aiState.mode} | Aggression: ${aiState.aggressionLevel.toFixed(2)}`, 10, 155);
356
+        text(`Target: ${Math.round(aiState.targetY)} | Intercept: ${Math.round(aiState.interceptY)}`, 10, 170);
357
+        text(`Ball: (${Math.round(ball.position.x)}, ${Math.round(ball.position.y)}) Vel: (${ball.velocity.x.toFixed(1)}, ${ball.velocity.y.toFixed(1)})`, 10, 185);
358
+        
359
+        // AI rotation info
360
+        if (aiState.rotationMode !== 'NEUTRAL') {
361
+            fill(150, 200, 255, 200);
362
+            text(`AI Rotation: ${aiState.rotationMode} | Target: ${(aiState.targetRotation * 180 / Math.PI).toFixed(1)}°`, 10, 200);
363
+        }
352364
         
353365
         // AI technique indicators
354366
         if (aiState.mode === 'WINDING_UP') {
355367
             fill(255, 150, 50, 200);
356
-            text(`AI WINDING UP | Phase: ${(aiState.windupPhase % (Math.PI * 2)).toFixed(2)} | Velocity: ${aiState.currentVelocity.toFixed(1)}`, 10, 175);
368
+            text(`AI WINDING UP | Phase: ${(aiState.windupPhase % (Math.PI * 2)).toFixed(2)} | Velocity: ${aiState.currentVelocity.toFixed(1)}`, 10, 215);
357369
             
358370
             if (aiState.comboBop) {
359371
                 fill(255, 50, 255, 200);
360
-                text("⚡ COMBO PLANNED!", 10, 190);
372
+                text("⚡ COMBO PLANNED!", 10, 230);
361373
             }
362374
         } else if (aiState.mode === 'SWINGING') {
363375
             fill(255, 50, 50, 200);
364
-            text("AI POWER SWING!", 10, 175);
376
+            text("AI POWER SWING!", 10, 215);
365377
         } else if (aiState.consideringBop) {
366378
             fill(255, 255, 100, 200);
367
-            text("AI PREPARING BOP!", 10, 175);
379
+            text("AI PREPARING BOP!", 10, 215);
368380
         }
369381
         
370382
         if (bopState.right.active) {
371383
             fill(255, 255, 0, 255);
372
-            text("AI BOPPING!", 10, 190);
384
+            text("AI BOPPING!", 10, 230);
373385
         }
374386
     }
375387
 }
@@ -382,10 +394,11 @@ function drawStartMessage(aiEnabled, aiDifficulty) {
382394
     textSize(14);
383395
     
384396
     if (aiEnabled) {
385
-        text("Player vs CPU | Left paddle: W/S or Mouse/Touch", width/2, height/2 + 125);
397
+        text("Player vs CPU | Move: W/S or Mouse/Touch | Rotate: A/D | Bop: Left Shift", width/2, height/2 + 125);
386398
         text(`AI Difficulty: ${aiDifficulty.toUpperCase()}`, width/2, height/2 + 145);
387399
     } else {
388
-        text("2 Player Mode | P1: W/S | P2: ↑/↓ | Mouse/Touch: Drag paddles", width/2, height/2 + 125);
400
+        text("2 Player Mode | P1: W/S + A/D + L.Shift | P2: ↑/↓ + ←/→ + Enter", width/2, height/2 + 125);
401
+        text("Mouse/Touch: Drag paddles", width/2, height/2 + 145);
389402
     }
390403
     
391404
     textSize(12);
@@ -451,9 +464,9 @@ function drawMenu(menuState) {
451464
     textSize(12);
452465
     fill(255, 100);
453466
     if (menuState.selectedOption === 0) {
454
-        text("Controls: W/S keys + LEFT SHIFT (bop) or Mouse/Touch", width/2, height - 30);
467
+        text("Controls: W/S (move) + A/D (rotate) + LEFT SHIFT (bop) or Mouse/Touch", width/2, height - 30);
455468
     } else {
456
-        text("Controls: P1 (W/S + L.Shift) | P2 (↑/↓ + Enter) | Mouse/Touch", width/2, height - 30);
469
+        text("P1: W/S + A/D + L.Shift | P2: ↑/↓ + ←/→ + Enter | Mouse/Touch", width/2, height - 30);
457470
     }
458471
 }
459472
 
js/sprong.jsmodified
@@ -49,6 +49,10 @@ let mouseInput = {
4949
 window.inputBuffer = inputBuffer;
5050
 window.moveSupportEnhanced = moveSupportEnhanced;
5151
 window.ball = null;
52
+window.rotationState = rotationState;
53
+window.rightPaddle = null;
54
+window.width = CANVAS_WIDTH;
55
+window.height = CANVAS_HEIGHT;
5256
 
5357
 function setup() {
5458
     let canvas = createCanvas(CANVAS_WIDTH, CANVAS_HEIGHT);
@@ -79,6 +83,7 @@ function setup() {
7983
     // Create ball
8084
     ball = resetBall(null, world, width, height);
8185
     window.ball = ball;
86
+    window.rightPaddle = rightPaddle;
8287
     
8388
     // Add everything to the world
8489
     World.add(world, [
@@ -125,6 +130,7 @@ function draw() {
125130
 function handleEnhancedInput() {
126131
     handleKeyboardInput();
127132
     handleMouseTouchInput();
133
+    handleRotationInput();  // Add rotation handling
128134
     handleBopInput(keys, aiEnabled, millis(), leftPaddle, rightPaddle, 
129135
                    leftSupport, rightSupport, engine, particles);
130136