/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Ball class: bounces, shrinks on hit, rebounds off everything
var Ball = Container.expand(function () {
var self = Container.call(this);
// Ball type: 'red' or 'green'
self.type = 'red';
self.radius = 90; // initial radius
self.minRadius = 40; // minimum radius after shrinking
self.shrinkAmount = 18; // how much to shrink per hit
self.shrinkCount = 0; // Tracks how many times shrink() has been called
// Attach asset
self.asset = null;
self.setType = function (type) {
self.type = type;
if (self.asset) self.removeChild(self.asset);
var assetId = type === 'red' ? 'ballRed' : 'ballGreen';
self.asset = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5,
width: self.radius * 2,
height: self.radius * 2
});
};
// Set initial type
self.setType(self.type);
// Physics
self.vx = 0;
self.vy = 0;
self.bounce = 0.98; // energy loss on bounce
self.gravity = 1.2;
// For collision detection
self.getRadius = function () {
return self.radius;
};
// Shrink ball
self.shrink = function () {
self.shrinkCount++; // Increment count for every shrink attempt/hit
if (self.radius > self.minRadius) {
self.radius -= self.shrinkAmount;
if (self.radius < self.minRadius) self.radius = self.minRadius;
// Animate shrink
tween(self.asset, {
width: self.radius * 2,
height: self.radius * 2
}, {
duration: 180,
easing: tween.easeOut
});
}
};
// Update per frame
self.update = function () {
// Move
self.x += self.vx;
self.y += self.vy;
// Gravity
self.vy += self.gravity;
// Bounce off screen boundaries (walls and corners)
// Horizontal bounces
if (self.x - self.radius < 0) {
self.x = self.radius;
self.vx = -self.vx * self.bounce;
} else if (self.x + self.radius > 2048) {
self.x = 2048 - self.radius;
self.vx = -self.vx * self.bounce;
}
// Vertical bounces
if (self.y - self.radius < 0) {
self.y = self.radius;
self.vy = -self.vy * self.bounce;
} else if (self.y + self.radius > 2732) {
self.y = 2732 - self.radius;
self.vy = -self.vy * self.bounce;
}
// --- Prevent balls from ever crossing any line drawn by the left mouse button ---
// We'll store all drawn lines in a global array: drawnLines
if (typeof drawnLines !== "undefined" && drawnLines && drawnLines.length > 0) {
for (var i = 0; i < drawnLines.length; ++i) {
var line = drawnLines[i];
// Each line has: {x1, y1, x2, y2}
var dx = line.x2 - line.x1;
var dy = line.y2 - line.y1;
var len = Math.sqrt(dx * dx + dy * dy);
if (len === 0) continue;
var nx = dx / len;
var ny = dy / len;
// Project ball center onto the line
var px = self.x - line.x1;
var py = self.y - line.y1;
var proj = px * nx + py * ny;
// Clamp projection to the line segment
if (proj < 0) proj = 0;
if (proj > len) proj = len;
// Closest point on the line segment
var closestX = line.x1 + nx * proj;
var closestY = line.y1 + ny * proj;
var distX = self.x - closestX;
var distY = self.y - closestY;
var dist = Math.sqrt(distX * distX + distY * distY);
if (dist < self.radius) {
// Ball is overlapping the line, push it out and reflect velocity
var overlap = self.radius - dist;
// Defensive: avoid division by zero
if (dist === 0) {
distX = 1;
distY = 0;
dist = 1;
}
var nnx = distX / dist;
var nny = distY / dist;
self.x += nnx * overlap;
self.y += nny * overlap;
// Project velocity onto normal
var dot = self.vx * nnx + self.vy * nny;
// Reflect velocity (elastic bounce)
self.vx -= 2 * dot * nnx;
self.vy -= 2 * dot * nny;
// Dampen to simulate energy loss (same as ball-ball)
self.vx *= self.bounce;
self.vy *= self.bounce;
}
}
}
// --- Prevent balls from ever crossing the aimLine and bounce off it like a wall ---
// Only enforce if aimLine exists and is visible
if (typeof aimLine !== "undefined" && aimLine && aimLine.visible) {
// Calculate the normal vector of the aimLine
var dx = Math.cos(aimLine.rotation);
var dy = Math.sin(aimLine.rotation);
// Vector from aimLine start to ball center
var px = self.x - aimLine.x;
var py = self.y - aimLine.y;
// Project vector onto aimLine direction to get closest point on the line
var proj = px * dx + py * dy;
// Clamp projection to the aimLine segment
var len = aimLine.width;
if (proj < 0) proj = 0;
if (proj > len) proj = len;
// Closest point on the aimLine
var closestX = aimLine.x + dx * proj;
var closestY = aimLine.y + dy * proj;
// Distance from ball center to closest point
var distX = self.x - closestX;
var distY = self.y - closestY;
var dist = Math.sqrt(distX * distX + distY * distY);
if (dist < self.radius) {
// Ball is overlapping the aimLine, push it out and reflect velocity
var overlap = self.radius - dist;
// Defensive: avoid division by zero
if (dist === 0) {
distX = 1;
distY = 0;
dist = 1;
}
var nx = distX / dist;
var ny = distY / dist;
self.x += nx * overlap;
self.y += ny * overlap;
// Project velocity onto normal
var dot = self.vx * nx + self.vy * ny;
// Reflect velocity (elastic bounce)
self.vx -= 2 * dot * nx;
self.vy -= 2 * dot * ny;
// Dampen to simulate energy loss (same as ball-ball)
self.vx *= self.bounce;
self.vy *= self.bounce;
}
}
};
return self;
});
// Chain class: fires in direction, stops at collision or edge
var Chain = Container.expand(function () {
var self = Container.call(this);
// Chain is a series of segments
self.segments = [];
self.segmentLength = 64;
self.maxSegments = 32;
self.dirX = 0;
self.dirY = 0;
self.originX = 0;
self.originY = 0;
self.active = true;
// For collision
self.getTip = function () {
if (self.segments.length === 0) return {
x: self.originX,
y: self.originY
};
var last = self.segments[self.segments.length - 1];
return {
x: last.x,
y: last.y
};
};
// Initialize chain
self.fire = function (originX, originY, dirX, dirY) {
self.originX = originX;
self.originY = originY;
self.dirX = dirX;
self.dirY = dirY;
self.segments = [];
self.active = true;
// Remove old children
while (self.children.length) self.removeChild(self.children[0]);
// Add first segment
var seg = LK.getAsset('chain', {
anchorX: 0.5,
anchorY: 0.5
});
seg.x = originX;
seg.y = originY;
self.addChild(seg);
self.segments.push(seg);
};
// Update per frame
self.update = function () {
if (!self.active) return;
// Defensive: Only proceed if there is at least one segment
if (self.segments.length === 0) return;
// Add new segment in direction
var last = self.segments[self.segments.length - 1];
var nx = last.x + self.dirX * self.segmentLength;
var ny = last.y + self.dirY * self.segmentLength;
// Stop if out of bounds
if (nx < 0 || nx > 2048 || ny < 0 || ny > 2732) {
self.active = false;
return;
}
// Add new segment
var seg = LK.getAsset('chain', {
anchorX: 0.5,
anchorY: 0.5
});
seg.x = nx;
seg.y = ny;
self.addChild(seg);
self.segments.push(seg);
// Limit length
if (self.segments.length > self.maxSegments) {
self.active = false;
}
};
// Remove all segments
self.clear = function () {
while (self.children.length) self.removeChild(self.children[0]);
self.segments = [];
self.active = false;
};
return self;
});
// Robot class: moves left/right, fires chain
var Robot = Container.expand(function () {
var self = Container.call(this);
self.asset = self.attachAsset('robot', {
anchorX: 0.5,
anchorY: 0.5
});
self.width = self.asset.width;
self.height = self.asset.height;
// For movement
self.speed = 0;
self.maxSpeed = 32;
self.targetX = 1024;
// For collision
// Update per frame
self.update = function () {
// Move towards targetX
var dx = self.targetX - self.x;
if (Math.abs(dx) > 8) {
self.speed = Math.max(-self.maxSpeed, Math.min(self.maxSpeed, dx * 0.25));
self.x += self.speed;
} else {
self.x = self.targetX;
self.speed = 0;
}
// Clamp to screen
if (self.x < self.width / 2) self.x = self.width / 2;
if (self.x > 2048 - self.width / 2) self.x = 2048 - self.width / 2;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x181818
});
/****
* Game Code
****/
// --- Game variables ---
// Robot: blue box
// Play intro music at game start
LK.playMusic('introMusic', {
loop: false
});
// Ball: red
// Ball: green
// Chain: yellow box (thin)
// Aiming line: white box (thin, will be scaled)
var robot = new Robot();
var balls = [];
var chain = new Chain();
var aimLine = null;
var isAiming = false;
var aimStart = {
x: 0,
y: 0
};
var aimEnd = {
x: 0,
y: 0
};
var canFire = true;
var fireCooldown = 18; // frames between shots
var fireTimer = 0;
var score = 0;
var scoreTxt = null;
// --- Add robot ---
robot.x = 1024;
robot.y = 2732 - 120;
game.addChild(robot);
// After intro music ends, start main music (simulate with timeout, since no event system)
LK.setTimeout(function () {
LK.playMusic('mainMusic', {
loop: true
});
}, 4000); // Adjust 4000ms to match your intro music length
// --- Add balls ---
var ball1 = new Ball();
ball1.setType('red');
ball1.radius = 160; // enlarged radius
ball1.asset.width = ball1.radius * 2;
ball1.asset.height = ball1.radius * 2;
ball1.x = 600;
ball1.y = 800;
// Set initial velocity to make the ball start moving
ball1.vx = 18;
ball1.vy = -22;
// Only add one red ball (remove this one)
// balls.push(ball1);
// game.addChild(ball1);
// Duplicate ball1 (keep only one red ball)
var ball1b = new Ball();
ball1b.setType('red');
ball1b.radius = 160;
ball1b.asset.width = ball1b.radius * 2;
ball1b.asset.height = ball1b.radius * 2;
ball1b.x = 600 + 120; // offset to avoid overlap
ball1b.y = 800 + 120;
ball1b.vx = 18;
ball1b.vy = -22;
balls.push(ball1b);
game.addChild(ball1b);
var ball2 = new Ball();
ball2.setType('green');
ball2.radius = 160; // enlarged radius
ball2.asset.width = ball2.radius * 2;
ball2.asset.height = ball2.radius * 2;
ball2.x = 1448;
ball2.y = 900;
// Set initial velocity to make the ball start moving
ball2.vx = -16;
ball2.vy = -18;
// Only add one green ball (remove this one)
// balls.push(ball2);
// game.addChild(ball2);
// Duplicate ball2 (keep only one green ball)
var ball2b = new Ball();
ball2b.setType('green');
ball2b.radius = 160;
ball2b.asset.width = ball2b.radius * 2;
ball2b.asset.height = ball2b.radius * 2;
ball2b.x = 1448 - 120; // offset to avoid overlap
ball2b.y = 900 + 120;
ball2b.vx = -16;
ball2b.vy = -18;
balls.push(ball2b);
game.addChild(ball2b);
// --- Add chain (hidden at start) ---
game.addChild(chain);
// --- Score text ---
// Scoreboard label
var scoreboardLabel = new Text2('²H collisions scoreboard', {
size: 64,
fill: 0xFFFFFF
});
scoreboardLabel.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreboardLabel);
scoreboardLabel.y = 0;
// Score text
scoreTxt = new Text2('0', {
size: 120,
fill: 0xFFFFFF
});
scoreTxt.anchor.set(0.5, 0);
scoreTxt.y = scoreboardLabel.height + 10;
LK.gui.top.addChild(scoreTxt);
// --- Store all drawn lines globally ---
var drawnLines = [];
// --- Aiming line ---
function updateAimLine() {
if (!aimLine) {
aimLine = LK.getAsset('aimLine', {
anchorX: 0,
anchorY: 0.5
});
aimLine.alpha = 0.5;
game.addChild(aimLine);
}
var dx = aimEnd.x - aimStart.x;
var dy = aimEnd.y - aimStart.y;
var len = Math.sqrt(dx * dx + dy * dy);
if (len < 80) len = 80; // minimum length
aimLine.x = aimStart.x;
aimLine.y = aimStart.y;
aimLine.width = len;
aimLine.height = 16;
aimLine.rotation = Math.atan2(dy, dx);
aimLine.visible = true;
}
function hideAimLine() {
if (aimLine) aimLine.visible = false;
}
// --- Input handling ---
game.down = function (x, y, obj) {
// Handle aiming or ball destruction based on tap location
if (y > 2732 - 600) {
// Aiming zone (lower part of screen)
isAiming = true;
aimStart.x = robot.x;
aimStart.y = robot.y - robot.height / 2;
aimEnd.x = x;
aimEnd.y = y;
updateAimLine();
} else {
// Destruction zone (upper part of screen - our "right-click" equivalent)
// When the "right mouse button" is pressed, the game continues running and a sound plays.
LK.getSound('shoot').play();
}
};
game.move = function (x, y, obj) {
if (isAiming) {
// Update aim direction
aimEnd.x = x;
aimEnd.y = y;
updateAimLine();
} else {
// Move robot left/right only
robot.targetX = x;
}
};
game.up = function (x, y, obj) {
if (isAiming && canFire) {
// Fire chain in direction
var dx = aimEnd.x - aimStart.x;
var dy = aimEnd.y - aimStart.y;
var len = Math.sqrt(dx * dx + dy * dy);
if (len > 80) {
var dirX = dx / len;
var dirY = dy / len;
chain.clear();
chain.fire(aimStart.x, aimStart.y, dirX, dirY);
canFire = false;
fireTimer = fireCooldown;
}
// Add the drawn line to drawnLines array if it is long enough
if (len > 80) {
drawnLines.push({
x1: aimStart.x,
y1: aimStart.y,
x2: aimEnd.x,
y2: aimEnd.y
});
}
// Remove all but the most recent line (keep only the last one)
if (drawnLines.length > 1) {
drawnLines.splice(0, drawnLines.length - 1);
}
}
isAiming = false;
hideAimLine();
};
// Helper function to get distance to robot if a ball is touching it
function getDistanceToRobotIfTouching(ball, robotInstance) {
var bounds = robotInstance.getBounds();
// Closest point on robot's bounding box to ball center
var cx = Math.max(bounds.left, Math.min(ball.x, bounds.right));
var cy = Math.max(bounds.top, Math.min(ball.y, bounds.bottom));
var dxBallToRobotRect = ball.x - cx;
var dyBallToRobotRect = ball.y - cy;
var distSquared = dxBallToRobotRect * dxBallToRobotRect + dyBallToRobotRect * dyBallToRobotRect;
var dist = Math.sqrt(distSquared);
// A ball is touching if the distance from its center to the robot's rectangle
// is less than its radius.
if (dist < ball.getRadius()) {
return dist; // Return the actual distance
}
return Infinity; // Not touching
}
// --- Ball-ball collision ---
function ballsCollide(b1, b2) {
var dx = b1.x - b2.x;
var dy = b1.y - b2.y;
var dist = Math.sqrt(dx * dx + dy * dy);
return dist < b1.radius + b2.radius;
}
function resolveBallCollision(b1, b2) {
var dx = b1.x - b2.x;
var dy = b1.y - b2.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist === 0) return;
var overlap = b1.radius + b2.radius - dist;
if (overlap > 0) {
// Push balls apart
var nx = dx / dist;
var ny = dy / dist;
b1.x += nx * (overlap / 2);
b1.y += ny * (overlap / 2);
b2.x -= nx * (overlap / 2);
b2.y -= ny * (overlap / 2);
// Exchange velocities (elastic)
var tx = nx;
var ty = ny;
var v1 = b1.vx * tx + b1.vy * ty;
var v2 = b2.vx * tx + b2.vy * ty;
var m1 = b1.radius;
var m2 = b2.radius;
var newV1 = (v1 * (m1 - m2) + 2 * m2 * v2) / (m1 + m2);
var newV2 = (v2 * (m2 - m1) + 2 * m1 * v1) / (m1 + m2);
b1.vx += (newV1 - v1) * tx;
b1.vy += (newV1 - v1) * ty;
b2.vx += (newV2 - v2) * tx;
b2.vy += (newV2 - v2) * ty;
}
}
// --- Ball-robot collision ---
function ballHitsRobot(ball, robot) {
var bounds = robot.getBounds();
// Closest point on robot to ball center
var cx = Math.max(bounds.left, Math.min(ball.x, bounds.right));
var cy = Math.max(bounds.top, Math.min(ball.y, bounds.bottom));
var dx = ball.x - cx;
var dy = ball.y - cy;
var dist = Math.sqrt(dx * dx + dy * dy);
return dist < ball.radius;
}
function resolveBallRobotCollision(ball, robot) {
var bounds = robot.getBounds();
var cx = Math.max(bounds.left, Math.min(ball.x, bounds.right));
var cy = Math.max(bounds.top, Math.min(ball.y, bounds.bottom));
var dx = ball.x - cx;
var dy = ball.y - cy;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist === 0) return;
var overlap = ball.radius - dist;
if (overlap > 0) {
// Push ball out
var nx = dx / dist;
var ny = dy / dist;
ball.x += nx * overlap;
ball.y += ny * overlap;
// Reflect velocity
var dot = ball.vx * nx + ball.vy * ny;
ball.vx -= 2 * dot * nx;
ball.vy -= 2 * dot * ny;
ball.vx *= 0.9;
ball.vy *= 0.9;
}
}
// --- Chain-ball collision ---
function chainHitsBall(chain, ball) {
// Check each segment
for (var i = 0; i < chain.segments.length; ++i) {
var seg = chain.segments[i];
var dx = seg.x - ball.x;
var dy = seg.y - ball.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < ball.radius) {
return true;
}
}
return false;
}
// --- Chain stops at first collision with ball or wall ---
function updateChainCollisions() {
if (!chain.active) return;
for (var i = 0; i < balls.length; ++i) {
if (chainHitsBall(chain, balls[i])) {
var hitBall = balls[i];
// --- Chain acts as a barrier: bounce ball off chain, do not allow pass-through ---
// Find the closest segment that collides
var closestSeg = null;
var minDist = Infinity;
for (var j = 0; j < chain.segments.length; ++j) {
var seg = chain.segments[j];
var dx = seg.x - hitBall.x;
var dy = seg.y - hitBall.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < hitBall.radius && dist < minDist) {
minDist = dist;
closestSeg = seg;
}
}
if (closestSeg) {
// Push ball out of the chain segment and reflect velocity (barrier bounce)
var dx = hitBall.x - closestSeg.x;
var dy = hitBall.y - closestSeg.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist === 0) {
// Avoid division by zero, nudge
dx = 1;
dy = 0;
dist = 1;
}
var overlap = hitBall.radius - dist;
if (overlap > 0) {
var nx = dx / dist;
var ny = dy / dist;
// Move ball out of the chain
hitBall.x += nx * overlap;
hitBall.y += ny * overlap;
// --- Ball bounce off chain, just like ball-ball collision ---
// Project velocity onto normal
var dot = hitBall.vx * nx + hitBall.vy * ny;
// Reflect velocity (elastic bounce)
hitBall.vx -= 2 * dot * nx;
hitBall.vy -= 2 * dot * ny;
// Dampen to simulate energy loss (same as ball-ball)
hitBall.vx *= hitBall.bounce;
hitBall.vy *= hitBall.bounce;
}
// Do NOT deactivate the chain here; allow it to persist as a barrier (chain acts as a wall)
}
// --- Shrink and split logic ---
if (hitBall.radius > hitBall.minRadius) {
// Shrink the ball down to minRadius if possible
hitBall.shrink();
// If after shrinking, it's still above minRadius + epsilon, allow further splitting
if (hitBall.radius > hitBall.minRadius) {
// Split into two balls
var newRadius = Math.max(hitBall.radius / 1.5, hitBall.minRadius);
for (var s = 0; s < 2; ++s) {
var newBall = new Ball();
newBall.setType(hitBall.type);
newBall.radius = newRadius;
newBall.asset.width = newRadius * 2;
newBall.asset.height = newRadius * 2;
// Offset new balls slightly to avoid overlap
var angle = Math.PI / 4 + s * (Math.PI / 2);
newBall.x = hitBall.x + Math.cos(angle) * newRadius;
newBall.y = hitBall.y + Math.sin(angle) * newRadius;
// Give them some velocity away from each other
newBall.vx = hitBall.vx + (s === 0 ? -12 : 12);
newBall.vy = hitBall.vy - 10;
balls.push(newBall);
game.addChild(newBall);
}
// Remove the original ball
game.removeChild(hitBall);
balls.splice(i, 1);
i--; // adjust index after removal
}
} else {
// Ball is at minimum size, destroy it
game.removeChild(hitBall);
balls.splice(i, 1);
i--; // adjust index after removal
}
score += 1;
scoreTxt.setText(score);
// Flash ball
LK.effects.flashObject(hitBall, 0xffff00, 200);
break;
}
}
}
// --- Game update loop ---
var ballCollisionCount = 0;
var maxBallCollisions = 100;
var maxLimitTxt = null;
var winMessage = null;
var gameOverMessage = null;
var screenOverlay = LK.getAsset('screen', {
anchorX: 0,
anchorY: 0,
width: 2048,
height: 2732,
x: 0,
y: 0
});
game.addChildAt(screenOverlay, 0);
game.update = function () {
// Show max limit message if not already shown
if (!maxLimitTxt) {
maxLimitTxt = new Text2("Max limit 100", {
size: 64,
fill: 0xff0000
});
maxLimitTxt.anchor.set(0.5, 0);
maxLimitTxt.y = scoreTxt.y + scoreTxt.height + 10;
LK.gui.top.addChild(maxLimitTxt);
}
// Update robot
robot.update();
// Update balls
for (var i = 0; i < balls.length; ++i) {
balls[i].update();
// --- Enforce chain as a barrier every frame, so balls never pass through ---
if (chain.active) {
// For each segment, check collision and bounce if needed
for (var j = 0; j < chain.segments.length; ++j) {
var seg = chain.segments[j];
var dx = balls[i].x - seg.x;
var dy = balls[i].y - seg.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < balls[i].radius) {
// Push ball out of the chain segment and reflect velocity (barrier bounce)
var overlap = balls[i].radius - dist;
if (dist === 0) {
dx = 1;
dy = 0;
dist = 1;
}
var nx = dx / dist;
var ny = dy / dist;
balls[i].x += nx * overlap;
balls[i].y += ny * overlap;
// Project velocity onto normal
var dot = balls[i].vx * nx + balls[i].vy * ny;
// Reflect velocity (elastic bounce)
balls[i].vx -= 2 * dot * nx;
balls[i].vy -= 2 * dot * ny;
// Dampen to simulate energy loss (same as ball-ball)
balls[i].vx *= balls[i].bounce;
balls[i].vy *= balls[i].bounce;
}
}
}
}
// Ball-ball collision
// Check all unique pairs of balls for collision
for (var i = 0; i < balls.length; ++i) {
for (var j = i + 1; j < balls.length; ++j) {
var b1 = balls[i];
var b2 = balls[j];
// Track last and current collision state for this pair
if (b1.lastCollided === undefined) b1.lastCollided = {};
if (b2.lastCollided === undefined) b2.lastCollided = {};
var pairKey = j; // unique for (i,j)
var lastColliding = b1.lastCollided[pairKey] || false;
var nowColliding = ballsCollide(b1, b2);
// Only increment score on the exact frame the collision starts
if (!lastColliding && nowColliding) {
// Only increment score if one is red and one is green (²H and ³H)
var t1 = b1.type;
var t2 = b2.type;
var isRedGreen = t1 === 'red' && t2 === 'green' || t1 === 'green' && t2 === 'red';
if (isRedGreen) {
if (ballCollisionCount < maxBallCollisions) {
score += 1;
if (score > maxBallCollisions) score = maxBallCollisions;
scoreTxt.setText(score);
}
}
// Always resolve collision on first contact
resolveBallCollision(b1, b2);
// Increment collision counter and check for game over
if (isRedGreen && ballCollisionCount < maxBallCollisions) {
ballCollisionCount++;
}
if (ballCollisionCount >= maxBallCollisions) {
// Clamp score to 100 and update display
score = maxBallCollisions;
scoreTxt.setText(score);
// Show overlay and GAME OVER message
if (!screenOverlay) {
screenOverlay = LK.getAsset('screen', {
anchorX: 0,
anchorY: 0,
width: 2048,
height: 2732,
x: 0,
y: 0
});
game.addChildAt(screenOverlay, 0);
}
if (!gameOverMessage) {
gameOverMessage = new Text2("GAME OVER", {
size: 220,
fill: 0xff0000
});
gameOverMessage.anchor.set(0.5, 0.5);
gameOverMessage.x = 2048 / 2;
gameOverMessage.y = 2732 / 2;
game.addChild(gameOverMessage);
}
// Play outro music
LK.playMusic('outroMusic', {
loop: false
});
// Show GAME OVER popup and stop the game
LK.showGameOver();
return;
}
} else if (nowColliding) {
// If still colliding, continue to resolve collision
resolveBallCollision(b1, b2);
}
// Update last collision state for this pair
b1.lastCollided[pairKey] = nowColliding;
}
}
// Ball-robot collision
for (var i = 0; i < balls.length; ++i) {
if (ballHitsRobot(balls[i], robot)) {
resolveBallRobotCollision(balls[i], robot);
// Game over
LK.effects.flashScreen(0xff0000, 1000);
LK.showGameOver();
return;
}
}
// Update chain
if (chain.active) {
chain.update();
// --- Atom splitter logic: check for chain-ball collisions and handle split/shrink ---
for (var i = 0; i < balls.length; ++i) {
var ball = balls[i];
// Track last and current collision state for this ball
if (ball.lastChainColliding === undefined) ball.lastChainColliding = false;
var currentlyColliding = chainHitsBall(chain, ball);
// Only trigger split/shrink on the exact frame the collision starts
if (!ball.lastChainColliding && currentlyColliding) {
// Find the closest segment that collides
var closestSeg = null;
var minDist = Infinity;
for (var j = 0; j < chain.segments.length; ++j) {
var seg = chain.segments[j];
var dx = seg.x - ball.x;
var dy = seg.y - ball.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < ball.radius && dist < minDist) {
minDist = dist;
closestSeg = seg;
}
}
if (closestSeg) {
// Push ball out of the chain segment and reflect velocity (barrier bounce)
var dx = ball.x - closestSeg.x;
var dy = ball.y - closestSeg.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist === 0) {
dx = 1;
dy = 0;
dist = 1;
}
var overlap = ball.radius - dist;
if (overlap > 0) {
var nx = dx / dist;
var ny = dy / dist;
ball.x += nx * overlap;
ball.y += ny * overlap;
// Project velocity onto normal
var dot = ball.vx * nx + ball.vy * ny;
// Reflect velocity (elastic bounce)
ball.vx -= 2 * dot * nx;
ball.vy -= 2 * dot * ny;
// Dampen to simulate energy loss (same as ball-ball)
ball.vx *= ball.bounce;
ball.vy *= ball.bounce;
}
}
// --- Shrink and split logic ---
if (ball.radius > ball.minRadius) {
// Shrink the ball down to minRadius if possible
ball.shrink();
// If after shrinking, it's still above minRadius + epsilon, allow further splitting
if (ball.radius > ball.minRadius) {
// Split into two balls
var newRadius = Math.max(ball.radius / 1.5, ball.minRadius);
for (var s = 0; s < 2; ++s) {
var newBall = new Ball();
newBall.setType(ball.type);
newBall.radius = newRadius;
newBall.asset.width = newRadius * 2;
newBall.asset.height = newRadius * 2;
// Offset new balls slightly to avoid overlap
var angle = Math.PI / 4 + s * (Math.PI / 2);
newBall.x = ball.x + Math.cos(angle) * newRadius;
newBall.y = ball.y + Math.sin(angle) * newRadius;
// Give them some velocity away from each other
newBall.vx = ball.vx + (s === 0 ? -12 : 12);
newBall.vy = ball.vy - 10;
balls.push(newBall);
game.addChild(newBall);
}
// Remove the original ball
game.removeChild(ball);
balls.splice(i, 1);
i--; // adjust index after removal
}
} else {
// Ball is at minimum size, destroy it
game.removeChild(ball);
balls.splice(i, 1);
i--; // adjust index after removal
}
score += 1;
scoreTxt.setText(score);
// Flash ball
LK.effects.flashObject(ball, 0xffff00, 200);
}
// Update last collision state
ball.lastChainColliding = currentlyColliding;
}
}
// Fire cooldown
if (!canFire) {
fireTimer--;
if (fireTimer <= 0) {
canFire = true;
}
}
// WIN condition: if no balls left, show YOU WIN!
if (balls.length === 0) {
if (!screenOverlay) {
screenOverlay = LK.getAsset('screen', {
anchorX: 0,
anchorY: 0,
width: 2048,
height: 2732,
x: 0,
y: 0
});
game.addChildAt(screenOverlay, 0);
}
if (!winMessage) {
winMessage = new Text2("YOU WIN!", {
size: 220,
fill: 0x00ff00
});
winMessage.anchor.set(0.5, 0.5);
winMessage.x = 2048 / 2;
winMessage.y = 2732 / 2;
game.addChild(winMessage);
// Play outro music
LK.playMusic('outroMusic', {
loop: false
});
}
return;
}
}; /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Ball class: bounces, shrinks on hit, rebounds off everything
var Ball = Container.expand(function () {
var self = Container.call(this);
// Ball type: 'red' or 'green'
self.type = 'red';
self.radius = 90; // initial radius
self.minRadius = 40; // minimum radius after shrinking
self.shrinkAmount = 18; // how much to shrink per hit
self.shrinkCount = 0; // Tracks how many times shrink() has been called
// Attach asset
self.asset = null;
self.setType = function (type) {
self.type = type;
if (self.asset) self.removeChild(self.asset);
var assetId = type === 'red' ? 'ballRed' : 'ballGreen';
self.asset = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5,
width: self.radius * 2,
height: self.radius * 2
});
};
// Set initial type
self.setType(self.type);
// Physics
self.vx = 0;
self.vy = 0;
self.bounce = 0.98; // energy loss on bounce
self.gravity = 1.2;
// For collision detection
self.getRadius = function () {
return self.radius;
};
// Shrink ball
self.shrink = function () {
self.shrinkCount++; // Increment count for every shrink attempt/hit
if (self.radius > self.minRadius) {
self.radius -= self.shrinkAmount;
if (self.radius < self.minRadius) self.radius = self.minRadius;
// Animate shrink
tween(self.asset, {
width: self.radius * 2,
height: self.radius * 2
}, {
duration: 180,
easing: tween.easeOut
});
}
};
// Update per frame
self.update = function () {
// Move
self.x += self.vx;
self.y += self.vy;
// Gravity
self.vy += self.gravity;
// Bounce off screen boundaries (walls and corners)
// Horizontal bounces
if (self.x - self.radius < 0) {
self.x = self.radius;
self.vx = -self.vx * self.bounce;
} else if (self.x + self.radius > 2048) {
self.x = 2048 - self.radius;
self.vx = -self.vx * self.bounce;
}
// Vertical bounces
if (self.y - self.radius < 0) {
self.y = self.radius;
self.vy = -self.vy * self.bounce;
} else if (self.y + self.radius > 2732) {
self.y = 2732 - self.radius;
self.vy = -self.vy * self.bounce;
}
// --- Prevent balls from ever crossing any line drawn by the left mouse button ---
// We'll store all drawn lines in a global array: drawnLines
if (typeof drawnLines !== "undefined" && drawnLines && drawnLines.length > 0) {
for (var i = 0; i < drawnLines.length; ++i) {
var line = drawnLines[i];
// Each line has: {x1, y1, x2, y2}
var dx = line.x2 - line.x1;
var dy = line.y2 - line.y1;
var len = Math.sqrt(dx * dx + dy * dy);
if (len === 0) continue;
var nx = dx / len;
var ny = dy / len;
// Project ball center onto the line
var px = self.x - line.x1;
var py = self.y - line.y1;
var proj = px * nx + py * ny;
// Clamp projection to the line segment
if (proj < 0) proj = 0;
if (proj > len) proj = len;
// Closest point on the line segment
var closestX = line.x1 + nx * proj;
var closestY = line.y1 + ny * proj;
var distX = self.x - closestX;
var distY = self.y - closestY;
var dist = Math.sqrt(distX * distX + distY * distY);
if (dist < self.radius) {
// Ball is overlapping the line, push it out and reflect velocity
var overlap = self.radius - dist;
// Defensive: avoid division by zero
if (dist === 0) {
distX = 1;
distY = 0;
dist = 1;
}
var nnx = distX / dist;
var nny = distY / dist;
self.x += nnx * overlap;
self.y += nny * overlap;
// Project velocity onto normal
var dot = self.vx * nnx + self.vy * nny;
// Reflect velocity (elastic bounce)
self.vx -= 2 * dot * nnx;
self.vy -= 2 * dot * nny;
// Dampen to simulate energy loss (same as ball-ball)
self.vx *= self.bounce;
self.vy *= self.bounce;
}
}
}
// --- Prevent balls from ever crossing the aimLine and bounce off it like a wall ---
// Only enforce if aimLine exists and is visible
if (typeof aimLine !== "undefined" && aimLine && aimLine.visible) {
// Calculate the normal vector of the aimLine
var dx = Math.cos(aimLine.rotation);
var dy = Math.sin(aimLine.rotation);
// Vector from aimLine start to ball center
var px = self.x - aimLine.x;
var py = self.y - aimLine.y;
// Project vector onto aimLine direction to get closest point on the line
var proj = px * dx + py * dy;
// Clamp projection to the aimLine segment
var len = aimLine.width;
if (proj < 0) proj = 0;
if (proj > len) proj = len;
// Closest point on the aimLine
var closestX = aimLine.x + dx * proj;
var closestY = aimLine.y + dy * proj;
// Distance from ball center to closest point
var distX = self.x - closestX;
var distY = self.y - closestY;
var dist = Math.sqrt(distX * distX + distY * distY);
if (dist < self.radius) {
// Ball is overlapping the aimLine, push it out and reflect velocity
var overlap = self.radius - dist;
// Defensive: avoid division by zero
if (dist === 0) {
distX = 1;
distY = 0;
dist = 1;
}
var nx = distX / dist;
var ny = distY / dist;
self.x += nx * overlap;
self.y += ny * overlap;
// Project velocity onto normal
var dot = self.vx * nx + self.vy * ny;
// Reflect velocity (elastic bounce)
self.vx -= 2 * dot * nx;
self.vy -= 2 * dot * ny;
// Dampen to simulate energy loss (same as ball-ball)
self.vx *= self.bounce;
self.vy *= self.bounce;
}
}
};
return self;
});
// Chain class: fires in direction, stops at collision or edge
var Chain = Container.expand(function () {
var self = Container.call(this);
// Chain is a series of segments
self.segments = [];
self.segmentLength = 64;
self.maxSegments = 32;
self.dirX = 0;
self.dirY = 0;
self.originX = 0;
self.originY = 0;
self.active = true;
// For collision
self.getTip = function () {
if (self.segments.length === 0) return {
x: self.originX,
y: self.originY
};
var last = self.segments[self.segments.length - 1];
return {
x: last.x,
y: last.y
};
};
// Initialize chain
self.fire = function (originX, originY, dirX, dirY) {
self.originX = originX;
self.originY = originY;
self.dirX = dirX;
self.dirY = dirY;
self.segments = [];
self.active = true;
// Remove old children
while (self.children.length) self.removeChild(self.children[0]);
// Add first segment
var seg = LK.getAsset('chain', {
anchorX: 0.5,
anchorY: 0.5
});
seg.x = originX;
seg.y = originY;
self.addChild(seg);
self.segments.push(seg);
};
// Update per frame
self.update = function () {
if (!self.active) return;
// Defensive: Only proceed if there is at least one segment
if (self.segments.length === 0) return;
// Add new segment in direction
var last = self.segments[self.segments.length - 1];
var nx = last.x + self.dirX * self.segmentLength;
var ny = last.y + self.dirY * self.segmentLength;
// Stop if out of bounds
if (nx < 0 || nx > 2048 || ny < 0 || ny > 2732) {
self.active = false;
return;
}
// Add new segment
var seg = LK.getAsset('chain', {
anchorX: 0.5,
anchorY: 0.5
});
seg.x = nx;
seg.y = ny;
self.addChild(seg);
self.segments.push(seg);
// Limit length
if (self.segments.length > self.maxSegments) {
self.active = false;
}
};
// Remove all segments
self.clear = function () {
while (self.children.length) self.removeChild(self.children[0]);
self.segments = [];
self.active = false;
};
return self;
});
// Robot class: moves left/right, fires chain
var Robot = Container.expand(function () {
var self = Container.call(this);
self.asset = self.attachAsset('robot', {
anchorX: 0.5,
anchorY: 0.5
});
self.width = self.asset.width;
self.height = self.asset.height;
// For movement
self.speed = 0;
self.maxSpeed = 32;
self.targetX = 1024;
// For collision
// Update per frame
self.update = function () {
// Move towards targetX
var dx = self.targetX - self.x;
if (Math.abs(dx) > 8) {
self.speed = Math.max(-self.maxSpeed, Math.min(self.maxSpeed, dx * 0.25));
self.x += self.speed;
} else {
self.x = self.targetX;
self.speed = 0;
}
// Clamp to screen
if (self.x < self.width / 2) self.x = self.width / 2;
if (self.x > 2048 - self.width / 2) self.x = 2048 - self.width / 2;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x181818
});
/****
* Game Code
****/
// --- Game variables ---
// Robot: blue box
// Play intro music at game start
LK.playMusic('introMusic', {
loop: false
});
// Ball: red
// Ball: green
// Chain: yellow box (thin)
// Aiming line: white box (thin, will be scaled)
var robot = new Robot();
var balls = [];
var chain = new Chain();
var aimLine = null;
var isAiming = false;
var aimStart = {
x: 0,
y: 0
};
var aimEnd = {
x: 0,
y: 0
};
var canFire = true;
var fireCooldown = 18; // frames between shots
var fireTimer = 0;
var score = 0;
var scoreTxt = null;
// --- Add robot ---
robot.x = 1024;
robot.y = 2732 - 120;
game.addChild(robot);
// After intro music ends, start main music (simulate with timeout, since no event system)
LK.setTimeout(function () {
LK.playMusic('mainMusic', {
loop: true
});
}, 4000); // Adjust 4000ms to match your intro music length
// --- Add balls ---
var ball1 = new Ball();
ball1.setType('red');
ball1.radius = 160; // enlarged radius
ball1.asset.width = ball1.radius * 2;
ball1.asset.height = ball1.radius * 2;
ball1.x = 600;
ball1.y = 800;
// Set initial velocity to make the ball start moving
ball1.vx = 18;
ball1.vy = -22;
// Only add one red ball (remove this one)
// balls.push(ball1);
// game.addChild(ball1);
// Duplicate ball1 (keep only one red ball)
var ball1b = new Ball();
ball1b.setType('red');
ball1b.radius = 160;
ball1b.asset.width = ball1b.radius * 2;
ball1b.asset.height = ball1b.radius * 2;
ball1b.x = 600 + 120; // offset to avoid overlap
ball1b.y = 800 + 120;
ball1b.vx = 18;
ball1b.vy = -22;
balls.push(ball1b);
game.addChild(ball1b);
var ball2 = new Ball();
ball2.setType('green');
ball2.radius = 160; // enlarged radius
ball2.asset.width = ball2.radius * 2;
ball2.asset.height = ball2.radius * 2;
ball2.x = 1448;
ball2.y = 900;
// Set initial velocity to make the ball start moving
ball2.vx = -16;
ball2.vy = -18;
// Only add one green ball (remove this one)
// balls.push(ball2);
// game.addChild(ball2);
// Duplicate ball2 (keep only one green ball)
var ball2b = new Ball();
ball2b.setType('green');
ball2b.radius = 160;
ball2b.asset.width = ball2b.radius * 2;
ball2b.asset.height = ball2b.radius * 2;
ball2b.x = 1448 - 120; // offset to avoid overlap
ball2b.y = 900 + 120;
ball2b.vx = -16;
ball2b.vy = -18;
balls.push(ball2b);
game.addChild(ball2b);
// --- Add chain (hidden at start) ---
game.addChild(chain);
// --- Score text ---
// Scoreboard label
var scoreboardLabel = new Text2('²H collisions scoreboard', {
size: 64,
fill: 0xFFFFFF
});
scoreboardLabel.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreboardLabel);
scoreboardLabel.y = 0;
// Score text
scoreTxt = new Text2('0', {
size: 120,
fill: 0xFFFFFF
});
scoreTxt.anchor.set(0.5, 0);
scoreTxt.y = scoreboardLabel.height + 10;
LK.gui.top.addChild(scoreTxt);
// --- Store all drawn lines globally ---
var drawnLines = [];
// --- Aiming line ---
function updateAimLine() {
if (!aimLine) {
aimLine = LK.getAsset('aimLine', {
anchorX: 0,
anchorY: 0.5
});
aimLine.alpha = 0.5;
game.addChild(aimLine);
}
var dx = aimEnd.x - aimStart.x;
var dy = aimEnd.y - aimStart.y;
var len = Math.sqrt(dx * dx + dy * dy);
if (len < 80) len = 80; // minimum length
aimLine.x = aimStart.x;
aimLine.y = aimStart.y;
aimLine.width = len;
aimLine.height = 16;
aimLine.rotation = Math.atan2(dy, dx);
aimLine.visible = true;
}
function hideAimLine() {
if (aimLine) aimLine.visible = false;
}
// --- Input handling ---
game.down = function (x, y, obj) {
// Handle aiming or ball destruction based on tap location
if (y > 2732 - 600) {
// Aiming zone (lower part of screen)
isAiming = true;
aimStart.x = robot.x;
aimStart.y = robot.y - robot.height / 2;
aimEnd.x = x;
aimEnd.y = y;
updateAimLine();
} else {
// Destruction zone (upper part of screen - our "right-click" equivalent)
// When the "right mouse button" is pressed, the game continues running and a sound plays.
LK.getSound('shoot').play();
}
};
game.move = function (x, y, obj) {
if (isAiming) {
// Update aim direction
aimEnd.x = x;
aimEnd.y = y;
updateAimLine();
} else {
// Move robot left/right only
robot.targetX = x;
}
};
game.up = function (x, y, obj) {
if (isAiming && canFire) {
// Fire chain in direction
var dx = aimEnd.x - aimStart.x;
var dy = aimEnd.y - aimStart.y;
var len = Math.sqrt(dx * dx + dy * dy);
if (len > 80) {
var dirX = dx / len;
var dirY = dy / len;
chain.clear();
chain.fire(aimStart.x, aimStart.y, dirX, dirY);
canFire = false;
fireTimer = fireCooldown;
}
// Add the drawn line to drawnLines array if it is long enough
if (len > 80) {
drawnLines.push({
x1: aimStart.x,
y1: aimStart.y,
x2: aimEnd.x,
y2: aimEnd.y
});
}
// Remove all but the most recent line (keep only the last one)
if (drawnLines.length > 1) {
drawnLines.splice(0, drawnLines.length - 1);
}
}
isAiming = false;
hideAimLine();
};
// Helper function to get distance to robot if a ball is touching it
function getDistanceToRobotIfTouching(ball, robotInstance) {
var bounds = robotInstance.getBounds();
// Closest point on robot's bounding box to ball center
var cx = Math.max(bounds.left, Math.min(ball.x, bounds.right));
var cy = Math.max(bounds.top, Math.min(ball.y, bounds.bottom));
var dxBallToRobotRect = ball.x - cx;
var dyBallToRobotRect = ball.y - cy;
var distSquared = dxBallToRobotRect * dxBallToRobotRect + dyBallToRobotRect * dyBallToRobotRect;
var dist = Math.sqrt(distSquared);
// A ball is touching if the distance from its center to the robot's rectangle
// is less than its radius.
if (dist < ball.getRadius()) {
return dist; // Return the actual distance
}
return Infinity; // Not touching
}
// --- Ball-ball collision ---
function ballsCollide(b1, b2) {
var dx = b1.x - b2.x;
var dy = b1.y - b2.y;
var dist = Math.sqrt(dx * dx + dy * dy);
return dist < b1.radius + b2.radius;
}
function resolveBallCollision(b1, b2) {
var dx = b1.x - b2.x;
var dy = b1.y - b2.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist === 0) return;
var overlap = b1.radius + b2.radius - dist;
if (overlap > 0) {
// Push balls apart
var nx = dx / dist;
var ny = dy / dist;
b1.x += nx * (overlap / 2);
b1.y += ny * (overlap / 2);
b2.x -= nx * (overlap / 2);
b2.y -= ny * (overlap / 2);
// Exchange velocities (elastic)
var tx = nx;
var ty = ny;
var v1 = b1.vx * tx + b1.vy * ty;
var v2 = b2.vx * tx + b2.vy * ty;
var m1 = b1.radius;
var m2 = b2.radius;
var newV1 = (v1 * (m1 - m2) + 2 * m2 * v2) / (m1 + m2);
var newV2 = (v2 * (m2 - m1) + 2 * m1 * v1) / (m1 + m2);
b1.vx += (newV1 - v1) * tx;
b1.vy += (newV1 - v1) * ty;
b2.vx += (newV2 - v2) * tx;
b2.vy += (newV2 - v2) * ty;
}
}
// --- Ball-robot collision ---
function ballHitsRobot(ball, robot) {
var bounds = robot.getBounds();
// Closest point on robot to ball center
var cx = Math.max(bounds.left, Math.min(ball.x, bounds.right));
var cy = Math.max(bounds.top, Math.min(ball.y, bounds.bottom));
var dx = ball.x - cx;
var dy = ball.y - cy;
var dist = Math.sqrt(dx * dx + dy * dy);
return dist < ball.radius;
}
function resolveBallRobotCollision(ball, robot) {
var bounds = robot.getBounds();
var cx = Math.max(bounds.left, Math.min(ball.x, bounds.right));
var cy = Math.max(bounds.top, Math.min(ball.y, bounds.bottom));
var dx = ball.x - cx;
var dy = ball.y - cy;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist === 0) return;
var overlap = ball.radius - dist;
if (overlap > 0) {
// Push ball out
var nx = dx / dist;
var ny = dy / dist;
ball.x += nx * overlap;
ball.y += ny * overlap;
// Reflect velocity
var dot = ball.vx * nx + ball.vy * ny;
ball.vx -= 2 * dot * nx;
ball.vy -= 2 * dot * ny;
ball.vx *= 0.9;
ball.vy *= 0.9;
}
}
// --- Chain-ball collision ---
function chainHitsBall(chain, ball) {
// Check each segment
for (var i = 0; i < chain.segments.length; ++i) {
var seg = chain.segments[i];
var dx = seg.x - ball.x;
var dy = seg.y - ball.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < ball.radius) {
return true;
}
}
return false;
}
// --- Chain stops at first collision with ball or wall ---
function updateChainCollisions() {
if (!chain.active) return;
for (var i = 0; i < balls.length; ++i) {
if (chainHitsBall(chain, balls[i])) {
var hitBall = balls[i];
// --- Chain acts as a barrier: bounce ball off chain, do not allow pass-through ---
// Find the closest segment that collides
var closestSeg = null;
var minDist = Infinity;
for (var j = 0; j < chain.segments.length; ++j) {
var seg = chain.segments[j];
var dx = seg.x - hitBall.x;
var dy = seg.y - hitBall.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < hitBall.radius && dist < minDist) {
minDist = dist;
closestSeg = seg;
}
}
if (closestSeg) {
// Push ball out of the chain segment and reflect velocity (barrier bounce)
var dx = hitBall.x - closestSeg.x;
var dy = hitBall.y - closestSeg.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist === 0) {
// Avoid division by zero, nudge
dx = 1;
dy = 0;
dist = 1;
}
var overlap = hitBall.radius - dist;
if (overlap > 0) {
var nx = dx / dist;
var ny = dy / dist;
// Move ball out of the chain
hitBall.x += nx * overlap;
hitBall.y += ny * overlap;
// --- Ball bounce off chain, just like ball-ball collision ---
// Project velocity onto normal
var dot = hitBall.vx * nx + hitBall.vy * ny;
// Reflect velocity (elastic bounce)
hitBall.vx -= 2 * dot * nx;
hitBall.vy -= 2 * dot * ny;
// Dampen to simulate energy loss (same as ball-ball)
hitBall.vx *= hitBall.bounce;
hitBall.vy *= hitBall.bounce;
}
// Do NOT deactivate the chain here; allow it to persist as a barrier (chain acts as a wall)
}
// --- Shrink and split logic ---
if (hitBall.radius > hitBall.minRadius) {
// Shrink the ball down to minRadius if possible
hitBall.shrink();
// If after shrinking, it's still above minRadius + epsilon, allow further splitting
if (hitBall.radius > hitBall.minRadius) {
// Split into two balls
var newRadius = Math.max(hitBall.radius / 1.5, hitBall.minRadius);
for (var s = 0; s < 2; ++s) {
var newBall = new Ball();
newBall.setType(hitBall.type);
newBall.radius = newRadius;
newBall.asset.width = newRadius * 2;
newBall.asset.height = newRadius * 2;
// Offset new balls slightly to avoid overlap
var angle = Math.PI / 4 + s * (Math.PI / 2);
newBall.x = hitBall.x + Math.cos(angle) * newRadius;
newBall.y = hitBall.y + Math.sin(angle) * newRadius;
// Give them some velocity away from each other
newBall.vx = hitBall.vx + (s === 0 ? -12 : 12);
newBall.vy = hitBall.vy - 10;
balls.push(newBall);
game.addChild(newBall);
}
// Remove the original ball
game.removeChild(hitBall);
balls.splice(i, 1);
i--; // adjust index after removal
}
} else {
// Ball is at minimum size, destroy it
game.removeChild(hitBall);
balls.splice(i, 1);
i--; // adjust index after removal
}
score += 1;
scoreTxt.setText(score);
// Flash ball
LK.effects.flashObject(hitBall, 0xffff00, 200);
break;
}
}
}
// --- Game update loop ---
var ballCollisionCount = 0;
var maxBallCollisions = 100;
var maxLimitTxt = null;
var winMessage = null;
var gameOverMessage = null;
var screenOverlay = LK.getAsset('screen', {
anchorX: 0,
anchorY: 0,
width: 2048,
height: 2732,
x: 0,
y: 0
});
game.addChildAt(screenOverlay, 0);
game.update = function () {
// Show max limit message if not already shown
if (!maxLimitTxt) {
maxLimitTxt = new Text2("Max limit 100", {
size: 64,
fill: 0xff0000
});
maxLimitTxt.anchor.set(0.5, 0);
maxLimitTxt.y = scoreTxt.y + scoreTxt.height + 10;
LK.gui.top.addChild(maxLimitTxt);
}
// Update robot
robot.update();
// Update balls
for (var i = 0; i < balls.length; ++i) {
balls[i].update();
// --- Enforce chain as a barrier every frame, so balls never pass through ---
if (chain.active) {
// For each segment, check collision and bounce if needed
for (var j = 0; j < chain.segments.length; ++j) {
var seg = chain.segments[j];
var dx = balls[i].x - seg.x;
var dy = balls[i].y - seg.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < balls[i].radius) {
// Push ball out of the chain segment and reflect velocity (barrier bounce)
var overlap = balls[i].radius - dist;
if (dist === 0) {
dx = 1;
dy = 0;
dist = 1;
}
var nx = dx / dist;
var ny = dy / dist;
balls[i].x += nx * overlap;
balls[i].y += ny * overlap;
// Project velocity onto normal
var dot = balls[i].vx * nx + balls[i].vy * ny;
// Reflect velocity (elastic bounce)
balls[i].vx -= 2 * dot * nx;
balls[i].vy -= 2 * dot * ny;
// Dampen to simulate energy loss (same as ball-ball)
balls[i].vx *= balls[i].bounce;
balls[i].vy *= balls[i].bounce;
}
}
}
}
// Ball-ball collision
// Check all unique pairs of balls for collision
for (var i = 0; i < balls.length; ++i) {
for (var j = i + 1; j < balls.length; ++j) {
var b1 = balls[i];
var b2 = balls[j];
// Track last and current collision state for this pair
if (b1.lastCollided === undefined) b1.lastCollided = {};
if (b2.lastCollided === undefined) b2.lastCollided = {};
var pairKey = j; // unique for (i,j)
var lastColliding = b1.lastCollided[pairKey] || false;
var nowColliding = ballsCollide(b1, b2);
// Only increment score on the exact frame the collision starts
if (!lastColliding && nowColliding) {
// Only increment score if one is red and one is green (²H and ³H)
var t1 = b1.type;
var t2 = b2.type;
var isRedGreen = t1 === 'red' && t2 === 'green' || t1 === 'green' && t2 === 'red';
if (isRedGreen) {
if (ballCollisionCount < maxBallCollisions) {
score += 1;
if (score > maxBallCollisions) score = maxBallCollisions;
scoreTxt.setText(score);
}
}
// Always resolve collision on first contact
resolveBallCollision(b1, b2);
// Increment collision counter and check for game over
if (isRedGreen && ballCollisionCount < maxBallCollisions) {
ballCollisionCount++;
}
if (ballCollisionCount >= maxBallCollisions) {
// Clamp score to 100 and update display
score = maxBallCollisions;
scoreTxt.setText(score);
// Show overlay and GAME OVER message
if (!screenOverlay) {
screenOverlay = LK.getAsset('screen', {
anchorX: 0,
anchorY: 0,
width: 2048,
height: 2732,
x: 0,
y: 0
});
game.addChildAt(screenOverlay, 0);
}
if (!gameOverMessage) {
gameOverMessage = new Text2("GAME OVER", {
size: 220,
fill: 0xff0000
});
gameOverMessage.anchor.set(0.5, 0.5);
gameOverMessage.x = 2048 / 2;
gameOverMessage.y = 2732 / 2;
game.addChild(gameOverMessage);
}
// Play outro music
LK.playMusic('outroMusic', {
loop: false
});
// Show GAME OVER popup and stop the game
LK.showGameOver();
return;
}
} else if (nowColliding) {
// If still colliding, continue to resolve collision
resolveBallCollision(b1, b2);
}
// Update last collision state for this pair
b1.lastCollided[pairKey] = nowColliding;
}
}
// Ball-robot collision
for (var i = 0; i < balls.length; ++i) {
if (ballHitsRobot(balls[i], robot)) {
resolveBallRobotCollision(balls[i], robot);
// Game over
LK.effects.flashScreen(0xff0000, 1000);
LK.showGameOver();
return;
}
}
// Update chain
if (chain.active) {
chain.update();
// --- Atom splitter logic: check for chain-ball collisions and handle split/shrink ---
for (var i = 0; i < balls.length; ++i) {
var ball = balls[i];
// Track last and current collision state for this ball
if (ball.lastChainColliding === undefined) ball.lastChainColliding = false;
var currentlyColliding = chainHitsBall(chain, ball);
// Only trigger split/shrink on the exact frame the collision starts
if (!ball.lastChainColliding && currentlyColliding) {
// Find the closest segment that collides
var closestSeg = null;
var minDist = Infinity;
for (var j = 0; j < chain.segments.length; ++j) {
var seg = chain.segments[j];
var dx = seg.x - ball.x;
var dy = seg.y - ball.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < ball.radius && dist < minDist) {
minDist = dist;
closestSeg = seg;
}
}
if (closestSeg) {
// Push ball out of the chain segment and reflect velocity (barrier bounce)
var dx = ball.x - closestSeg.x;
var dy = ball.y - closestSeg.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist === 0) {
dx = 1;
dy = 0;
dist = 1;
}
var overlap = ball.radius - dist;
if (overlap > 0) {
var nx = dx / dist;
var ny = dy / dist;
ball.x += nx * overlap;
ball.y += ny * overlap;
// Project velocity onto normal
var dot = ball.vx * nx + ball.vy * ny;
// Reflect velocity (elastic bounce)
ball.vx -= 2 * dot * nx;
ball.vy -= 2 * dot * ny;
// Dampen to simulate energy loss (same as ball-ball)
ball.vx *= ball.bounce;
ball.vy *= ball.bounce;
}
}
// --- Shrink and split logic ---
if (ball.radius > ball.minRadius) {
// Shrink the ball down to minRadius if possible
ball.shrink();
// If after shrinking, it's still above minRadius + epsilon, allow further splitting
if (ball.radius > ball.minRadius) {
// Split into two balls
var newRadius = Math.max(ball.radius / 1.5, ball.minRadius);
for (var s = 0; s < 2; ++s) {
var newBall = new Ball();
newBall.setType(ball.type);
newBall.radius = newRadius;
newBall.asset.width = newRadius * 2;
newBall.asset.height = newRadius * 2;
// Offset new balls slightly to avoid overlap
var angle = Math.PI / 4 + s * (Math.PI / 2);
newBall.x = ball.x + Math.cos(angle) * newRadius;
newBall.y = ball.y + Math.sin(angle) * newRadius;
// Give them some velocity away from each other
newBall.vx = ball.vx + (s === 0 ? -12 : 12);
newBall.vy = ball.vy - 10;
balls.push(newBall);
game.addChild(newBall);
}
// Remove the original ball
game.removeChild(ball);
balls.splice(i, 1);
i--; // adjust index after removal
}
} else {
// Ball is at minimum size, destroy it
game.removeChild(ball);
balls.splice(i, 1);
i--; // adjust index after removal
}
score += 1;
scoreTxt.setText(score);
// Flash ball
LK.effects.flashObject(ball, 0xffff00, 200);
}
// Update last collision state
ball.lastChainColliding = currentlyColliding;
}
}
// Fire cooldown
if (!canFire) {
fireTimer--;
if (fireTimer <= 0) {
canFire = true;
}
}
// WIN condition: if no balls left, show YOU WIN!
if (balls.length === 0) {
if (!screenOverlay) {
screenOverlay = LK.getAsset('screen', {
anchorX: 0,
anchorY: 0,
width: 2048,
height: 2732,
x: 0,
y: 0
});
game.addChildAt(screenOverlay, 0);
}
if (!winMessage) {
winMessage = new Text2("YOU WIN!", {
size: 220,
fill: 0x00ff00
});
winMessage.anchor.set(0.5, 0.5);
winMessage.x = 2048 / 2;
winMessage.y = 2732 / 2;
game.addChild(winMessage);
// Play outro music
LK.playMusic('outroMusic', {
loop: false
});
}
return;
}
};