User prompt
took me to level 1
User prompt
Do this with this logic until level 100 and as it gets harder the obstacles will move horizontally or vertically.
User prompt
make restart button purple with different font
User prompt
obstacle difficulty increases proportionally to level
User prompt
add more levels
User prompt
go back to previous version
User prompt
Make 100 levels and change the ball and target positions in each level
User prompt
use background music in assets and make it infinite
User prompt
use bounce sound in assets for every bounce
User prompt
add restart button
User prompt
make ball more smooth and fast
Code edit (1 edits merged)
Please save this source code
User prompt
Bounce Quest: Precision Puzzle
Initial prompt
Overview This is a 2D physics-based puzzle game designed for mobile platforms. The core mechanic revolves around a limited-bounce ball that the player must launch toward a reward zone, navigating through and around obstacles while minimizing the number of bounces. Core Gameplay Mechanics Player Input: The player drags and releases the ball to determine its launch direction and power. This simulates a slingshot-style interaction common in mobile games. Ball Behavior: The ball bounces off walls and obstacles, obeying basic 2D reflection physics. Each bounce reduces the allowed bounce count. Once the bounce limit is reached, the ball stops and the level is failed unless the goal has already been reached. Bounce Limit: The ball has a maximum number of allowed bounces, which decreases as levels progress to increase difficulty. Bounce limits are enforced per level and reset with each new attempt. Goal Zones: Each level contains a square-shaped “reward zone”. The player must get the ball to enter this zone to successfully complete the level. These reward zones are surrounded by obstacles, making the path to them more complex. Obstacles: Static, rectangular blocks that deflect the ball upon collision. Strategically placed to require careful planning and skillful launching. Level Design: Increasing complexity across levels: more obstacles, tighter angles, fewer allowed bounces. Emphasis on puzzle-solving and precision. Objectives The main objective is to reach the reward zone using the minimum number of bounces. Players are rewarded for efficiency: fewer bounces, higher scores. Platform and Controls Target Platform: Mobile (iOS, Android) Controls: Touch and drag input to control the ball's launch direction and power. UI Elements: Bounce counter Reset button Level progression Optional hint system Visual & Audio Design (Optional for AI Planning) Minimalistic and clean graphics for clarity. Use of color-coded zones: e.g., red for obstacles, green for reward zone. Sound effects for bounces, launches, and successful goals. Summary for AI Implementation Implement a 2D physics simulation using a game engine such as Unity or Godot. Model the ball with collision logic and bounce counting. Design levels with obstacle layouts and target zones. Create UI and input handling for mobile touch gestures. Add progression logic: increasing difficulty, scoring system, bounce constraints.
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1", { level: 1 }); /**** * Classes ****/ // Ball class var Ball = Container.expand(function () { var self = Container.call(this); var ballGfx = self.attachAsset('ball', { anchorX: 0.5, anchorY: 0.5 }); self.radius = ballGfx.width / 2; self.vx = 0; self.vy = 0; self.isMoving = false; self.bouncesLeft = 0; self.update = function () { if (!self.isMoving) return; // Move self.x += self.vx; self.y += self.vy; // Reduced friction for smoother, faster movement self.vx *= 0.998; self.vy *= 0.998; // Gravity (none for now, can add later) // Wall collision var bounced = false; // Left wall if (self.x - self.radius < 0) { self.x = self.radius; self.vx = -self.vx; bounced = true; } // Right wall if (self.x + self.radius > 2048) { self.x = 2048 - self.radius; self.vx = -self.vx; bounced = true; } // Top wall if (self.y - self.radius < 0) { self.y = self.radius; self.vy = -self.vy; bounced = true; } // Bottom wall if (self.y + self.radius > 2732) { self.y = 2732 - self.radius; self.vy = -self.vy; bounced = true; } // Obstacle collision for (var i = 0; i < obstacles.length; i++) { var obs = obstacles[i]; if (circleRectIntersect(self, obs)) { // Find the closest point on the rectangle to the ball center var closestX = clamp(self.x, obs.x, obs.x + obs.width); var closestY = clamp(self.y, obs.y, obs.y + obs.height); // Calculate the distance between the ball's center and this closest point var dx = self.x - closestX; var dy = self.y - closestY; // If the distance is less than the radius, we have a collision var dist = Math.sqrt(dx * dx + dy * dy); if (dist < self.radius) { // Reflect velocity // Determine which side was hit var overlapX = Math.min(Math.abs(self.x - obs.x), Math.abs(self.x - (obs.x + obs.width))); var overlapY = Math.min(Math.abs(self.y - obs.y), Math.abs(self.y - (obs.y + obs.height))); if (overlapX < overlapY) { self.vx = -self.vx; // Nudge out if (self.x < obs.x) self.x = obs.x - self.radius;else self.x = obs.x + obs.width + self.radius; } else { self.vy = -self.vy; if (self.y < obs.y) self.y = obs.y - self.radius;else self.y = obs.y + obs.height + self.radius; } bounced = true; } } } if (bounced) { self.bouncesLeft--; updateBounceCounter(); LK.effects.flashObject(self, 0x00b4d8, 200); // Play bounce sound LK.getSound('bounce').play(); if (self.bouncesLeft < 0) { // Out of bounces endLevel(false); } } // Reward zone check if (!self.hasWon && self.intersects(rewardZone)) { self.hasWon = true; endLevel(true); } }; return self; }); // Obstacle class var Obstacle = Container.expand(function () { var self = Container.call(this); var obsGfx = self.attachAsset('obstacle', { anchorX: 0, anchorY: 0 }); self.width = obsGfx.width; self.height = obsGfx.height; // Movement properties self.moveType = null; // 'horizontal' or 'vertical' self.moveRange = 0; self.moveSpeed = 0; self.baseX = 0; self.baseY = 0; self.movePhase = 0; self.update = function () { if (!self.moveType) return; self.movePhase += self.moveSpeed; if (self.moveType === 'horizontal') { self.x = self.baseX + Math.sin(self.movePhase) * self.moveRange; } else if (self.moveType === 'vertical') { self.y = self.baseY + Math.sin(self.movePhase) * self.moveRange; } }; return self; }); // Trajectory preview dot var PreviewDot = Container.expand(function () { var self = Container.call(this); self.attachAsset('previewDot', { anchorX: 0.5, anchorY: 0.5, alpha: 0.5 }); return self; }); // Reward zone class var RewardZone = Container.expand(function () { var self = Container.call(this); var rewardGfx = self.attachAsset('reward', { anchorX: 0.5, anchorY: 0.5 }); self.width = rewardGfx.width; self.height = rewardGfx.height; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x000000 }); /**** * Game Code ****/ // --- Utility functions --- // Ball (player) // Reward zone // Obstacle // Trajectory preview function clamp(val, min, max) { if (val < min) return min; if (val > max) return max; return val; } function circleRectIntersect(circle, rect) { // rect: x, y, width, height (top-left) var cx = circle.x, cy = circle.y, r = circle.radius; var rx = rect.x, ry = rect.y, rw = rect.width, rh = rect.height; var closestX = clamp(cx, rx, rx + rw); var closestY = clamp(cy, ry, ry + rh); var dx = cx - closestX; var dy = cy - closestY; return dx * dx + dy * dy < r * r; } // --- Level Data --- var levels = [{ ball: { x: 300, y: 2200, bounces: 5 }, reward: { x: 1700, y: 500 }, obstacles: [{ x: 800, y: 1200, width: 400, height: 60 }, { x: 1200, y: 1800, width: 60, height: 400 }] }, { ball: { x: 400, y: 2100, bounces: 4 }, reward: { x: 1800, y: 400 }, obstacles: [{ x: 900, y: 1000, width: 400, height: 60 }, { x: 1300, y: 1700, width: 60, height: 400 }] }, { ball: { x: 350, y: 2300, bounces: 6 }, reward: { x: 1600, y: 600 }, obstacles: [{ x: 1000, y: 1300, width: 400, height: 60 }, { x: 1100, y: 1600, width: 60, height: 400 }] }, { ball: { x: 500, y: 2000, bounces: 5 }, reward: { x: 1700, y: 700 }, obstacles: [{ x: 700, y: 1100, width: 400, height: 60 }, { x: 1400, y: 1500, width: 60, height: 400 }] }, { ball: { x: 600, y: 2200, bounces: 7 }, reward: { x: 1500, y: 800 }, obstacles: [{ x: 1200, y: 1200, width: 400, height: 60 }, { x: 900, y: 1700, width: 60, height: 400 }] }, { ball: { x: 300, y: 2100, bounces: 4 }, reward: { x: 1800, y: 900 }, obstacles: [{ x: 1000, y: 1400, width: 400, height: 60 }, { x: 1200, y: 1600, width: 60, height: 400 }] }, { ball: { x: 400, y: 2200, bounces: 6 }, reward: { x: 1700, y: 1000 }, obstacles: [{ x: 1100, y: 1200, width: 400, height: 60 }, { x: 1300, y: 1800, width: 60, height: 400 }] }, { ball: { x: 350, y: 2000, bounces: 5 }, reward: { x: 1600, y: 1100 }, obstacles: [{ x: 900, y: 1300, width: 400, height: 60 }, { x: 1400, y: 1700, width: 60, height: 400 }] }, { ball: { x: 500, y: 2300, bounces: 7 }, reward: { x: 1500, y: 1200 }, obstacles: [{ x: 1200, y: 1100, width: 400, height: 60 }, { x: 1000, y: 1600, width: 60, height: 400 }] }, { ball: { x: 600, y: 2100, bounces: 6 }, reward: { x: 1400, y: 1300 }, obstacles: [{ x: 800, y: 1200, width: 400, height: 60 }, { x: 1200, y: 1800, width: 60, height: 400 }] }]; // --- Game State --- var currentLevel = 1; storage.level = 1; if (currentLevel < 1) currentLevel = 1; if (currentLevel > levels.length) currentLevel = levels.length; var ball = null; var rewardZone = null; var obstacles = []; var previewDots = []; var isDragging = false; var dragStart = { x: 0, y: 0 }; var dragEnd = { x: 0, y: 0 }; var bounceCounterTxt = null; var levelTxt = null; var hintTxt = null; var canLaunch = true; // --- UI Setup --- bounceCounterTxt = new Text2('', { size: 90, fill: 0x22223B }); bounceCounterTxt.anchor.set(0.5, 0); LK.gui.top.addChild(bounceCounterTxt); levelTxt = new Text2('', { size: 70, fill: 0x3A86FF }); levelTxt.anchor.set(0.5, 0); LK.gui.top.addChild(levelTxt); hintTxt = new Text2('', { size: 60, fill: 0xADB5BD }); hintTxt.anchor.set(0.5, 0); LK.gui.bottom.addChild(hintTxt); // --- Restart Button --- var restartBtn = new Text2('Restart', { size: 70, fill: 0x8000ff, // purple font: "'Comic Sans MS', 'Comic Sans', cursive" }); restartBtn.anchor.set(0.5, 0.5); // Place at top right, but not in the top left 100x100 reserved area restartBtn.x = -160; // offset from right edge restartBtn.y = 100; // below top edge, outside reserved area restartBtn.interactive = true; restartBtn.buttonMode = true; restartBtn.down = function (x, y, obj) { loadLevel(currentLevel); }; LK.gui.topRight.addChild(restartBtn); // --- Level Loader --- function loadLevel(n) { // Clean up previous if (ball) { ball.destroy(); ball = null; } if (rewardZone) { rewardZone.destroy(); rewardZone = null; } for (var i = 0; i < obstacles.length; i++) obstacles[i].destroy(); obstacles = []; for (var i = 0; i < previewDots.length; i++) previewDots[i].destroy(); previewDots = []; isDragging = false; canLaunch = true; hintTxt.setText(''); // Clamp level if (n < 1) n = 1; if (n > levels.length) n = levels.length; currentLevel = n; storage.level = n; var lvl = levels[n - 1]; // Ball ball = new Ball(); ball.x = lvl.ball.x; ball.y = lvl.ball.y; ball.vx = 0; ball.vy = 0; ball.isMoving = false; ball.bouncesLeft = lvl.ball.bounces; ball.hasWon = false; game.addChild(ball); // Reward rewardZone = new RewardZone(); rewardZone.x = lvl.reward.x; rewardZone.y = lvl.reward.y; game.addChild(rewardZone); // Obstacles // Difficulty scaling: increase size and number of obstacles with level var difficultyScale = 1 + (currentLevel - 1) * 0.08; // 8% harder per level var extraObstacles = Math.floor((currentLevel - 1) / 3); // Add 1 extra every 3 levels // Place original obstacles, scaled for (var i = 0; i < lvl.obstacles.length; i++) { var o = lvl.obstacles[i]; var obs = new Obstacle(); obs.x = o.x; obs.y = o.y; obs.width = o.width * difficultyScale; obs.height = o.height * difficultyScale; obs.children[0].width = obs.width; obs.children[0].height = obs.height; // Add movement for higher levels if (currentLevel > 5) { obs.baseX = obs.x; obs.baseY = obs.y; // Alternate movement type for variety if (i % 2 === 0) { obs.moveType = 'horizontal'; obs.moveRange = 60 + currentLevel * 3; // Range increases with level obs.moveSpeed = 0.012 + currentLevel * 0.0007; // Speed increases with level } else { obs.moveType = 'vertical'; obs.moveRange = 60 + currentLevel * 3; obs.moveSpeed = 0.012 + currentLevel * 0.0007; } obs.movePhase = Math.random() * Math.PI * 2; } obstacles.push(obs); game.addChild(obs); } // Add extra obstacles for higher levels for (var j = 0; j < extraObstacles; j++) { // Place extra obstacles in a pattern, e.g. diagonal, and randomize a bit var obs = new Obstacle(); var baseX = 400 + j * 200 + currentLevel * 13 % 300; var baseY = 900 + j * 300 + currentLevel * 17 % 400; obs.x = Math.min(1800, baseX + currentLevel * 23 % 100); obs.y = Math.min(2200, baseY + currentLevel * 31 % 100); obs.width = 200 * difficultyScale; obs.height = 60 * difficultyScale; obs.children[0].width = obs.width; obs.children[0].height = obs.height; // Add movement for extra obstacles as well if (currentLevel > 5) { obs.baseX = obs.x; obs.baseY = obs.y; if (j % 2 === 0) { obs.moveType = 'horizontal'; obs.moveRange = 80 + currentLevel * 4; obs.moveSpeed = 0.014 + currentLevel * 0.0008; } else { obs.moveType = 'vertical'; obs.moveRange = 80 + currentLevel * 4; obs.moveSpeed = 0.014 + currentLevel * 0.0008; } obs.movePhase = Math.random() * Math.PI * 2; } obstacles.push(obs); game.addChild(obs); } updateBounceCounter(); levelTxt.setText('Level ' + n); // Hint if (n === 1) { hintTxt.setText('Drag and release to launch the ball!'); } else if (n === 2) { hintTxt.setText('Bounce off walls and avoid obstacles.'); } else if (n === 3) { hintTxt.setText('Plan your shot to use fewer bounces.'); } else { hintTxt.setText(''); } } // --- UI Update --- function updateBounceCounter() { if (!ball) return; bounceCounterTxt.setText('Bounces: ' + Math.max(0, ball.bouncesLeft)); } // --- End Level --- function endLevel(won) { canLaunch = false; if (won) { LK.effects.flashScreen(0x83de44, 600); LK.setScore(currentLevel); if (currentLevel >= levels.length) { LK.showYouWin(); } else { // Next level after short delay LK.setTimeout(function () { loadLevel(currentLevel + 1); }, 1200); } } else { LK.effects.flashScreen(0xff595e, 800); LK.setTimeout(function () { loadLevel(currentLevel); }, 1200); } } // --- Trajectory Preview --- function showTrajectoryPreview(fromX, fromY, toX, toY) { // Remove old dots for (var i = 0; i < previewDots.length; i++) previewDots[i].destroy(); previewDots = []; // Calculate initial velocity var dx = fromX - toX; var dy = fromY - toY; var power = Math.sqrt(dx * dx + dy * dy); if (power < 30) return; var maxPower = 900; if (power > maxPower) power = maxPower; var angle = Math.atan2(dy, dx); var speed = 0.055 * power; // Match ball launch speed for accurate preview var vx = Math.cos(angle) * speed; var vy = Math.sin(angle) * speed; // Simulate var px = fromX, py = fromY; var pvx = vx, pvy = vy; var bounces = ball ? ball.bouncesLeft : 3; var simRadius = ball ? ball.radius : 50; var simSteps = 0; var maxDots = 18; while (bounces >= 0 && simSteps < maxDots) { // Move px += pvx * 8; py += pvy * 8; // Friction pvx *= 0.995; pvy *= 0.995; // Wall collision var bounced = false; if (px - simRadius < 0) { px = simRadius; pvx = -pvx; bounced = true; } if (px + simRadius > 2048) { px = 2048 - simRadius; pvx = -pvx; bounced = true; } if (py - simRadius < 0) { py = simRadius; pvy = -pvy; bounced = true; } if (py + simRadius > 2732) { py = 2732 - simRadius; pvy = -pvy; bounced = true; } // Obstacle collision for (var i = 0; i < obstacles.length; i++) { var obs = obstacles[i]; if (circleRectIntersect({ x: px, y: py, radius: simRadius }, obs)) { // Find the closest point on the rectangle to the ball center var closestX = clamp(px, obs.x, obs.x + obs.width); var closestY = clamp(py, obs.y, obs.y + obs.height); var dx2 = px - closestX; var dy2 = py - closestY; var dist = Math.sqrt(dx2 * dx2 + dy2 * dy2); if (dist < simRadius) { var overlapX = Math.min(Math.abs(px - obs.x), Math.abs(px - (obs.x + obs.width))); var overlapY = Math.min(Math.abs(py - obs.y), Math.abs(py - (obs.y + obs.height))); if (overlapX < overlapY) { pvx = -pvx; if (px < obs.x) px = obs.x - simRadius;else px = obs.x + obs.width + simRadius; } else { pvy = -pvy; if (py < obs.y) py = obs.y - simRadius;else py = obs.y + obs.height + simRadius; } bounced = true; } } } if (bounced) bounces--; // Place dot var dot = new PreviewDot(); dot.x = px; dot.y = py; game.addChild(dot); previewDots.push(dot); simSteps++; } } // --- Input Handling --- game.down = function (x, y, obj) { if (!canLaunch || !ball || ball.isMoving) return; // Only allow drag if touch is on ball var dx = x - ball.x; var dy = y - ball.y; if (dx * dx + dy * dy > ball.radius * ball.radius * 1.2) return; isDragging = true; dragStart.x = ball.x; dragStart.y = ball.y; dragEnd.x = x; dragEnd.y = y; showTrajectoryPreview(dragStart.x, dragStart.y, dragEnd.x, dragEnd.y); }; game.move = function (x, y, obj) { if (!isDragging) return; dragEnd.x = x; dragEnd.y = y; showTrajectoryPreview(dragStart.x, dragStart.y, dragEnd.x, dragEnd.y); }; game.up = function (x, y, obj) { if (!isDragging) return; isDragging = false; // Remove preview for (var i = 0; i < previewDots.length; i++) previewDots[i].destroy(); previewDots = []; // Launch var dx = dragStart.x - dragEnd.x; var dy = dragStart.y - dragEnd.y; var power = Math.sqrt(dx * dx + dy * dy); if (power < 30) return; // Too short var maxPower = 900; if (power > maxPower) power = maxPower; var angle = Math.atan2(dy, dx); var speed = 0.055 * power; // Increased speed for more dynamic launch ball.vx = Math.cos(angle) * speed; ball.vy = Math.sin(angle) * speed; ball.isMoving = true; canLaunch = false; }; // --- Main Update Loop --- game.update = function () { if (ball) ball.update(); for (var i = 0; i < obstacles.length; i++) { if (typeof obstacles[i].update === 'function') obstacles[i].update(); } }; // --- Start Game --- loadLevel(currentLevel); // Play background music infinitely LK.playMusic('Bg');
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1", {
level: 1
});
/****
* Classes
****/
// Ball class
var Ball = Container.expand(function () {
var self = Container.call(this);
var ballGfx = self.attachAsset('ball', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = ballGfx.width / 2;
self.vx = 0;
self.vy = 0;
self.isMoving = false;
self.bouncesLeft = 0;
self.update = function () {
if (!self.isMoving) return;
// Move
self.x += self.vx;
self.y += self.vy;
// Reduced friction for smoother, faster movement
self.vx *= 0.998;
self.vy *= 0.998;
// Gravity (none for now, can add later)
// Wall collision
var bounced = false;
// Left wall
if (self.x - self.radius < 0) {
self.x = self.radius;
self.vx = -self.vx;
bounced = true;
}
// Right wall
if (self.x + self.radius > 2048) {
self.x = 2048 - self.radius;
self.vx = -self.vx;
bounced = true;
}
// Top wall
if (self.y - self.radius < 0) {
self.y = self.radius;
self.vy = -self.vy;
bounced = true;
}
// Bottom wall
if (self.y + self.radius > 2732) {
self.y = 2732 - self.radius;
self.vy = -self.vy;
bounced = true;
}
// Obstacle collision
for (var i = 0; i < obstacles.length; i++) {
var obs = obstacles[i];
if (circleRectIntersect(self, obs)) {
// Find the closest point on the rectangle to the ball center
var closestX = clamp(self.x, obs.x, obs.x + obs.width);
var closestY = clamp(self.y, obs.y, obs.y + obs.height);
// Calculate the distance between the ball's center and this closest point
var dx = self.x - closestX;
var dy = self.y - closestY;
// If the distance is less than the radius, we have a collision
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < self.radius) {
// Reflect velocity
// Determine which side was hit
var overlapX = Math.min(Math.abs(self.x - obs.x), Math.abs(self.x - (obs.x + obs.width)));
var overlapY = Math.min(Math.abs(self.y - obs.y), Math.abs(self.y - (obs.y + obs.height)));
if (overlapX < overlapY) {
self.vx = -self.vx;
// Nudge out
if (self.x < obs.x) self.x = obs.x - self.radius;else self.x = obs.x + obs.width + self.radius;
} else {
self.vy = -self.vy;
if (self.y < obs.y) self.y = obs.y - self.radius;else self.y = obs.y + obs.height + self.radius;
}
bounced = true;
}
}
}
if (bounced) {
self.bouncesLeft--;
updateBounceCounter();
LK.effects.flashObject(self, 0x00b4d8, 200);
// Play bounce sound
LK.getSound('bounce').play();
if (self.bouncesLeft < 0) {
// Out of bounces
endLevel(false);
}
}
// Reward zone check
if (!self.hasWon && self.intersects(rewardZone)) {
self.hasWon = true;
endLevel(true);
}
};
return self;
});
// Obstacle class
var Obstacle = Container.expand(function () {
var self = Container.call(this);
var obsGfx = self.attachAsset('obstacle', {
anchorX: 0,
anchorY: 0
});
self.width = obsGfx.width;
self.height = obsGfx.height;
// Movement properties
self.moveType = null; // 'horizontal' or 'vertical'
self.moveRange = 0;
self.moveSpeed = 0;
self.baseX = 0;
self.baseY = 0;
self.movePhase = 0;
self.update = function () {
if (!self.moveType) return;
self.movePhase += self.moveSpeed;
if (self.moveType === 'horizontal') {
self.x = self.baseX + Math.sin(self.movePhase) * self.moveRange;
} else if (self.moveType === 'vertical') {
self.y = self.baseY + Math.sin(self.movePhase) * self.moveRange;
}
};
return self;
});
// Trajectory preview dot
var PreviewDot = Container.expand(function () {
var self = Container.call(this);
self.attachAsset('previewDot', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.5
});
return self;
});
// Reward zone class
var RewardZone = Container.expand(function () {
var self = Container.call(this);
var rewardGfx = self.attachAsset('reward', {
anchorX: 0.5,
anchorY: 0.5
});
self.width = rewardGfx.width;
self.height = rewardGfx.height;
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x000000
});
/****
* Game Code
****/
// --- Utility functions ---
// Ball (player)
// Reward zone
// Obstacle
// Trajectory preview
function clamp(val, min, max) {
if (val < min) return min;
if (val > max) return max;
return val;
}
function circleRectIntersect(circle, rect) {
// rect: x, y, width, height (top-left)
var cx = circle.x,
cy = circle.y,
r = circle.radius;
var rx = rect.x,
ry = rect.y,
rw = rect.width,
rh = rect.height;
var closestX = clamp(cx, rx, rx + rw);
var closestY = clamp(cy, ry, ry + rh);
var dx = cx - closestX;
var dy = cy - closestY;
return dx * dx + dy * dy < r * r;
}
// --- Level Data ---
var levels = [{
ball: {
x: 300,
y: 2200,
bounces: 5
},
reward: {
x: 1700,
y: 500
},
obstacles: [{
x: 800,
y: 1200,
width: 400,
height: 60
}, {
x: 1200,
y: 1800,
width: 60,
height: 400
}]
}, {
ball: {
x: 400,
y: 2100,
bounces: 4
},
reward: {
x: 1800,
y: 400
},
obstacles: [{
x: 900,
y: 1000,
width: 400,
height: 60
}, {
x: 1300,
y: 1700,
width: 60,
height: 400
}]
}, {
ball: {
x: 350,
y: 2300,
bounces: 6
},
reward: {
x: 1600,
y: 600
},
obstacles: [{
x: 1000,
y: 1300,
width: 400,
height: 60
}, {
x: 1100,
y: 1600,
width: 60,
height: 400
}]
}, {
ball: {
x: 500,
y: 2000,
bounces: 5
},
reward: {
x: 1700,
y: 700
},
obstacles: [{
x: 700,
y: 1100,
width: 400,
height: 60
}, {
x: 1400,
y: 1500,
width: 60,
height: 400
}]
}, {
ball: {
x: 600,
y: 2200,
bounces: 7
},
reward: {
x: 1500,
y: 800
},
obstacles: [{
x: 1200,
y: 1200,
width: 400,
height: 60
}, {
x: 900,
y: 1700,
width: 60,
height: 400
}]
}, {
ball: {
x: 300,
y: 2100,
bounces: 4
},
reward: {
x: 1800,
y: 900
},
obstacles: [{
x: 1000,
y: 1400,
width: 400,
height: 60
}, {
x: 1200,
y: 1600,
width: 60,
height: 400
}]
}, {
ball: {
x: 400,
y: 2200,
bounces: 6
},
reward: {
x: 1700,
y: 1000
},
obstacles: [{
x: 1100,
y: 1200,
width: 400,
height: 60
}, {
x: 1300,
y: 1800,
width: 60,
height: 400
}]
}, {
ball: {
x: 350,
y: 2000,
bounces: 5
},
reward: {
x: 1600,
y: 1100
},
obstacles: [{
x: 900,
y: 1300,
width: 400,
height: 60
}, {
x: 1400,
y: 1700,
width: 60,
height: 400
}]
}, {
ball: {
x: 500,
y: 2300,
bounces: 7
},
reward: {
x: 1500,
y: 1200
},
obstacles: [{
x: 1200,
y: 1100,
width: 400,
height: 60
}, {
x: 1000,
y: 1600,
width: 60,
height: 400
}]
}, {
ball: {
x: 600,
y: 2100,
bounces: 6
},
reward: {
x: 1400,
y: 1300
},
obstacles: [{
x: 800,
y: 1200,
width: 400,
height: 60
}, {
x: 1200,
y: 1800,
width: 60,
height: 400
}]
}];
// --- Game State ---
var currentLevel = 1;
storage.level = 1;
if (currentLevel < 1) currentLevel = 1;
if (currentLevel > levels.length) currentLevel = levels.length;
var ball = null;
var rewardZone = null;
var obstacles = [];
var previewDots = [];
var isDragging = false;
var dragStart = {
x: 0,
y: 0
};
var dragEnd = {
x: 0,
y: 0
};
var bounceCounterTxt = null;
var levelTxt = null;
var hintTxt = null;
var canLaunch = true;
// --- UI Setup ---
bounceCounterTxt = new Text2('', {
size: 90,
fill: 0x22223B
});
bounceCounterTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(bounceCounterTxt);
levelTxt = new Text2('', {
size: 70,
fill: 0x3A86FF
});
levelTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(levelTxt);
hintTxt = new Text2('', {
size: 60,
fill: 0xADB5BD
});
hintTxt.anchor.set(0.5, 0);
LK.gui.bottom.addChild(hintTxt);
// --- Restart Button ---
var restartBtn = new Text2('Restart', {
size: 70,
fill: 0x8000ff,
// purple
font: "'Comic Sans MS', 'Comic Sans', cursive"
});
restartBtn.anchor.set(0.5, 0.5);
// Place at top right, but not in the top left 100x100 reserved area
restartBtn.x = -160; // offset from right edge
restartBtn.y = 100; // below top edge, outside reserved area
restartBtn.interactive = true;
restartBtn.buttonMode = true;
restartBtn.down = function (x, y, obj) {
loadLevel(currentLevel);
};
LK.gui.topRight.addChild(restartBtn);
// --- Level Loader ---
function loadLevel(n) {
// Clean up previous
if (ball) {
ball.destroy();
ball = null;
}
if (rewardZone) {
rewardZone.destroy();
rewardZone = null;
}
for (var i = 0; i < obstacles.length; i++) obstacles[i].destroy();
obstacles = [];
for (var i = 0; i < previewDots.length; i++) previewDots[i].destroy();
previewDots = [];
isDragging = false;
canLaunch = true;
hintTxt.setText('');
// Clamp level
if (n < 1) n = 1;
if (n > levels.length) n = levels.length;
currentLevel = n;
storage.level = n;
var lvl = levels[n - 1];
// Ball
ball = new Ball();
ball.x = lvl.ball.x;
ball.y = lvl.ball.y;
ball.vx = 0;
ball.vy = 0;
ball.isMoving = false;
ball.bouncesLeft = lvl.ball.bounces;
ball.hasWon = false;
game.addChild(ball);
// Reward
rewardZone = new RewardZone();
rewardZone.x = lvl.reward.x;
rewardZone.y = lvl.reward.y;
game.addChild(rewardZone);
// Obstacles
// Difficulty scaling: increase size and number of obstacles with level
var difficultyScale = 1 + (currentLevel - 1) * 0.08; // 8% harder per level
var extraObstacles = Math.floor((currentLevel - 1) / 3); // Add 1 extra every 3 levels
// Place original obstacles, scaled
for (var i = 0; i < lvl.obstacles.length; i++) {
var o = lvl.obstacles[i];
var obs = new Obstacle();
obs.x = o.x;
obs.y = o.y;
obs.width = o.width * difficultyScale;
obs.height = o.height * difficultyScale;
obs.children[0].width = obs.width;
obs.children[0].height = obs.height;
// Add movement for higher levels
if (currentLevel > 5) {
obs.baseX = obs.x;
obs.baseY = obs.y;
// Alternate movement type for variety
if (i % 2 === 0) {
obs.moveType = 'horizontal';
obs.moveRange = 60 + currentLevel * 3; // Range increases with level
obs.moveSpeed = 0.012 + currentLevel * 0.0007; // Speed increases with level
} else {
obs.moveType = 'vertical';
obs.moveRange = 60 + currentLevel * 3;
obs.moveSpeed = 0.012 + currentLevel * 0.0007;
}
obs.movePhase = Math.random() * Math.PI * 2;
}
obstacles.push(obs);
game.addChild(obs);
}
// Add extra obstacles for higher levels
for (var j = 0; j < extraObstacles; j++) {
// Place extra obstacles in a pattern, e.g. diagonal, and randomize a bit
var obs = new Obstacle();
var baseX = 400 + j * 200 + currentLevel * 13 % 300;
var baseY = 900 + j * 300 + currentLevel * 17 % 400;
obs.x = Math.min(1800, baseX + currentLevel * 23 % 100);
obs.y = Math.min(2200, baseY + currentLevel * 31 % 100);
obs.width = 200 * difficultyScale;
obs.height = 60 * difficultyScale;
obs.children[0].width = obs.width;
obs.children[0].height = obs.height;
// Add movement for extra obstacles as well
if (currentLevel > 5) {
obs.baseX = obs.x;
obs.baseY = obs.y;
if (j % 2 === 0) {
obs.moveType = 'horizontal';
obs.moveRange = 80 + currentLevel * 4;
obs.moveSpeed = 0.014 + currentLevel * 0.0008;
} else {
obs.moveType = 'vertical';
obs.moveRange = 80 + currentLevel * 4;
obs.moveSpeed = 0.014 + currentLevel * 0.0008;
}
obs.movePhase = Math.random() * Math.PI * 2;
}
obstacles.push(obs);
game.addChild(obs);
}
updateBounceCounter();
levelTxt.setText('Level ' + n);
// Hint
if (n === 1) {
hintTxt.setText('Drag and release to launch the ball!');
} else if (n === 2) {
hintTxt.setText('Bounce off walls and avoid obstacles.');
} else if (n === 3) {
hintTxt.setText('Plan your shot to use fewer bounces.');
} else {
hintTxt.setText('');
}
}
// --- UI Update ---
function updateBounceCounter() {
if (!ball) return;
bounceCounterTxt.setText('Bounces: ' + Math.max(0, ball.bouncesLeft));
}
// --- End Level ---
function endLevel(won) {
canLaunch = false;
if (won) {
LK.effects.flashScreen(0x83de44, 600);
LK.setScore(currentLevel);
if (currentLevel >= levels.length) {
LK.showYouWin();
} else {
// Next level after short delay
LK.setTimeout(function () {
loadLevel(currentLevel + 1);
}, 1200);
}
} else {
LK.effects.flashScreen(0xff595e, 800);
LK.setTimeout(function () {
loadLevel(currentLevel);
}, 1200);
}
}
// --- Trajectory Preview ---
function showTrajectoryPreview(fromX, fromY, toX, toY) {
// Remove old dots
for (var i = 0; i < previewDots.length; i++) previewDots[i].destroy();
previewDots = [];
// Calculate initial velocity
var dx = fromX - toX;
var dy = fromY - toY;
var power = Math.sqrt(dx * dx + dy * dy);
if (power < 30) return;
var maxPower = 900;
if (power > maxPower) power = maxPower;
var angle = Math.atan2(dy, dx);
var speed = 0.055 * power; // Match ball launch speed for accurate preview
var vx = Math.cos(angle) * speed;
var vy = Math.sin(angle) * speed;
// Simulate
var px = fromX,
py = fromY;
var pvx = vx,
pvy = vy;
var bounces = ball ? ball.bouncesLeft : 3;
var simRadius = ball ? ball.radius : 50;
var simSteps = 0;
var maxDots = 18;
while (bounces >= 0 && simSteps < maxDots) {
// Move
px += pvx * 8;
py += pvy * 8;
// Friction
pvx *= 0.995;
pvy *= 0.995;
// Wall collision
var bounced = false;
if (px - simRadius < 0) {
px = simRadius;
pvx = -pvx;
bounced = true;
}
if (px + simRadius > 2048) {
px = 2048 - simRadius;
pvx = -pvx;
bounced = true;
}
if (py - simRadius < 0) {
py = simRadius;
pvy = -pvy;
bounced = true;
}
if (py + simRadius > 2732) {
py = 2732 - simRadius;
pvy = -pvy;
bounced = true;
}
// Obstacle collision
for (var i = 0; i < obstacles.length; i++) {
var obs = obstacles[i];
if (circleRectIntersect({
x: px,
y: py,
radius: simRadius
}, obs)) {
// Find the closest point on the rectangle to the ball center
var closestX = clamp(px, obs.x, obs.x + obs.width);
var closestY = clamp(py, obs.y, obs.y + obs.height);
var dx2 = px - closestX;
var dy2 = py - closestY;
var dist = Math.sqrt(dx2 * dx2 + dy2 * dy2);
if (dist < simRadius) {
var overlapX = Math.min(Math.abs(px - obs.x), Math.abs(px - (obs.x + obs.width)));
var overlapY = Math.min(Math.abs(py - obs.y), Math.abs(py - (obs.y + obs.height)));
if (overlapX < overlapY) {
pvx = -pvx;
if (px < obs.x) px = obs.x - simRadius;else px = obs.x + obs.width + simRadius;
} else {
pvy = -pvy;
if (py < obs.y) py = obs.y - simRadius;else py = obs.y + obs.height + simRadius;
}
bounced = true;
}
}
}
if (bounced) bounces--;
// Place dot
var dot = new PreviewDot();
dot.x = px;
dot.y = py;
game.addChild(dot);
previewDots.push(dot);
simSteps++;
}
}
// --- Input Handling ---
game.down = function (x, y, obj) {
if (!canLaunch || !ball || ball.isMoving) return;
// Only allow drag if touch is on ball
var dx = x - ball.x;
var dy = y - ball.y;
if (dx * dx + dy * dy > ball.radius * ball.radius * 1.2) return;
isDragging = true;
dragStart.x = ball.x;
dragStart.y = ball.y;
dragEnd.x = x;
dragEnd.y = y;
showTrajectoryPreview(dragStart.x, dragStart.y, dragEnd.x, dragEnd.y);
};
game.move = function (x, y, obj) {
if (!isDragging) return;
dragEnd.x = x;
dragEnd.y = y;
showTrajectoryPreview(dragStart.x, dragStart.y, dragEnd.x, dragEnd.y);
};
game.up = function (x, y, obj) {
if (!isDragging) return;
isDragging = false;
// Remove preview
for (var i = 0; i < previewDots.length; i++) previewDots[i].destroy();
previewDots = [];
// Launch
var dx = dragStart.x - dragEnd.x;
var dy = dragStart.y - dragEnd.y;
var power = Math.sqrt(dx * dx + dy * dy);
if (power < 30) return; // Too short
var maxPower = 900;
if (power > maxPower) power = maxPower;
var angle = Math.atan2(dy, dx);
var speed = 0.055 * power; // Increased speed for more dynamic launch
ball.vx = Math.cos(angle) * speed;
ball.vy = Math.sin(angle) * speed;
ball.isMoving = true;
canLaunch = false;
};
// --- Main Update Loop ---
game.update = function () {
if (ball) ball.update();
for (var i = 0; i < obstacles.length; i++) {
if (typeof obstacles[i].update === 'function') obstacles[i].update();
}
};
// --- Start Game ---
loadLevel(currentLevel);
// Play background music infinitely
LK.playMusic('Bg');