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; // Friction (minimal, for realism) self.vx *= 0.995; self.vy *= 0.995; // 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); 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; 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: 0xf8f9fa }); /**** * Game Code ****/ // Trajectory preview // Obstacle // Reward zone // Ball (player) // --- Utility functions --- 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 = [ // Level 1: Simple, no obstacles { ball: { x: 400, y: 2200, bounces: 3 }, reward: { x: 1700, y: 400 }, obstacles: [] }, // Level 2: One obstacle { ball: { x: 400, y: 2200, bounces: 4 }, reward: { x: 1700, y: 400 }, obstacles: [{ x: 900, y: 1200, width: 400, height: 60 }] }, // Level 3: Two obstacles { ball: { x: 300, y: 2300, bounces: 5 }, reward: { x: 1800, y: 400 }, obstacles: [{ x: 700, y: 1000, width: 500, height: 60 }, { x: 1200, y: 1700, width: 400, height: 60 }] }, // Level 4: Narrow passage { ball: { x: 200, y: 2500, bounces: 6 }, reward: { x: 1800, y: 300 }, obstacles: [{ x: 600, y: 800, width: 900, height: 60 }, { x: 600, y: 1600, width: 900, height: 60 }, { x: 600, y: 1200, width: 60, height: 400 }, { x: 1440, y: 1200, width: 60, height: 400 }] } // Add more levels as needed ]; // --- Game State --- var currentLevel = 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); // --- 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 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; obs.height = o.height; obs.children[0].width = o.width; obs.children[0].height = o.height; 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.035 * power; // Tune as needed 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.035 * power; 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(); }; // --- Start Game --- loadLevel(currentLevel);
===================================================================
--- original.js
+++ change.js
@@ -1,6 +1,532 @@
-/****
+/****
+* 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;
+ // Friction (minimal, for realism)
+ self.vx *= 0.995;
+ self.vy *= 0.995;
+ // 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);
+ 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;
+ 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
-});
\ No newline at end of file
+ backgroundColor: 0xf8f9fa
+});
+
+/****
+* Game Code
+****/
+// Trajectory preview
+// Obstacle
+// Reward zone
+// Ball (player)
+// --- Utility functions ---
+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 = [
+// Level 1: Simple, no obstacles
+{
+ ball: {
+ x: 400,
+ y: 2200,
+ bounces: 3
+ },
+ reward: {
+ x: 1700,
+ y: 400
+ },
+ obstacles: []
+},
+// Level 2: One obstacle
+{
+ ball: {
+ x: 400,
+ y: 2200,
+ bounces: 4
+ },
+ reward: {
+ x: 1700,
+ y: 400
+ },
+ obstacles: [{
+ x: 900,
+ y: 1200,
+ width: 400,
+ height: 60
+ }]
+},
+// Level 3: Two obstacles
+{
+ ball: {
+ x: 300,
+ y: 2300,
+ bounces: 5
+ },
+ reward: {
+ x: 1800,
+ y: 400
+ },
+ obstacles: [{
+ x: 700,
+ y: 1000,
+ width: 500,
+ height: 60
+ }, {
+ x: 1200,
+ y: 1700,
+ width: 400,
+ height: 60
+ }]
+},
+// Level 4: Narrow passage
+{
+ ball: {
+ x: 200,
+ y: 2500,
+ bounces: 6
+ },
+ reward: {
+ x: 1800,
+ y: 300
+ },
+ obstacles: [{
+ x: 600,
+ y: 800,
+ width: 900,
+ height: 60
+ }, {
+ x: 600,
+ y: 1600,
+ width: 900,
+ height: 60
+ }, {
+ x: 600,
+ y: 1200,
+ width: 60,
+ height: 400
+ }, {
+ x: 1440,
+ y: 1200,
+ width: 60,
+ height: 400
+ }]
+}
+// Add more levels as needed
+];
+// --- Game State ---
+var currentLevel = 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);
+// --- 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
+ 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;
+ obs.height = o.height;
+ obs.children[0].width = o.width;
+ obs.children[0].height = o.height;
+ 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.035 * power; // Tune as needed
+ 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.035 * power;
+ 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();
+};
+// --- Start Game ---
+loadLevel(currentLevel);
\ No newline at end of file