/**** * 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;
}
};