User prompt
Please fix the bug: 'Error: Invalid value. Only literals or 1-level deep objects/arrays containing literals are allowed.' in or related to this line: 'storage.leaderboard = leaderboard;' Line Number: 574 ↪💡 Consider importing and using the following plugins: @upit/storage.v1
User prompt
ask players to insert their names in the beginning and keep track of every player that played the game and make a leaderboard with the best 10 players name and scores ↪💡 Consider importing and using the following plugins: @upit/storage.v1
User prompt
player can choose the spinning level between -20 to 20
User prompt
goalkeeper should sometimes saves the ball ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
make the goalkeeper saves the shots that are close to the middle of the goal. only the balls that goes to the close to the corner of the goals will not be saved by the goalkeeper ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
bring the goalkeeper in front of the goal
User prompt
goalkeeper should return to its initial position after each shot ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
goalkeeper moves faster to save the goal and sometimes the goalkeeper will save ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
goalkeeper now moves towards the balls direction to save from scoring ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
bring the defenders in front of the goal, they shouldnt stay inside of the goal
User prompt
adjust the scoring zone in line with the goal zone, only if the ball touches to goal or the inside lines it will be counted as a score
User prompt
can you adjust goalkeeper to lower of the screen like he is standing in front of the goal
User prompt
I want lighter pink
User prompt
I changed my mind. I want pink background
User prompt
I want a little darker green
User prompt
can you change background color to lighter green
User prompt
move the buttons for spin to a little bit left and right just to adjust according to the text
User prompt
make all the texts and buttons bigger by 25%
User prompt
make all the texts and buttons bigger by x2
User prompt
can you add to the instruction page the info of "inside of the lines are counted as scores as well"
User prompt
can you add personal best score to the game screen? so that player can compete with its personal best ↪💡 Consider importing and using the following plugins: @upit/storage.v1
User prompt
make the 2 defenders stay at the same place side by side, but make the other 2 players randomly
User prompt
starting from to 10th round put 1 random defender in front of the goal
User prompt
starting from to 10th round put 1 random defender close the goal
User prompt
change button press mechanism to clicking the arrows to change spin levels
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
var Ball = Container.expand(function () {
var self = Container.call(this);
var ballGraphics = self.attachAsset('ball', {
anchorX: 0.5,
anchorY: 0.5
});
self.velocityX = 0;
self.velocityY = 0;
self.gravity = 0;
self.friction = 0.98;
self.isMoving = false;
self.spinForce = 0;
self.spinDecay = 0.98;
self.shoot = function (power, angle, spin) {
self.velocityX = Math.cos(angle) * power;
self.velocityY = Math.sin(angle) * power;
self.spinForce = spin || 0;
self.isMoving = true;
LK.getSound('kick').play();
};
self.reset = function () {
self.x = ballStartX;
self.y = ballStartY;
self.velocityX = 0;
self.velocityY = 0;
self.isMoving = false;
self.spinForce = 0;
};
self.update = function () {
if (self.isMoving) {
// Apply spin curve effect
if (self.spinForce !== 0) {
// Calculate perpendicular force to current velocity for curve
var speed = Math.sqrt(self.velocityX * self.velocityX + self.velocityY * self.velocityY);
if (speed > 0) {
var normalX = -self.velocityY / speed;
var normalY = self.velocityX / speed;
self.velocityX += normalX * self.spinForce * 0.1;
self.velocityY += normalY * self.spinForce * 0.1;
}
// Decay spin over time
self.spinForce *= self.spinDecay;
}
self.x += self.velocityX;
self.y += self.velocityY;
// No friction applied - ball maintains constant velocity
// Bounds checking
if (self.x < 0 || self.x > 2048 || self.y > 2732) {
self.isMoving = false;
ballMissed();
}
}
};
return self;
});
var Defender = Container.expand(function () {
var self = Container.call(this);
var defenderGraphics = self.attachAsset('defender', {
anchorX: 0.5,
anchorY: 1.0
});
return self;
});
var Goalkeeper = Container.expand(function () {
var self = Container.call(this);
var keeperGraphics = self.attachAsset('goalkeeper', {
anchorX: 0.5,
anchorY: 1.0
});
return self;
});
var TrajectoryDot = Container.expand(function () {
var self = Container.call(this);
var dotGraphics = self.attachAsset('trajectory', {
anchorX: 0.5,
anchorY: 0.5
});
dotGraphics.alpha = 0.6;
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x7DD87D
});
/****
* Game Code
****/
// Game variables
var currentRound = 1;
var attemptsLeft = 3;
var ballStartX = 1024;
var ballStartY = 2500;
var goalX = 1024;
var goalY = 400;
var isAiming = false;
var dragStartX = 0;
var dragStartY = 0;
var dragCurrentX = 0;
var dragCurrentY = 0;
var trajectoryDots = [];
var defenders = [];
// Swipe tracking variables
var swipePath = [];
var swipeStartTime = 0;
var swipeSpeed = 0;
var swipeCurve = 0;
var maxSwipePoints = 10;
// Create grass field
var grass = game.addChild(LK.getAsset('grass', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0
}));
// Create goal structure
var goal = game.addChild(LK.getAsset('goal', {
anchorX: 0.5,
anchorY: 0.5,
x: goalX,
y: goalY + 100
}));
goal.alpha = 0.3; // Make goal semi-transparent so we can see the ball
// Create goalkeeper
var goalkeeper = game.addChild(new Goalkeeper());
goalkeeper.x = goalX;
goalkeeper.y = goalY + 450; // Move goalkeeper in front of the goal
// Store goalkeeper's initial position for reset
var goalkeeperInitialX = goalkeeper.x;
var goalkeeperInitialY = goalkeeper.y;
// Create ball
var ball = game.addChild(new Ball());
ball.reset();
// UI Elements
var roundText = new Text2('Round: 1', {
size: 100,
fill: 0xFFFFFF
});
roundText.anchor.set(0.5, 0);
LK.gui.top.addChild(roundText);
var attemptsText = new Text2('Attempts: 3', {
size: 75,
fill: 0xFFFFFF
});
attemptsText.anchor.set(1.0, 0);
LK.gui.topRight.addChild(attemptsText);
var personalBest = storage.personalBest || 0;
var personalBestText = new Text2('Best: ' + personalBest, {
size: 75,
fill: 0x00FF00
});
personalBestText.anchor.set(0, 0);
LK.gui.topLeft.addChild(personalBestText);
personalBestText.x = 120; // Move away from platform menu icon
var powerText = new Text2('', {
size: 62,
fill: 0xFFFF00
});
powerText.anchor.set(0.5, 0.5);
game.addChild(powerText);
var curveText = new Text2('Spin: 0', {
size: 75,
fill: 0xFFFFFF
});
curveText.anchor.set(0.5, 0.5);
curveText.x = 1024;
curveText.y = 200;
game.addChild(curveText);
var selectedCurve = 0;
var maxCurve = 8;
var curveValues = [-8, -6, -4, -2, -1, 0, 1, 2, 4, 6, 8];
var curveIndex = 5; // Start at 0 curve
var fixedSpeed = 35;
// Create curve adjustment buttons
var leftCurveButton = new Text2('<', {
size: 125,
fill: 0xFFFF00
});
leftCurveButton.anchor.set(0.5, 0.5);
leftCurveButton.x = 850;
leftCurveButton.y = 200;
game.addChild(leftCurveButton);
var rightCurveButton = new Text2('>', {
size: 125,
fill: 0xFFFF00
});
rightCurveButton.anchor.set(0.5, 0.5);
rightCurveButton.x = 1198;
rightCurveButton.y = 200;
game.addChild(rightCurveButton);
// Function to update curve display
function updateCurveDisplay() {
selectedCurve = curveValues[curveIndex];
if (selectedCurve === 0) {
curveText.setText('Spin: 0');
} else {
curveText.setText('Spin: ' + (selectedCurve > 0 ? '+' : '') + selectedCurve);
}
console.log('Curve updated to:', selectedCurve, 'Index:', curveIndex);
}
// Initialize curve display
updateCurveDisplay();
// Initialize defenders for current round
function setupRound() {
// Clear existing defenders
for (var i = 0; i < defenders.length; i++) {
defenders[i].destroy();
}
defenders = [];
// Add defenders based on round
var numDefenders = Math.min(currentRound - 1, 4);
for (var i = 0; i < numDefenders; i++) {
var defender = game.addChild(new Defender());
if (i < 2) {
// First 2 defenders stay side by side in fixed positions
var spacing = 120;
var startX = goalX - spacing / 2;
defender.x = startX + i * spacing;
defender.y = goalY + 450 + currentRound * 30;
} else {
// Other 2 defenders are placed randomly
var randomX = goalX + (Math.random() - 0.5) * 600; // Random X within a reasonable area
var randomY = goalY + 400 + Math.random() * 200; // Random Y in front of goal
defender.x = randomX;
defender.y = randomY;
}
defenders.push(defender);
}
// Starting from round 10, add 1 random defender in front of the goal
if (currentRound >= 10) {
var randomDefender = game.addChild(new Defender());
// Random position in front of goal area
var randomX = goalX + (Math.random() - 0.5) * 800; // Random X within goal width area
var randomY = goalY + 350 + Math.random() * 150; // Random Y in front of goal
randomDefender.x = randomX;
randomDefender.y = randomY;
defenders.push(randomDefender);
}
roundText.setText('Round: ' + currentRound);
attemptsText.setText('Attempts: ' + attemptsLeft);
}
// Create trajectory preview
function createTrajectoryPreview(power, angle, spin) {
// Clear existing dots
for (var i = 0; i < trajectoryDots.length; i++) {
trajectoryDots[i].destroy();
}
trajectoryDots = [];
// Calculate trajectory points
var steps = 15;
var stepTime = 0.8;
var tempVelX = Math.cos(angle) * power * 0.7;
var tempVelY = Math.sin(angle) * power * 0.7;
var tempX = ball.x;
var tempY = ball.y;
var tempSpin = spin || 0;
for (var i = 0; i < steps; i++) {
// Apply spin curve effect in preview
if (tempSpin !== 0) {
var speed = Math.sqrt(tempVelX * tempVelX + tempVelY * tempVelY);
if (speed > 0) {
var normalX = -tempVelY / speed;
var normalY = tempVelX / speed;
tempVelX += normalX * tempSpin * 0.1;
tempVelY += normalY * tempSpin * 0.1;
}
tempSpin *= 0.98;
}
tempX += tempVelX * stepTime;
tempY += tempVelY * stepTime;
// No gravity applied to trajectory preview
tempVelX *= 0.98;
tempVelY *= 0.98;
if (tempY > 2732 || tempX < 0 || tempX > 2048) break;
var dot = game.addChild(new TrajectoryDot());
dot.x = tempX;
dot.y = tempY;
trajectoryDots.push(dot);
}
}
// Clear trajectory preview
function clearTrajectoryPreview() {
for (var i = 0; i < trajectoryDots.length; i++) {
trajectoryDots[i].destroy();
}
trajectoryDots = [];
}
// Calculate curve from swipe path
function calculateSwipeCurve() {
if (swipePath.length < 3) return 0;
var totalCurvature = 0;
var validSegments = 0;
var curvatureSum = 0;
var pathLength = 0;
// Calculate curvature at each point in the path
for (var i = 1; i < swipePath.length - 1; i++) {
var p1 = swipePath[i - 1];
var p2 = swipePath[i];
var p3 = swipePath[i + 1];
// Calculate vectors
var v1x = p2.x - p1.x;
var v1y = p2.y - p1.y;
var v2x = p3.x - p2.x;
var v2y = p3.y - p2.y;
// Calculate cross product to determine curve direction
var crossProduct = v1x * v2y - v1y * v2x;
// Calculate magnitudes
var mag1 = Math.sqrt(v1x * v1x + v1y * v1y);
var mag2 = Math.sqrt(v2x * v2x + v2y * v2y);
if (mag1 > 5 && mag2 > 5) {
// Only consider significant movements
curvatureSum += crossProduct / (mag1 * mag2);
validSegments++;
pathLength += mag1;
}
}
if (validSegments > 0) {
totalCurvature = curvatureSum / validSegments;
// Scale based on path length and speed
var lengthFactor = Math.min(pathLength / 200, 2);
var speedFactor = Math.min(swipeSpeed / 500, 2);
var finalCurve = totalCurvature * lengthFactor * speedFactor * 15;
return Math.max(-10, Math.min(10, finalCurve));
}
return 0;
}
// Calculate swipe speed
function calculateSwipeSpeed() {
if (swipePath.length < 2) return 0;
var totalDistance = 0;
var totalTime = 0;
for (var i = 1; i < swipePath.length; i++) {
var p1 = swipePath[i - 1];
var p2 = swipePath[i];
var dx = p2.x - p1.x;
var dy = p2.y - p1.y;
totalDistance += Math.sqrt(dx * dx + dy * dy);
totalTime += p2.time - p1.time;
}
return totalTime > 0 ? totalDistance / totalTime * 1000 : 0;
}
// Ball collision detection
function checkCollisions() {
// Check goalkeeper collision first - this prevents scoring
if (ball.intersects(goalkeeper)) {
// Enhanced goalkeeper save logic based on ball position relative to goal
var goalLeft = goalX - 400; // Left side of goal
var goalRight = goalX + 400; // Right side of goal
var goalWidth = 800; // Total goal width
var goalCenter = goalX; // Goal center X position
var distanceFromCenter = Math.abs(ball.x - goalCenter);
var distanceFromGoalEdge = Math.min(Math.abs(ball.x - goalLeft), Math.abs(ball.x - goalRight));
var ballSpeed = Math.sqrt(ball.velocityX * ball.velocityX + ball.velocityY * ball.velocityY);
// Calculate save chance based on position in goal
var saveChance = 0;
// Define middle zone (center 60% of goal) and corner zones (outer 20% each side)
var middleZoneWidth = goalWidth * 0.6; // 480 pixels from center
var cornerZoneWidth = goalWidth * 0.2; // 160 pixels each corner
if (distanceFromCenter <= middleZoneWidth / 2) {
// Ball is in the middle zone - high save chance
saveChance = 0.95; // 95% save chance in middle
} else if (distanceFromGoalEdge <= cornerZoneWidth) {
// Ball is in corner zone - very low save chance
saveChance = 0.1; // Only 10% save chance in corners
} else {
// Ball is in transition zone between middle and corner
saveChance = 0.4; // Moderate save chance in transition
}
// Adjust save chance based on ball speed (faster balls harder to save)
if (ballSpeed > 30) saveChance -= 0.2;
if (ballSpeed > 40) saveChance -= 0.2;
// Ensure save chance stays within reasonable bounds
saveChance = Math.max(0.05, Math.min(0.98, saveChance));
// Random save attempt
if (Math.random() < saveChance) {
// Successful save
ball.isMoving = false;
LK.getSound('save').play();
LK.effects.flashObject(goalkeeper, 0xffff00, 500);
// Animate goalkeeper dive with tween
tween(goalkeeper, {
scaleX: 1.3,
scaleY: 0.8
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(goalkeeper, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 200,
onFinish: function onFinish() {
resetGoalkeeper(); // Reset goalkeeper position after dive animation
}
});
}
});
ballMissed();
return;
} else {
// Goalkeeper fails to save - ball continues
LK.effects.flashObject(goalkeeper, 0xff6666, 300);
}
}
// Check goal scoring - ball must be inside goal area including side lines
// Goal asset is positioned at goalY + 100 and has height 584.38, width 800
var goalLeft = goalX - 400; // Left side line of goal (800/2 = 400)
var goalRight = goalX + 400; // Right side line of goal
var goalTop = goalY + 100 - 292; // Top of goal (goalY + 100 - height/2)
var goalBottom = goalY + 100 + 292; // Bottom of goal (goalY + 100 + height/2)
if (ball.x >= goalLeft && ball.x <= goalRight && ball.y >= goalTop && ball.y <= goalBottom) {
// Goal scored! Ball touched inside goal area or side lines
ball.isMoving = false;
LK.getSound('goal').play();
LK.effects.flashScreen(0x00ff00, 500);
LK.setScore(LK.getScore() + 1);
// Check and update personal best
var currentScore = LK.getScore();
if (currentScore > personalBest) {
personalBest = currentScore;
storage.personalBest = personalBest;
personalBestText.setText('Best: ' + personalBest);
LK.effects.flashObject(personalBestText, 0xFFFF00, 1000);
}
currentRound++;
// Reset attempts for next round
attemptsLeft = 3;
LK.setTimeout(function () {
ball.reset();
resetGoalkeeper(); // Reset goalkeeper position
setupRound();
}, 1000);
return;
}
// Check defender collisions
for (var i = 0; i < defenders.length; i++) {
if (ball.intersects(defenders[i])) {
ball.isMoving = false;
LK.effects.flashObject(defenders[i], 0xff0000, 500);
ballMissed();
return;
}
}
// Goal structure collision removed to allow ball to pass through for scoring
}
// Function to reset goalkeeper to initial position
function resetGoalkeeper() {
tween(goalkeeper, {
x: goalkeeperInitialX,
y: goalkeeperInitialY
}, {
duration: 800,
easing: tween.easeOut
});
}
function ballMissed() {
attemptsLeft--;
attemptsText.setText('Attempts: ' + attemptsLeft);
if (attemptsLeft <= 0) {
LK.effects.flashScreen(0xff0000, 1000);
LK.setTimeout(function () {
LK.showGameOver(); // Show game over screen and reset game
}, 1500);
} else {
LK.setTimeout(function () {
ball.reset();
resetGoalkeeper(); // Reset goalkeeper position
}, 1000);
}
}
// Game controls are now defined as variables above
game.update = function () {
if (ball.isMoving) {
// Goalkeeper AI - move towards ball's predicted position
if (ball.y < goalY + 400) {
// Only react when ball is getting close to goal
var ballTargetX = ball.x + ball.velocityX * 15; // Predict where ball will be (increased prediction)
var keeperSpeed = 15; // Increased goalkeeper movement speed
var maxKeeperX = goalX + 350; // Right boundary for keeper movement
var minKeeperX = goalX - 350; // Left boundary for keeper movement
// Clamp keeper target position within goal area
ballTargetX = Math.max(minKeeperX, Math.min(maxKeeperX, ballTargetX));
// Move keeper towards predicted ball position with tween animation
var deltaX = ballTargetX - goalkeeper.x;
if (Math.abs(deltaX) > 5) {
// Use tween for smooth goalkeeper movement
tween(goalkeeper, {
x: ballTargetX
}, {
duration: 200,
easing: tween.easeOut
});
}
}
checkCollisions();
}
};
// Game instructions
var instructionsText = new Text2('Swipe to aim and shoot!\nUse < > buttons to adjust spin.\nSwipe direction sets aim, spin is manual.\nInside of the lines are counted as scores as well.', {
size: 75,
fill: 0xFFFFFF,
align: 'center'
});
instructionsText.anchor.set(0.5, 0.5);
instructionsText.x = 1024;
instructionsText.y = 1366;
game.addChild(instructionsText);
var startButton = new Text2('TAP TO START', {
size: 100,
fill: 0x00FF00
});
startButton.anchor.set(0.5, 0.5);
startButton.x = 1024;
startButton.y = 1600;
game.addChild(startButton);
var gameStarted = false;
// Store the actual game control functions
var originalDown = function originalDown(x, y, obj) {
if (!ball.isMoving && ball.y > 2000) {
isAiming = true;
dragStartX = x;
dragStartY = y;
swipePath = [{
x: x,
y: y,
time: Date.now()
}];
swipeStartTime = Date.now();
}
};
var originalMove = function originalMove(x, y, obj) {
if (isAiming && !ball.isMoving) {
dragCurrentX = x;
dragCurrentY = y;
// Track swipe path
var currentTime = Date.now();
swipePath.push({
x: x,
y: y,
time: currentTime
});
// Keep only recent points
if (swipePath.length > maxSwipePoints) {
swipePath.shift();
}
var deltaX = x - dragStartX;
var deltaY = y - dragStartY;
var distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
var power = fixedSpeed;
var angle = Math.atan2(-deltaY, deltaX);
powerText.x = ball.x;
powerText.y = ball.y - 100;
powerText.setText('');
// Show trajectory with current manual curve setting
createTrajectoryPreview(power, angle, selectedCurve);
}
};
var originalUp = function originalUp(x, y, obj) {
if (isAiming && !ball.isMoving) {
var deltaX = x - dragStartX;
var deltaY = y - dragStartY;
var distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
if (distance > 20) {
var power = fixedSpeed;
var angle = Math.atan2(-deltaY, deltaX);
// Use manually selected curve
var spin = selectedCurve;
ball.shoot(power, angle, spin);
}
isAiming = false;
powerText.setText('');
clearTrajectoryPreview();
// Clear swipe path
swipePath = [];
}
};
// Add click handlers to curve buttons
leftCurveButton.down = function (x, y, obj) {
if (gameStarted && !ball.isMoving) {
if (curveIndex > 0) {
curveIndex--;
updateCurveDisplay();
LK.effects.flashObject(leftCurveButton, 0x00ff00, 200);
}
}
};
rightCurveButton.down = function (x, y, obj) {
if (gameStarted && !ball.isMoving) {
if (curveIndex < curveValues.length - 1) {
curveIndex++;
updateCurveDisplay();
LK.effects.flashObject(rightCurveButton, 0x00ff00, 200);
}
}
};
// Override game controls until game starts
game.down = function (x, y, obj) {
if (!gameStarted) {
// Start the game
gameStarted = true;
instructionsText.destroy();
startButton.destroy();
// Restore original controls
game.down = originalDown;
game.move = originalMove;
game.up = originalUp;
// Initialize first round
setupRound();
// Call the original down handler for this event
originalDown(x, y, obj);
return;
}
};
game.move = function (x, y, obj) {
// Do nothing until game starts
};
game.up = function (x, y, obj) {
// Do nothing until game starts
}; ===================================================================
--- original.js
+++ change.js
@@ -354,17 +354,36 @@
// Ball collision detection
function checkCollisions() {
// Check goalkeeper collision first - this prevents scoring
if (ball.intersects(goalkeeper)) {
- // Goalkeeper save probability based on ball speed and position
+ // Enhanced goalkeeper save logic based on ball position relative to goal
+ var goalLeft = goalX - 400; // Left side of goal
+ var goalRight = goalX + 400; // Right side of goal
+ var goalWidth = 800; // Total goal width
+ var goalCenter = goalX; // Goal center X position
+ var distanceFromCenter = Math.abs(ball.x - goalCenter);
+ var distanceFromGoalEdge = Math.min(Math.abs(ball.x - goalLeft), Math.abs(ball.x - goalRight));
var ballSpeed = Math.sqrt(ball.velocityX * ball.velocityX + ball.velocityY * ball.velocityY);
- var distanceFromCenter = Math.abs(ball.x - goalkeeper.x);
- var saveChance = 0.7; // Base 70% save chance
- // Reduce save chance for faster balls
- if (ballSpeed > 25) saveChance -= 0.2;
- if (ballSpeed > 35) saveChance -= 0.2;
- // Reduce save chance if ball is far from keeper center
- if (distanceFromCenter > 100) saveChance -= 0.3;
+ // Calculate save chance based on position in goal
+ var saveChance = 0;
+ // Define middle zone (center 60% of goal) and corner zones (outer 20% each side)
+ var middleZoneWidth = goalWidth * 0.6; // 480 pixels from center
+ var cornerZoneWidth = goalWidth * 0.2; // 160 pixels each corner
+ if (distanceFromCenter <= middleZoneWidth / 2) {
+ // Ball is in the middle zone - high save chance
+ saveChance = 0.95; // 95% save chance in middle
+ } else if (distanceFromGoalEdge <= cornerZoneWidth) {
+ // Ball is in corner zone - very low save chance
+ saveChance = 0.1; // Only 10% save chance in corners
+ } else {
+ // Ball is in transition zone between middle and corner
+ saveChance = 0.4; // Moderate save chance in transition
+ }
+ // Adjust save chance based on ball speed (faster balls harder to save)
+ if (ballSpeed > 30) saveChance -= 0.2;
+ if (ballSpeed > 40) saveChance -= 0.2;
+ // Ensure save chance stays within reasonable bounds
+ saveChance = Math.max(0.05, Math.min(0.98, saveChance));
// Random save attempt
if (Math.random() < saveChance) {
// Successful save
ball.isMoving = false;