/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ // Balloon class var Balloon = Container.expand(function () { var self = Container.call(this); // Store color for logic self.color = self.color || 0xff0000; // Default red, will be overwritten on creation // Store grid position self.gridX = 0; self.gridY = 0; // Is this balloon popped? self.popped = false; // Attach balloon asset (ellipse shape) - will be replaced in buildGrid with correct color var balloonGraphics = self.attachAsset('balloon_red', { anchorX: 0.5, anchorY: 0.5 }); // Pop animation and logic self.pop = function () { if (self.popped) return; self.popped = true; // Animate scale down and fade out tween(self, { scaleX: 0, scaleY: 0, alpha: 0 }, { duration: 250, easing: tween.easeIn, onFinish: function onFinish() { self.destroy(); } }); }; // Down event (touch/click) self.down = function (x, y, obj) { if (self.popped) return; // Only allow popping if this is the current color // Always compare as integer values to avoid color type mismatch if (parseInt(self.color, 10) !== parseInt(currentColor, 10)) return; // Find all connected balloons of the same color (side-by-side or vertical) var group = findConnectedBalloons(self.gridX, self.gridY, self.color); // Only pop if at least 2 connected (including self) if (group.length < 2) return; // Push current state to undo stack before popping undoStack.push(cloneGameState()); for (var i = 0; i < group.length; i++) { if (!group[i].popped) { group[i].pop(); } } // Update score (100 points per balloon) LK.setScore(LK.getScore() + group.length * 100); scoreTxt.setText(LK.getScore()); // After popping, let balloons above fall down to fill empty spaces LK.setTimeout(function () { for (var x = 0; x < GRID_COLS; x++) { // For each column, go from bottom to top for (var y = GRID_ROWS - 1; y >= 0; y--) { if (!balloons[x][y] || balloons[x][y].popped) { // Find the nearest non-popped balloon above for (var aboveY = y - 1; aboveY >= 0; aboveY--) { if (balloons[x][aboveY] && !balloons[x][aboveY].popped) { // Move this balloon down var b = balloons[x][aboveY]; // Update grid balloons[x][y] = b; balloons[x][aboveY] = null; b.gridY = y; // Animate drop var targetY = balloons[x][y].y = Math.floor((2732 - GRID_ROWS * balloonSize) / 2) + balloonSize / 2 + y * (balloonSize + balloonSpacing); tween(b, { y: targetY }, { duration: 200, easing: tween.easeIn }); break; } } } } } // After all drops, fill empty spots at the top with null (no new balloons) for (var x = 0; x < GRID_COLS; x++) { for (var y = 0; y < GRID_ROWS; y++) { if (!balloons[x][y] || balloons[x][y] && balloons[x][y].popped) { balloons[x][y] = null; } } } }, 260); // Wait for pop animation to finish }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0xffffff }); /**** * Game Code ****/ // Balloon grid settings var GRID_COLS = 10; var GRID_ROWS = 12; var BALLOON_COLORS = [0xff4d4d, // red 0x4db8ff, // blue 0x4dff4d, // green 0xffe14d, // yellow 0xff4df7 // pink ]; var BALLOON_COLOR_NAMES = ['red', 'blue', 'green', 'yellow', 'pink']; // Balloon size and spacing var balloonSize = Math.floor(2048 / GRID_COLS); var balloonSpacing = 0; // No gap // Store balloons in a 2D array var balloons = []; // Store all balloon objects for easy iteration var allBalloons = []; // Current color to pop var currentColor = null; var currentColorIdx = 0; // Score text var scoreTxt = new Text2('0', { size: 120, fill: 0x222222 }); // Score text at top left (leave 110px margin for menu icon) scoreTxt.anchor.set(0, 0); scoreTxt.x = 110; scoreTxt.y = 40; LK.gui.topLeft.addChild(scoreTxt); // Timer text at top center (always visible, in LK.gui.top) var timerTxt = new Text2('Time: 0s', { size: 90, fill: 0x222222 }); timerTxt.anchor.set(0.5, 0); timerTxt.x = 2048 / 2; timerTxt.y = 40; LK.gui.top.addChild(timerTxt); // Color indicator balloon and text (always visible, in LK.gui.topRight) var colorIndicatorBalloon = null; var colorIndicatorText = null; // Add poppable color name text at bottom right (English, always visible) var colorNameTopRightText = new Text2('', { size: 90, fill: 0x222222, font: "'GillSans-Bold',Impact,'Arial Black',Tahoma" }); // Anchor at right bottom colorNameTopRightText.anchor.set(1, 1); // Position at bottom right, 40px from the bottom edge and 40px from the right edge colorNameTopRightText.x = 2048 - 40; colorNameTopRightText.y = 2732 - 40; LK.gui.bottom.addChild(colorNameTopRightText); // (Removed popColorMainText at top center) // Background pulse tween state var bgPulseTween = null; var bgPulseState = { t: 0 }; // t: 0 (white), t: 1 (color) function setColorIndicator(color) { // Remove previous indicator if exists if (colorIndicatorBalloon) { colorIndicatorBalloon.destroy(); colorIndicatorBalloon = null; } // Remove previous color name text if exists if (colorIndicatorText) { colorIndicatorText.destroy(); colorIndicatorText = null; } // Find color index and name var idx = -1; for (var i = 0; i < BALLOON_COLORS.length; i++) { if (parseInt(BALLOON_COLORS[i], 10) === parseInt(color, 10)) { idx = i; break; } } if (idx === -1) return; var name = BALLOON_COLOR_NAMES[idx]; // Create indicator balloon colorIndicatorBalloon = LK.getAsset('balloon_' + name, { anchorX: 1, anchorY: 0, x: 2048 - 40, y: 40, scaleX: 1.2, scaleY: 1.2 }); // Add to GUI top right (not game, so it stays on top) LK.gui.topRight.addChild(colorIndicatorBalloon); // Create and add color name text colorIndicatorText = new Text2(name.toUpperCase(), { size: 90, fill: color, font: "'GillSans-Bold',Impact,'Arial Black',Tahoma" }); colorIndicatorText.anchor.set(1, 0); // Position text at top right, aligned with the balloon colorIndicatorText.x = 2048 - 40; colorIndicatorText.y = 40; colorIndicatorText.setStyle({ fill: color }); // Ensure text is in the actual color LK.gui.topRight.addChild(colorIndicatorText); // (Removed update of popColorMainText at top center) // Update the bottom left color name text (English, capitalized) and set its color to the actual color if (typeof colorNameTopRightText !== "undefined" && colorNameTopRightText) { colorNameTopRightText.setText(name.charAt(0).toUpperCase() + name.slice(1)); colorNameTopRightText.setStyle({ fill: color }); } // Animate pulse (fade in/out forever) function pulseIndicator() { if (!colorIndicatorBalloon) return; colorIndicatorBalloon.alpha = 1; tween(colorIndicatorBalloon, { alpha: 0.3 }, { duration: 400, easing: tween.easeIn, onFinish: function onFinish() { if (!colorIndicatorBalloon) return; tween(colorIndicatorBalloon, { alpha: 1 }, { duration: 400, easing: tween.easeOut, onFinish: function onFinish() { pulseIndicator(); } }); } }); } pulseIndicator(); // Update all frame sides to match the poppable color updateFrameColor(color); // No background color pulsing; keep background static } // Helper: find all connected balloons of the same color (DFS, only side-by-side or vertical) function findConnectedBalloons(x, y, color) { var visited = {}; var group = []; function dfs(cx, cy) { var key = cx + ',' + cy; if (visited[key]) return; if (cx < 0 || cy < 0 || cx >= GRID_COLS || cy >= GRID_ROWS) return; var b = balloons[cx][cy]; if (!b || b.popped || parseInt(b.color, 10) !== parseInt(color, 10)) return; visited[key] = true; group.push(b); // Check neighbors (up, down, left, right) - no diagonal dfs(cx - 1, cy); // left dfs(cx + 1, cy); // right dfs(cx, cy - 1); // up dfs(cx, cy + 1); // down } dfs(x, y); return group; } // Helper: pick a new color (random, but must exist on board) function pickNewColor() { // Find all colors still present, and count how many of each var present = {}; for (var i = 0; i < allBalloons.length; i++) { var b = allBalloons[i]; if (!b.popped) { if (!present[b.color]) present[b.color] = 0; present[b.color]++; } } var presentColors = []; for (var i = 0; i < BALLOON_COLORS.length; i++) { if (present[BALLOON_COLORS[i]] && present[BALLOON_COLORS[i]] > 0) presentColors.push(BALLOON_COLORS[i]); } if (presentColors.length === 0) { // Win condition: all balloons popped LK.showYouWin(); return; } // Pick random color var idx = Math.floor(Math.random() * presentColors.length); currentColor = presentColors[idx]; setColorIndicator(currentColor); } // Build the balloon grid function buildGrid() { var offsetX = Math.floor((2048 - GRID_COLS * balloonSize) / 2) + balloonSize / 2; var offsetY = Math.floor((2732 - GRID_ROWS * balloonSize) / 2) + balloonSize / 2; balloons = []; allBalloons = []; for (var x = 0; x < GRID_COLS; x++) { balloons[x] = []; for (var y = 0; y < GRID_ROWS; y++) { // Pick random color var colorIdx = Math.floor(Math.random() * BALLOON_COLORS.length); var color = BALLOON_COLORS[colorIdx]; var name = BALLOON_COLOR_NAMES[colorIdx]; var balloon = new Balloon(); balloon.color = color; balloon.gridX = x; balloon.gridY = y; // Set asset to correct color balloon.removeChildren(); balloon.attachAsset('balloon_' + name, { anchorX: 0.5, anchorY: 0.5 }); balloon.x = offsetX + x * (balloonSize + balloonSpacing); balloon.y = offsetY + y * (balloonSize + balloonSpacing); balloons[x][y] = balloon; allBalloons.push(balloon); game.addChild(balloon); } } } // Reset game state function resetGame() { LK.setScore(0); scoreTxt.setText('0'); // Remove all balloons for (var i = 0; i < allBalloons.length; i++) { allBalloons[i].destroy(); } buildGrid(); pickNewColor(); startElapsedTimer(); } // Timer for color change var colorTimer = null; function startColorTimer() { if (colorTimer) LK.clearInterval(colorTimer); colorTimer = LK.setInterval(function () { pickNewColor(); }, 3000); } // Timer for elapsed seconds var elapsedSeconds = 0; var elapsedTimer = null; var timerTxt = new Text2('Time: 0s', { size: 90, fill: 0x222222 }); // timerTxt is now positioned in gui.top in the correct place above function startElapsedTimer() { if (elapsedTimer) LK.clearInterval(elapsedTimer); elapsedSeconds = 0; timerTxt.setText('Time: 0s'); elapsedTimer = LK.setInterval(function () { elapsedSeconds++; timerTxt.setText('Time: ' + elapsedSeconds + 's'); }, 1000); } function stopElapsedTimer() { if (elapsedTimer) { LK.clearInterval(elapsedTimer); elapsedTimer = null; } } // --- Draw four static rectangle frame sides around the game area --- // Frame settings var frameThickness = 24; var frameColor = 0x222222; // Top frame var frameTop = LK.getAsset('frame_top', { width: 2048, height: frameThickness, color: frameColor, shape: 'box', anchorX: 0, anchorY: 0 }); frameTop.x = 0; frameTop.y = 0; game.addChild(frameTop); // Bottom frame var frameBottom = LK.getAsset('frame_bottom', { width: 2048, height: frameThickness, color: frameColor, shape: 'box', anchorX: 0, anchorY: 0 }); frameBottom.x = 0; frameBottom.y = 2732 - frameThickness; game.addChild(frameBottom); // Left frame var frameLeft = LK.getAsset('frame_left', { width: frameThickness, height: 2732, color: frameColor, shape: 'box', anchorX: 0, anchorY: 0 }); frameLeft.x = 0; frameLeft.y = 0; game.addChild(frameLeft); // Right frame var frameRight = LK.getAsset('frame_right', { width: frameThickness, height: 2732, color: frameColor, shape: 'box', anchorX: 0, anchorY: 0 }); frameRight.x = 2048 - frameThickness; frameRight.y = 0; game.addChild(frameRight); // Helper to update frame color (all four sides) function updateFrameColor(color) { if (frameTop && frameTop.setStyle) frameTop.setStyle({ fill: color }); if (frameBottom && frameBottom.setStyle) frameBottom.setStyle({ fill: color }); if (frameLeft && frameLeft.setStyle) frameLeft.setStyle({ fill: color }); if (frameRight && frameRight.setStyle) frameRight.setStyle({ fill: color }); if (frameTop && frameTop.tint !== undefined) frameTop.tint = color; if (frameBottom && frameBottom.tint !== undefined) frameBottom.tint = color; if (frameLeft && frameLeft.tint !== undefined) frameLeft.tint = color; if (frameRight && frameRight.tint !== undefined) frameRight.tint = color; if (frameTop) frameTop.color = color; if (frameBottom) frameBottom.color = color; if (frameLeft) frameLeft.color = color; if (frameRight) frameRight.color = color; } // Start game resetGame(); startColorTimer(); // Game update (not much needed) game.update = function () { // Check win condition var remaining = 0; for (var i = 0; i < allBalloons.length; i++) { if (!allBalloons[i].popped) remaining++; } if (remaining === 0) { stopElapsedTimer(); // Calculate score per second (100 points per balloon) var totalScore = LK.getScore(); var seconds = elapsedSeconds > 0 ? elapsedSeconds : 1; var scorePerSecond = Math.round(totalScore / seconds * 100) / 100; LK.showYouWin('Score: ' + totalScore + '\nTime: ' + seconds + 's\nScore/s: ' + scorePerSecond); return; } // Check for at least one pair of adjacent same-color balloons (horizontal or vertical) var foundPair = false; for (var x = 0; x < GRID_COLS; x++) { for (var y = 0; y < GRID_ROWS; y++) { var b = balloons[x][y]; if (!b || b.popped) continue; // Check right neighbor if (x < GRID_COLS - 1) { var bRight = balloons[x + 1][y]; if (bRight && !bRight.popped && parseInt(b.color, 10) === parseInt(bRight.color, 10)) { foundPair = true; break; } } // Check down neighbor if (y < GRID_ROWS - 1) { var bDown = balloons[x][y + 1]; if (bDown && !bDown.popped && parseInt(b.color, 10) === parseInt(bDown.color, 10)) { foundPair = true; break; } } } if (foundPair) break; } // If no pair found, game over if (!foundPair) { stopElapsedTimer(); // Calculate score per second (100 points per balloon) var totalScore = LK.getScore(); var seconds = elapsedSeconds > 0 ? elapsedSeconds : 1; var scorePerSecond = Math.round(totalScore / seconds * 100) / 100; LK.showGameOver('Score: ' + totalScore + '\nTime: ' + seconds + 's\nScore/s: ' + scorePerSecond); } }; // Undo stack for game state var undoStack = []; function cloneGameState() { // Deep clone balloons and allBalloons arrays, and score var balloonsClone = []; for (var x = 0; x < GRID_COLS; x++) { balloonsClone[x] = []; for (var y = 0; y < GRID_ROWS; y++) { var b = balloons[x][y]; if (b) { balloonsClone[x][y] = { color: b.color, gridX: b.gridX, gridY: b.gridY, popped: b.popped }; } else { balloonsClone[x][y] = null; } } } var allBalloonsClone = []; for (var i = 0; i < allBalloons.length; i++) { var b = allBalloons[i]; allBalloonsClone.push({ color: b.color, gridX: b.gridX, gridY: b.gridY, popped: b.popped }); } return { balloons: balloonsClone, allBalloons: allBalloonsClone, score: LK.getScore() }; } function restoreGameState(state) { // Remove all balloons for (var i = 0; i < allBalloons.length; i++) { allBalloons[i].destroy(); } // Restore balloons and allBalloons balloons = []; allBalloons = []; var offsetX = Math.floor((2048 - GRID_COLS * balloonSize) / 2) + balloonSize / 2; var offsetY = Math.floor((2732 - GRID_ROWS * balloonSize) / 2) + balloonSize / 2; for (var x = 0; x < GRID_COLS; x++) { balloons[x] = []; for (var y = 0; y < GRID_ROWS; y++) { var bData = state.balloons[x][y]; if (bData) { var colorIdx = BALLOON_COLORS.indexOf(bData.color); var name = BALLOON_COLOR_NAMES[colorIdx]; var balloon = new Balloon(); balloon.color = bData.color; balloon.gridX = bData.gridX; balloon.gridY = bData.gridY; balloon.popped = bData.popped; balloon.removeChildren(); balloon.attachAsset('balloon_' + name, { anchorX: 0.5, anchorY: 0.5 }); balloon.x = offsetX + x * (balloonSize + balloonSpacing); balloon.y = offsetY + y * (balloonSize + balloonSpacing); if (!balloon.popped) { game.addChild(balloon); } balloons[x][y] = balloon; allBalloons.push(balloon); } else { balloons[x][y] = null; } } } LK.setScore(state.score); scoreTxt.setText(state.score); } function undoLastAction() { if (undoStack.length > 0) { var prevState = undoStack.pop(); restoreGameState(prevState); } } // Undo button at top center var undoBtn = new Text2('Undo', { size: 90, fill: 0x222222, font: "'GillSans-Bold',Impact,'Arial Black',Tahoma" }); undoBtn.anchor.set(0.5, 0); undoBtn.x = 2048 / 2; undoBtn.y = 140; undoBtn.interactive = true; undoBtn.buttonMode = true; undoBtn.down = function () { undoLastAction(); }; LK.gui.top.addChild(undoBtn); // On game over or win, reset game game.onGameOver = function () { resetGame(); startColorTimer(); startElapsedTimer(); }; game.onYouWin = function () { resetGame(); startColorTimer(); startElapsedTimer(); };
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Balloon class
var Balloon = Container.expand(function () {
var self = Container.call(this);
// Store color for logic
self.color = self.color || 0xff0000; // Default red, will be overwritten on creation
// Store grid position
self.gridX = 0;
self.gridY = 0;
// Is this balloon popped?
self.popped = false;
// Attach balloon asset (ellipse shape) - will be replaced in buildGrid with correct color
var balloonGraphics = self.attachAsset('balloon_red', {
anchorX: 0.5,
anchorY: 0.5
});
// Pop animation and logic
self.pop = function () {
if (self.popped) return;
self.popped = true;
// Animate scale down and fade out
tween(self, {
scaleX: 0,
scaleY: 0,
alpha: 0
}, {
duration: 250,
easing: tween.easeIn,
onFinish: function onFinish() {
self.destroy();
}
});
};
// Down event (touch/click)
self.down = function (x, y, obj) {
if (self.popped) return;
// Only allow popping if this is the current color
// Always compare as integer values to avoid color type mismatch
if (parseInt(self.color, 10) !== parseInt(currentColor, 10)) return;
// Find all connected balloons of the same color (side-by-side or vertical)
var group = findConnectedBalloons(self.gridX, self.gridY, self.color);
// Only pop if at least 2 connected (including self)
if (group.length < 2) return;
// Push current state to undo stack before popping
undoStack.push(cloneGameState());
for (var i = 0; i < group.length; i++) {
if (!group[i].popped) {
group[i].pop();
}
}
// Update score (100 points per balloon)
LK.setScore(LK.getScore() + group.length * 100);
scoreTxt.setText(LK.getScore());
// After popping, let balloons above fall down to fill empty spaces
LK.setTimeout(function () {
for (var x = 0; x < GRID_COLS; x++) {
// For each column, go from bottom to top
for (var y = GRID_ROWS - 1; y >= 0; y--) {
if (!balloons[x][y] || balloons[x][y].popped) {
// Find the nearest non-popped balloon above
for (var aboveY = y - 1; aboveY >= 0; aboveY--) {
if (balloons[x][aboveY] && !balloons[x][aboveY].popped) {
// Move this balloon down
var b = balloons[x][aboveY];
// Update grid
balloons[x][y] = b;
balloons[x][aboveY] = null;
b.gridY = y;
// Animate drop
var targetY = balloons[x][y].y = Math.floor((2732 - GRID_ROWS * balloonSize) / 2) + balloonSize / 2 + y * (balloonSize + balloonSpacing);
tween(b, {
y: targetY
}, {
duration: 200,
easing: tween.easeIn
});
break;
}
}
}
}
}
// After all drops, fill empty spots at the top with null (no new balloons)
for (var x = 0; x < GRID_COLS; x++) {
for (var y = 0; y < GRID_ROWS; y++) {
if (!balloons[x][y] || balloons[x][y] && balloons[x][y].popped) {
balloons[x][y] = null;
}
}
}
}, 260); // Wait for pop animation to finish
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0xffffff
});
/****
* Game Code
****/
// Balloon grid settings
var GRID_COLS = 10;
var GRID_ROWS = 12;
var BALLOON_COLORS = [0xff4d4d,
// red
0x4db8ff,
// blue
0x4dff4d,
// green
0xffe14d,
// yellow
0xff4df7 // pink
];
var BALLOON_COLOR_NAMES = ['red', 'blue', 'green', 'yellow', 'pink'];
// Balloon size and spacing
var balloonSize = Math.floor(2048 / GRID_COLS);
var balloonSpacing = 0; // No gap
// Store balloons in a 2D array
var balloons = [];
// Store all balloon objects for easy iteration
var allBalloons = [];
// Current color to pop
var currentColor = null;
var currentColorIdx = 0;
// Score text
var scoreTxt = new Text2('0', {
size: 120,
fill: 0x222222
});
// Score text at top left (leave 110px margin for menu icon)
scoreTxt.anchor.set(0, 0);
scoreTxt.x = 110;
scoreTxt.y = 40;
LK.gui.topLeft.addChild(scoreTxt);
// Timer text at top center (always visible, in LK.gui.top)
var timerTxt = new Text2('Time: 0s', {
size: 90,
fill: 0x222222
});
timerTxt.anchor.set(0.5, 0);
timerTxt.x = 2048 / 2;
timerTxt.y = 40;
LK.gui.top.addChild(timerTxt);
// Color indicator balloon and text (always visible, in LK.gui.topRight)
var colorIndicatorBalloon = null;
var colorIndicatorText = null;
// Add poppable color name text at bottom right (English, always visible)
var colorNameTopRightText = new Text2('', {
size: 90,
fill: 0x222222,
font: "'GillSans-Bold',Impact,'Arial Black',Tahoma"
});
// Anchor at right bottom
colorNameTopRightText.anchor.set(1, 1);
// Position at bottom right, 40px from the bottom edge and 40px from the right edge
colorNameTopRightText.x = 2048 - 40;
colorNameTopRightText.y = 2732 - 40;
LK.gui.bottom.addChild(colorNameTopRightText);
// (Removed popColorMainText at top center)
// Background pulse tween state
var bgPulseTween = null;
var bgPulseState = {
t: 0
}; // t: 0 (white), t: 1 (color)
function setColorIndicator(color) {
// Remove previous indicator if exists
if (colorIndicatorBalloon) {
colorIndicatorBalloon.destroy();
colorIndicatorBalloon = null;
}
// Remove previous color name text if exists
if (colorIndicatorText) {
colorIndicatorText.destroy();
colorIndicatorText = null;
}
// Find color index and name
var idx = -1;
for (var i = 0; i < BALLOON_COLORS.length; i++) {
if (parseInt(BALLOON_COLORS[i], 10) === parseInt(color, 10)) {
idx = i;
break;
}
}
if (idx === -1) return;
var name = BALLOON_COLOR_NAMES[idx];
// Create indicator balloon
colorIndicatorBalloon = LK.getAsset('balloon_' + name, {
anchorX: 1,
anchorY: 0,
x: 2048 - 40,
y: 40,
scaleX: 1.2,
scaleY: 1.2
});
// Add to GUI top right (not game, so it stays on top)
LK.gui.topRight.addChild(colorIndicatorBalloon);
// Create and add color name text
colorIndicatorText = new Text2(name.toUpperCase(), {
size: 90,
fill: color,
font: "'GillSans-Bold',Impact,'Arial Black',Tahoma"
});
colorIndicatorText.anchor.set(1, 0);
// Position text at top right, aligned with the balloon
colorIndicatorText.x = 2048 - 40;
colorIndicatorText.y = 40;
colorIndicatorText.setStyle({
fill: color
}); // Ensure text is in the actual color
LK.gui.topRight.addChild(colorIndicatorText);
// (Removed update of popColorMainText at top center)
// Update the bottom left color name text (English, capitalized) and set its color to the actual color
if (typeof colorNameTopRightText !== "undefined" && colorNameTopRightText) {
colorNameTopRightText.setText(name.charAt(0).toUpperCase() + name.slice(1));
colorNameTopRightText.setStyle({
fill: color
});
}
// Animate pulse (fade in/out forever)
function pulseIndicator() {
if (!colorIndicatorBalloon) return;
colorIndicatorBalloon.alpha = 1;
tween(colorIndicatorBalloon, {
alpha: 0.3
}, {
duration: 400,
easing: tween.easeIn,
onFinish: function onFinish() {
if (!colorIndicatorBalloon) return;
tween(colorIndicatorBalloon, {
alpha: 1
}, {
duration: 400,
easing: tween.easeOut,
onFinish: function onFinish() {
pulseIndicator();
}
});
}
});
}
pulseIndicator();
// Update all frame sides to match the poppable color
updateFrameColor(color);
// No background color pulsing; keep background static
}
// Helper: find all connected balloons of the same color (DFS, only side-by-side or vertical)
function findConnectedBalloons(x, y, color) {
var visited = {};
var group = [];
function dfs(cx, cy) {
var key = cx + ',' + cy;
if (visited[key]) return;
if (cx < 0 || cy < 0 || cx >= GRID_COLS || cy >= GRID_ROWS) return;
var b = balloons[cx][cy];
if (!b || b.popped || parseInt(b.color, 10) !== parseInt(color, 10)) return;
visited[key] = true;
group.push(b);
// Check neighbors (up, down, left, right) - no diagonal
dfs(cx - 1, cy); // left
dfs(cx + 1, cy); // right
dfs(cx, cy - 1); // up
dfs(cx, cy + 1); // down
}
dfs(x, y);
return group;
}
// Helper: pick a new color (random, but must exist on board)
function pickNewColor() {
// Find all colors still present, and count how many of each
var present = {};
for (var i = 0; i < allBalloons.length; i++) {
var b = allBalloons[i];
if (!b.popped) {
if (!present[b.color]) present[b.color] = 0;
present[b.color]++;
}
}
var presentColors = [];
for (var i = 0; i < BALLOON_COLORS.length; i++) {
if (present[BALLOON_COLORS[i]] && present[BALLOON_COLORS[i]] > 0) presentColors.push(BALLOON_COLORS[i]);
}
if (presentColors.length === 0) {
// Win condition: all balloons popped
LK.showYouWin();
return;
}
// Pick random color
var idx = Math.floor(Math.random() * presentColors.length);
currentColor = presentColors[idx];
setColorIndicator(currentColor);
}
// Build the balloon grid
function buildGrid() {
var offsetX = Math.floor((2048 - GRID_COLS * balloonSize) / 2) + balloonSize / 2;
var offsetY = Math.floor((2732 - GRID_ROWS * balloonSize) / 2) + balloonSize / 2;
balloons = [];
allBalloons = [];
for (var x = 0; x < GRID_COLS; x++) {
balloons[x] = [];
for (var y = 0; y < GRID_ROWS; y++) {
// Pick random color
var colorIdx = Math.floor(Math.random() * BALLOON_COLORS.length);
var color = BALLOON_COLORS[colorIdx];
var name = BALLOON_COLOR_NAMES[colorIdx];
var balloon = new Balloon();
balloon.color = color;
balloon.gridX = x;
balloon.gridY = y;
// Set asset to correct color
balloon.removeChildren();
balloon.attachAsset('balloon_' + name, {
anchorX: 0.5,
anchorY: 0.5
});
balloon.x = offsetX + x * (balloonSize + balloonSpacing);
balloon.y = offsetY + y * (balloonSize + balloonSpacing);
balloons[x][y] = balloon;
allBalloons.push(balloon);
game.addChild(balloon);
}
}
}
// Reset game state
function resetGame() {
LK.setScore(0);
scoreTxt.setText('0');
// Remove all balloons
for (var i = 0; i < allBalloons.length; i++) {
allBalloons[i].destroy();
}
buildGrid();
pickNewColor();
startElapsedTimer();
}
// Timer for color change
var colorTimer = null;
function startColorTimer() {
if (colorTimer) LK.clearInterval(colorTimer);
colorTimer = LK.setInterval(function () {
pickNewColor();
}, 3000);
}
// Timer for elapsed seconds
var elapsedSeconds = 0;
var elapsedTimer = null;
var timerTxt = new Text2('Time: 0s', {
size: 90,
fill: 0x222222
});
// timerTxt is now positioned in gui.top in the correct place above
function startElapsedTimer() {
if (elapsedTimer) LK.clearInterval(elapsedTimer);
elapsedSeconds = 0;
timerTxt.setText('Time: 0s');
elapsedTimer = LK.setInterval(function () {
elapsedSeconds++;
timerTxt.setText('Time: ' + elapsedSeconds + 's');
}, 1000);
}
function stopElapsedTimer() {
if (elapsedTimer) {
LK.clearInterval(elapsedTimer);
elapsedTimer = null;
}
}
// --- Draw four static rectangle frame sides around the game area ---
// Frame settings
var frameThickness = 24;
var frameColor = 0x222222;
// Top frame
var frameTop = LK.getAsset('frame_top', {
width: 2048,
height: frameThickness,
color: frameColor,
shape: 'box',
anchorX: 0,
anchorY: 0
});
frameTop.x = 0;
frameTop.y = 0;
game.addChild(frameTop);
// Bottom frame
var frameBottom = LK.getAsset('frame_bottom', {
width: 2048,
height: frameThickness,
color: frameColor,
shape: 'box',
anchorX: 0,
anchorY: 0
});
frameBottom.x = 0;
frameBottom.y = 2732 - frameThickness;
game.addChild(frameBottom);
// Left frame
var frameLeft = LK.getAsset('frame_left', {
width: frameThickness,
height: 2732,
color: frameColor,
shape: 'box',
anchorX: 0,
anchorY: 0
});
frameLeft.x = 0;
frameLeft.y = 0;
game.addChild(frameLeft);
// Right frame
var frameRight = LK.getAsset('frame_right', {
width: frameThickness,
height: 2732,
color: frameColor,
shape: 'box',
anchorX: 0,
anchorY: 0
});
frameRight.x = 2048 - frameThickness;
frameRight.y = 0;
game.addChild(frameRight);
// Helper to update frame color (all four sides)
function updateFrameColor(color) {
if (frameTop && frameTop.setStyle) frameTop.setStyle({
fill: color
});
if (frameBottom && frameBottom.setStyle) frameBottom.setStyle({
fill: color
});
if (frameLeft && frameLeft.setStyle) frameLeft.setStyle({
fill: color
});
if (frameRight && frameRight.setStyle) frameRight.setStyle({
fill: color
});
if (frameTop && frameTop.tint !== undefined) frameTop.tint = color;
if (frameBottom && frameBottom.tint !== undefined) frameBottom.tint = color;
if (frameLeft && frameLeft.tint !== undefined) frameLeft.tint = color;
if (frameRight && frameRight.tint !== undefined) frameRight.tint = color;
if (frameTop) frameTop.color = color;
if (frameBottom) frameBottom.color = color;
if (frameLeft) frameLeft.color = color;
if (frameRight) frameRight.color = color;
}
// Start game
resetGame();
startColorTimer();
// Game update (not much needed)
game.update = function () {
// Check win condition
var remaining = 0;
for (var i = 0; i < allBalloons.length; i++) {
if (!allBalloons[i].popped) remaining++;
}
if (remaining === 0) {
stopElapsedTimer();
// Calculate score per second (100 points per balloon)
var totalScore = LK.getScore();
var seconds = elapsedSeconds > 0 ? elapsedSeconds : 1;
var scorePerSecond = Math.round(totalScore / seconds * 100) / 100;
LK.showYouWin('Score: ' + totalScore + '\nTime: ' + seconds + 's\nScore/s: ' + scorePerSecond);
return;
}
// Check for at least one pair of adjacent same-color balloons (horizontal or vertical)
var foundPair = false;
for (var x = 0; x < GRID_COLS; x++) {
for (var y = 0; y < GRID_ROWS; y++) {
var b = balloons[x][y];
if (!b || b.popped) continue;
// Check right neighbor
if (x < GRID_COLS - 1) {
var bRight = balloons[x + 1][y];
if (bRight && !bRight.popped && parseInt(b.color, 10) === parseInt(bRight.color, 10)) {
foundPair = true;
break;
}
}
// Check down neighbor
if (y < GRID_ROWS - 1) {
var bDown = balloons[x][y + 1];
if (bDown && !bDown.popped && parseInt(b.color, 10) === parseInt(bDown.color, 10)) {
foundPair = true;
break;
}
}
}
if (foundPair) break;
}
// If no pair found, game over
if (!foundPair) {
stopElapsedTimer();
// Calculate score per second (100 points per balloon)
var totalScore = LK.getScore();
var seconds = elapsedSeconds > 0 ? elapsedSeconds : 1;
var scorePerSecond = Math.round(totalScore / seconds * 100) / 100;
LK.showGameOver('Score: ' + totalScore + '\nTime: ' + seconds + 's\nScore/s: ' + scorePerSecond);
}
};
// Undo stack for game state
var undoStack = [];
function cloneGameState() {
// Deep clone balloons and allBalloons arrays, and score
var balloonsClone = [];
for (var x = 0; x < GRID_COLS; x++) {
balloonsClone[x] = [];
for (var y = 0; y < GRID_ROWS; y++) {
var b = balloons[x][y];
if (b) {
balloonsClone[x][y] = {
color: b.color,
gridX: b.gridX,
gridY: b.gridY,
popped: b.popped
};
} else {
balloonsClone[x][y] = null;
}
}
}
var allBalloonsClone = [];
for (var i = 0; i < allBalloons.length; i++) {
var b = allBalloons[i];
allBalloonsClone.push({
color: b.color,
gridX: b.gridX,
gridY: b.gridY,
popped: b.popped
});
}
return {
balloons: balloonsClone,
allBalloons: allBalloonsClone,
score: LK.getScore()
};
}
function restoreGameState(state) {
// Remove all balloons
for (var i = 0; i < allBalloons.length; i++) {
allBalloons[i].destroy();
}
// Restore balloons and allBalloons
balloons = [];
allBalloons = [];
var offsetX = Math.floor((2048 - GRID_COLS * balloonSize) / 2) + balloonSize / 2;
var offsetY = Math.floor((2732 - GRID_ROWS * balloonSize) / 2) + balloonSize / 2;
for (var x = 0; x < GRID_COLS; x++) {
balloons[x] = [];
for (var y = 0; y < GRID_ROWS; y++) {
var bData = state.balloons[x][y];
if (bData) {
var colorIdx = BALLOON_COLORS.indexOf(bData.color);
var name = BALLOON_COLOR_NAMES[colorIdx];
var balloon = new Balloon();
balloon.color = bData.color;
balloon.gridX = bData.gridX;
balloon.gridY = bData.gridY;
balloon.popped = bData.popped;
balloon.removeChildren();
balloon.attachAsset('balloon_' + name, {
anchorX: 0.5,
anchorY: 0.5
});
balloon.x = offsetX + x * (balloonSize + balloonSpacing);
balloon.y = offsetY + y * (balloonSize + balloonSpacing);
if (!balloon.popped) {
game.addChild(balloon);
}
balloons[x][y] = balloon;
allBalloons.push(balloon);
} else {
balloons[x][y] = null;
}
}
}
LK.setScore(state.score);
scoreTxt.setText(state.score);
}
function undoLastAction() {
if (undoStack.length > 0) {
var prevState = undoStack.pop();
restoreGameState(prevState);
}
}
// Undo button at top center
var undoBtn = new Text2('Undo', {
size: 90,
fill: 0x222222,
font: "'GillSans-Bold',Impact,'Arial Black',Tahoma"
});
undoBtn.anchor.set(0.5, 0);
undoBtn.x = 2048 / 2;
undoBtn.y = 140;
undoBtn.interactive = true;
undoBtn.buttonMode = true;
undoBtn.down = function () {
undoLastAction();
};
LK.gui.top.addChild(undoBtn);
// On game over or win, reset game
game.onGameOver = function () {
resetGame();
startColorTimer();
startElapsedTimer();
};
game.onYouWin = function () {
resetGame();
startColorTimer();
startElapsedTimer();
};