/**** * 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');