/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ var Ball = Container.expand(function (color) { var self = Container.call(this); self.ballColor = color; self.gridX = 0; self.gridY = 0; self.isAnimating = false; var ballAssets = ['redBall', 'blueBall', 'greenBall', 'yellowBall', 'purpleBall', 'orangeBall']; var assetToUse = ballAssets[color]; var ballGraphics = self.attachAsset(assetToUse, { anchorX: 0.5, anchorY: 0.5 }); self.setGridPosition = function (gridX, gridY) { self.gridX = gridX; self.gridY = gridY; self.x = gridStartX + gridX * cellSize + cellSize / 2; self.y = gridStartY + gridY * cellSize + cellSize / 2; }; self.explode = function () { // Scale up and fade out explosion effect tween(self, { scaleX: 1.3, scaleY: 1.3, alpha: 0 }, { duration: 600, easing: tween.easeInOut, onFinish: function onFinish() { if (self.parent) { self.destroy(); } } }); }; return self; }); var Bomb = Container.expand(function () { var self = Container.call(this); self.gridX = 0; self.gridY = 0; self.isAnimating = false; self.isBomb = true; self.isDragging = false; // Create bomb visual using the Bomba asset var bombGraphics = self.attachAsset('Bomba', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.8, scaleY: 0.8 }); // Add pulsing animation to make it stand out self.pulseAnimation = function () { tween(self, { scaleX: 1.1, scaleY: 1.1 }, { duration: 500, easing: tween.easeInOut, onFinish: function onFinish() { tween(self, { scaleX: 0.8, scaleY: 0.8 }, { duration: 500, easing: tween.easeInOut, onFinish: function onFinish() { if (self.parent && !self.isDragging) { self.pulseAnimation(); } } }); } }); }; self.setGridPosition = function (gridX, gridY) { self.gridX = gridX; self.gridY = gridY; self.x = gridStartX + gridX * cellSize + cellSize / 2; self.y = gridStartY + gridY * cellSize + cellSize / 2; }; self.explode = function () { // Simple bomb explosion effect (same as Ball explode method) tween(self, { scaleX: 1.3, scaleY: 1.3, alpha: 0 }, { duration: 600, easing: tween.easeInOut, onFinish: function onFinish() { if (self.parent) { self.destroy(); } } }); }; self.explode3x3 = function () { // Explode all balls in a 3x3 area around the bomb var centerX = self.gridX; var centerY = self.gridY; var explodedCount = 0; for (var dx = -1; dx <= 1; dx++) { for (var dy = -1; dy <= 1; dy++) { var targetX = centerX + dx; var targetY = centerY + dy; if (targetX >= 0 && targetX < gridSize && targetY >= 0 && targetY < gridSize) { var ball = grid[targetY][targetX]; if (ball) { // If it's a chained ball, break the chain first if (ball.isChained) { ball.breakChain(); // Give extra points for breaking chains with bombs LK.setScore(LK.getScore() + 5); } ball.explode(); grid[targetY][targetX] = null; explodedCount++; } } } } // Award points for exploded balls (apply combo multiplier) var bombPoints = Math.floor(explodedCount * 15 * comboMultiplier); LK.setScore(LK.getScore() + bombPoints); scoreText.setText('Score: ' + LK.getScore()); // Update progress bar var currentScore = LK.getScore(); // Calculate score needed for current level progression function getScoreForLevel(level) { if (level <= 1) return 0; var totalScore = 0; for (var i = 1; i < level; i++) { totalScore += 400 + (i - 1) * 150; } return totalScore; } function getScoreNeededForNextLevel(level) { return 400 + (level - 1) * 150; } var scoreInCurrentLevel = currentScore - getScoreForLevel(currentLevel); var scoreNeededThisLevel = getScoreNeededForNextLevel(currentLevel); var progress = Math.min(1, scoreInCurrentLevel / scoreNeededThisLevel); progressBarFill.width = progress * 400; progressText.setText(scoreInCurrentLevel + '/' + scoreNeededThisLevel); checkLevelProgression(); // Remove bomb from grid grid[self.gridY][self.gridX] = null; // Explosion effect for bomb itself tween(self, { scaleX: 2, scaleY: 2, alpha: 0 }, { duration: 400, easing: tween.easeOut, onFinish: function onFinish() { if (self.parent) { self.destroy(); } } }); }; // Start pulsing animation when created self.pulseAnimation(); return self; }); var ChainedBall = Container.expand(function (color) { var self = Container.call(this); self.ballColor = color; self.gridX = 0; self.gridY = 0; self.isAnimating = false; self.isChained = true; // Create chain background first (larger, darker) var chainGraphics = self.attachAsset('chain', { anchorX: 0.5, anchorY: 0.5, alpha: 0.7 }); // Create ball on top of chain var ballAssets = ['redBall', 'blueBall', 'greenBall', 'yellowBall', 'purpleBall', 'orangeBall']; var assetToUse = ballAssets[color]; var ballGraphics = self.attachAsset(assetToUse, { anchorX: 0.5, anchorY: 0.5, scaleX: 0.85, scaleY: 0.85 }); // Add subtle chain animation self.chainAnimation = function () { tween(chainGraphics, { rotation: 0.1 }, { duration: 1000, easing: tween.easeInOut, onFinish: function onFinish() { tween(chainGraphics, { rotation: -0.1 }, { duration: 1000, easing: tween.easeInOut, onFinish: function onFinish() { if (self.parent && self.isChained) { self.chainAnimation(); } } }); } }); }; self.setGridPosition = function (gridX, gridY) { self.gridX = gridX; self.gridY = gridY; self.x = gridStartX + gridX * cellSize + cellSize / 2; self.y = gridStartY + gridY * cellSize + cellSize / 2; }; self.explode = function () { // Scale up and fade out explosion effect tween(self, { scaleX: 1.3, scaleY: 1.3, alpha: 0 }, { duration: 600, easing: tween.easeInOut, onFinish: function onFinish() { if (self.parent) { self.destroy(); } } }); }; self.breakChain = function () { // Break the chain and convert to regular ball self.isChained = false; // Animate chain breaking tween(chainGraphics, { scaleX: 1.5, scaleY: 1.5, alpha: 0 }, { duration: 300, easing: tween.easeOut }); // Scale ball back to normal size tween(ballGraphics, { scaleX: 1.0, scaleY: 1.0 }, { duration: 300, easing: tween.easeOut }); // Flash effect when chain breaks LK.effects.flashObject(self, 0xFFFFFF, 400); }; // Start chain animation self.chainAnimation(); return self; }); var FrozenBall = Container.expand(function (color) { var self = Container.call(this); self.ballColor = color; self.gridX = 0; self.gridY = 0; self.isAnimating = false; self.isFrozen = true; self.matchesAround = 0; // Track how many matches are around this ball var ballAssets = ['redBall', 'blueBall', 'greenBall', 'yellowBall', 'purpleBall', 'orangeBall']; var assetToUse = ballAssets[color]; // Create ice background first (larger, semi-transparent white/blue) var iceGraphics = self.attachAsset('Ice', { anchorX: 0.5, anchorY: 0.5, alpha: 0.8, scaleX: 1.1, scaleY: 1.1 }); // Create ball on top of ice var ballGraphics = self.attachAsset(assetToUse, { anchorX: 0.5, anchorY: 0.5, scaleX: 0.85, scaleY: 0.85, alpha: 0.8 }); // Add subtle frost animation self.frostAnimation = function () { tween(iceGraphics, { alpha: 0.4 }, { duration: 1200, easing: tween.easeInOut, onFinish: function onFinish() { tween(iceGraphics, { alpha: 0.6 }, { duration: 1200, easing: tween.easeInOut, onFinish: function onFinish() { if (self.parent && self.isFrozen) { self.frostAnimation(); } } }); } }); }; self.setGridPosition = function (gridX, gridY) { self.gridX = gridX; self.gridY = gridY; self.x = gridStartX + gridX * cellSize + cellSize / 2; self.y = gridStartY + gridY * cellSize + cellSize / 2; }; self.explode = function () { // Scale up and fade out explosion effect tween(self, { scaleX: 1.3, scaleY: 1.3, alpha: 0 }, { duration: 600, easing: tween.easeInOut, onFinish: function onFinish() { if (self.parent) { self.destroy(); } } }); }; self.melt = function () { // Melt the ice and convert to regular ball self.isFrozen = false; // Animate ice melting tween(iceGraphics, { scaleX: 1.5, scaleY: 1.5, alpha: 0 }, { duration: 400, easing: tween.easeOut }); // Scale ball back to normal size and opacity tween(ballGraphics, { scaleX: 1.0, scaleY: 1.0, alpha: 1.0 }, { duration: 400, easing: tween.easeOut }); // Flash effect when ice melts LK.effects.flashObject(self, 0x00FFFF, 400); }; self.checkForMelting = function () { if (!self.isFrozen) return; // Count matches around this frozen ball var matchCount = 0; var directions = [{ dx: -1, dy: 0 }, { dx: 1, dy: 0 }, { dx: 0, dy: -1 }, { dx: 0, dy: 1 }]; for (var d = 0; d < directions.length; d++) { var dir = directions[d]; var adjX = self.gridX + dir.dx; var adjY = self.gridY + dir.dy; if (adjX >= 0 && adjX < gridSize && adjY >= 0 && adjY < gridSize) { var adjacentBall = grid[adjY][adjX]; if (adjacentBall && adjacentBall.ballColor !== undefined && !adjacentBall.isFrozen) { // Check if this adjacent ball is part of a match if (isPartOfMatch(adjX, adjY)) { matchCount++; } } } } // Melt if 2 or more matches around if (matchCount >= 2) { self.melt(); } }; // Start frost animation self.frostAnimation(); return self; }); // Helper function to check if a ball is part of a match var HealthBox = Container.expand(function () { var self = Container.call(this); self.gridX = 0; self.gridY = 0; self.isAnimating = false; self.isHealthBox = true; self.isDragging = false; // Create health box visual using the healthBox asset var healthGraphics = self.attachAsset('healthBox', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.8, scaleY: 0.8 }); // Add gentle glow animation self.glowAnimation = function () { tween(self, { alpha: 0.7 }, { duration: 800, easing: tween.easeInOut, onFinish: function onFinish() { tween(self, { alpha: 1.0 }, { duration: 800, easing: tween.easeInOut, onFinish: function onFinish() { if (self.parent && !self.isDragging) { self.glowAnimation(); } } }); } }); }; self.setGridPosition = function (gridX, gridY) { self.gridX = gridX; self.gridY = gridY; self.x = gridStartX + gridX * cellSize + cellSize / 2; self.y = gridStartY + gridY * cellSize + cellSize / 2; }; self.explode = function () { // Simple health box explosion effect tween(self, { scaleX: 1.3, scaleY: 1.3, alpha: 0 }, { duration: 600, easing: tween.easeInOut, onFinish: function onFinish() { if (self.parent) { self.destroy(); } } }); }; self.activateHealthBonus = function () { // Add 8 moves when health box is clicked movesLeft += 8; movesText.setText('Moves: ' + movesLeft); // Remove health box from grid grid[self.gridY][self.gridX] = null; // Visual effect for health box activation tween(self, { scaleX: 1.5, scaleY: 1.5, alpha: 0 }, { duration: 400, easing: tween.easeOut, onFinish: function onFinish() { if (self.parent) { self.destroy(); } // After health box is removed, trigger ball dropping and filling LK.setTimeout(function () { dropBalls(); fillEmptySpaces(); // Check for any new matches that might form processMatches(); }, 100); } }); // Flash screen green to indicate health bonus LK.effects.flashScreen(0x00FF00, 500); }; // Start glow animation when created self.glowAnimation(); return self; }); var Rocket = Container.expand(function () { var self = Container.call(this); self.gridX = 0; self.gridY = 0; self.isAnimating = false; self.isRocket = true; self.isDragging = false; // Create rocket visual using the Roket asset var rocketGraphics = self.attachAsset('Roket', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.9, scaleY: 0.9 }); // Add rocket animation to make it stand out with swaying and ignition effects self.rocketAnimation = function () { // Swaying motion tween(self, { rotation: 0.3, x: self.x + 5 }, { duration: 600, easing: tween.easeInOut, onFinish: function onFinish() { tween(self, { rotation: -0.3, x: self.x - 10 }, { duration: 600, easing: tween.easeInOut, onFinish: function onFinish() { tween(self, { rotation: 0, x: gridStartX + self.gridX * cellSize + cellSize / 2 }, { duration: 400, easing: tween.easeInOut, onFinish: function onFinish() { if (self.parent && !self.isDragging) { self.rocketAnimation(); } } }); } }); } }); // Add ignition effect self.ignitionEffect(); }; // Add ignition effect method self.ignitionEffect = function () { // Create pulsing glow effect to simulate rocket ignition tween(rocketGraphics, { alpha: 0.6 }, { duration: 200, easing: tween.easeInOut, onFinish: function onFinish() { tween(rocketGraphics, { alpha: 1.0 }, { duration: 200, easing: tween.easeInOut, onFinish: function onFinish() { if (self.parent && !self.isDragging) { // Random interval for ignition effect to look more natural var nextIgnition = 800 + Math.random() * 1200; LK.setTimeout(function () { if (self.parent && !self.isDragging) { self.ignitionEffect(); } }, nextIgnition); } } }); } }); }; self.setGridPosition = function (gridX, gridY) { self.gridX = gridX; self.gridY = gridY; self.x = gridStartX + gridX * cellSize + cellSize / 2; self.y = gridStartY + gridY * cellSize + cellSize / 2; }; self.explode = function () { // Simple rocket explosion effect tween(self, { scaleX: 1.3, scaleY: 1.3, alpha: 0 }, { duration: 600, easing: tween.easeInOut, onFinish: function onFinish() { if (self.parent) { self.destroy(); } } }); }; self.explode10x2 = function () { // Explode all balls in a 2x10 area around the rocket var centerX = self.gridX; var centerY = self.gridY; var explodedCount = 0; // Create 2x10 explosion pattern (2 columns, 5 cells up, rocket center, 4 cells down) for (var dx = -1; dx <= 0; dx++) { for (var dy = -5; dy <= 4; dy++) { var targetX = centerX + dx; var targetY = centerY + dy; if (targetX >= 0 && targetX < gridSize && targetY >= 0 && targetY < gridSize) { var ball = grid[targetY][targetX]; if (ball) { // If it's a chained ball, break the chain first if (ball.isChained) { ball.breakChain(); // Give extra points for breaking chains with rockets LK.setScore(LK.getScore() + 5); } ball.explode(); grid[targetY][targetX] = null; explodedCount++; } } } } // Award points for exploded balls (apply combo multiplier) var rocketPoints = Math.floor(explodedCount * 20 * comboMultiplier); LK.setScore(LK.getScore() + rocketPoints); scoreText.setText('Score: ' + LK.getScore()); // Update progress bar var currentScore = LK.getScore(); // Calculate score needed for current level progression function getScoreForLevel(level) { if (level <= 1) return 0; var totalScore = 0; for (var i = 1; i < level; i++) { totalScore += 400 + (i - 1) * 150; } return totalScore; } function getScoreNeededForNextLevel(level) { return 400 + (level - 1) * 150; } var scoreInCurrentLevel = currentScore - getScoreForLevel(currentLevel); var scoreNeededThisLevel = getScoreNeededForNextLevel(currentLevel); var progress = Math.min(1, scoreInCurrentLevel / scoreNeededThisLevel); progressBarFill.width = progress * 400; progressText.setText(scoreInCurrentLevel + '/' + scoreNeededThisLevel); checkLevelProgression(); // Remove rocket from grid grid[self.gridY][self.gridX] = null; // Explosion effect for rocket itself tween(self, { scaleX: 2.5, scaleY: 2.5, alpha: 0 }, { duration: 500, easing: tween.easeOut, onFinish: function onFinish() { if (self.parent) { self.destroy(); } } }); }; // Start rocket animation when created self.rocketAnimation(); return self; }); var TimeBonus = Container.expand(function () { var self = Container.call(this); self.gridX = 0; self.gridY = 0; self.isAnimating = false; self.isTimeBonus = true; self.isDragging = false; // Create time bonus visual using the Saat asset var timeBonusGraphics = self.attachAsset('Saat', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.9, scaleY: 0.9 }); // Add clock-like pulsing animation self.clockAnimation = function () { tween(self, { scaleX: 1.1, scaleY: 1.1 }, { duration: 600, easing: tween.easeInOut, onFinish: function onFinish() { tween(self, { scaleX: 0.9, scaleY: 0.9 }, { duration: 600, easing: tween.easeInOut, onFinish: function onFinish() { if (self.parent && !self.isDragging) { self.clockAnimation(); } } }); } }); }; self.setGridPosition = function (gridX, gridY) { self.gridX = gridX; self.gridY = gridY; self.x = gridStartX + gridX * cellSize + cellSize / 2; self.y = gridStartY + gridY * cellSize + cellSize / 2; }; self.explode = function () { // Simple time bonus explosion effect tween(self, { scaleX: 1.3, scaleY: 1.3, alpha: 0 }, { duration: 600, easing: tween.easeInOut, onFinish: function onFinish() { if (self.parent) { self.destroy(); } } }); }; self.activateTimeBonus = function () { // Add 10 seconds when time bonus is clicked in time trial mode if (gameMode === 'timetrial') { timeLeft += 10; updateTimer(); } // Remove time bonus from grid grid[self.gridY][self.gridX] = null; // Visual effect for time bonus activation tween(self, { scaleX: 1.5, scaleY: 1.5, alpha: 0 }, { duration: 400, easing: tween.easeOut, onFinish: function onFinish() { if (self.parent) { self.destroy(); } // After time bonus is removed, trigger ball dropping and filling LK.setTimeout(function () { dropBalls(); fillEmptySpaces(); // Check for any new matches that might form processMatches(); }, 100); } }); // Flash screen cyan to indicate time bonus LK.effects.flashScreen(0x00FFFF, 500); }; // Start clock animation when created self.clockAnimation(); return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x2c3e50 }); /**** * Game Code ****/ // Helper function to check if a ball is part of a match // Game state management function isPartOfMatch(x, y) { var ball = grid[y][x]; if (!ball || ball.ballColor === undefined) return false; var color = ball.ballColor; // Check horizontal match var horizCount = 1; // Check left for (var i = x - 1; i >= 0; i--) { var leftBall = grid[y][i]; if (leftBall && leftBall.ballColor === color && !leftBall.isFrozen) { horizCount++; } else { break; } } // Check right for (var i = x + 1; i < gridSize; i++) { var rightBall = grid[y][i]; if (rightBall && rightBall.ballColor === color && !rightBall.isFrozen) { horizCount++; } else { break; } } if (horizCount >= 3) return true; // Check vertical match var vertCount = 1; // Check up for (var i = y - 1; i >= 0; i--) { var upBall = grid[i][x]; if (upBall && upBall.ballColor === color && !upBall.isFrozen) { vertCount++; } else { break; } } // Check down for (var i = y + 1; i < gridSize; i++) { var downBall = grid[i][x]; if (downBall && downBall.ballColor === color && !downBall.isFrozen) { vertCount++; } else { break; } } return vertCount >= 3; } var gameState = 'cinematic'; // 'cinematic', 'menu' or 'playing' var gameMode = 'classic'; // 'classic', 'timetrial', or 'freezefrenzy' var menuContainer; var gameContainer; var cinematicContainer; // Time trial specific variables var timeTrialDuration = 120; // 2 minutes in seconds var timeLeft = timeTrialDuration; var timerText; var gameTimer; // Grid configuration variables needed by Ball class var gridSize = 8; var cellSize = 220; var gridStartX = (2048 - gridSize * cellSize) / 2; var gridStartY = 400; var grid = []; var gridShape = []; // Array to define which cells are active in current level // Define different grid shapes for each level var levelShapes = [ // Level 1: Full 8x8 square function (size) { var shape = []; for (var y = 0; y < size; y++) { shape[y] = []; for (var x = 0; x < size; x++) { shape[y][x] = true; } } return shape; }, // Level 2: Diamond shape function (size) { var shape = []; var center = Math.floor(size / 2); for (var y = 0; y < size; y++) { shape[y] = []; for (var x = 0; x < size; x++) { var distanceFromCenter = Math.abs(x - center) + Math.abs(y - center); shape[y][x] = distanceFromCenter <= center; } } return shape; }, // Level 3: Cross shape function (size) { var shape = []; var center = Math.floor(size / 2); for (var y = 0; y < size; y++) { shape[y] = []; for (var x = 0; x < size; x++) { shape[y][x] = x === center || y === center; } } return shape; }, // Level 4: Hollow square (border only) function (size) { var shape = []; for (var y = 0; y < size; y++) { shape[y] = []; for (var x = 0; x < size; x++) { shape[y][x] = x === 0 || x === size - 1 || y === 0 || y === size - 1; } } return shape; }, // Level 5: L shape function (size) { var shape = []; for (var y = 0; y < size; y++) { shape[y] = []; for (var x = 0; x < size; x++) { shape[y][x] = x >= 0 && x < 4 && y >= size - 7 || y >= size - 4 && x >= 0 && x < 7; } } return shape; }, // Level 6: Circle shape function (size) { var shape = []; var center = size / 2 - 0.5; var radius = center; for (var y = 0; y < size; y++) { shape[y] = []; for (var x = 0; x < size; x++) { var dx = x - center; var dy = y - center; var distance = Math.sqrt(dx * dx + dy * dy); shape[y][x] = distance <= radius; } } return shape; }]; // Function to get current level's grid shape function getCurrentGridShape() { var shapeIndex = (currentLevel - 1) % levelShapes.length; return levelShapes[shapeIndex](gridSize); } // Function to check if a grid position is valid for current shape function isValidGridPosition(x, y) { return gridShape[y] && gridShape[y][x]; } var selectedBall = null; var movesLeft = getMovesForLevel(1); // Start with moves calculated for level 1 var isProcessingMatches = false; var ballColors = 4; // Start with 4 colors for easier beginning var bombSpawnChance = 0.01; // 1% chance to spawn bomb instead of ball var rocketSpawnChance = 0.025; // 2.5% chance to spawn rocket instead of ball var healthBoxSpawnChance = 0.04; // 4% chance to spawn health box instead of ball var timeBonusSpawnChance = 0.03; // 3% chance to spawn time bonus instead of ball (time trial mode only) var draggedBomb = null; var draggedRocket = null; var lastMoveTime = 0; var currentLevel = 1; var baseMovesPerLevel = 30; var pointsToNextLevel = 400; // Points needed to advance to next level var totalPointsEarned = 0; // Combo system variables var currentCombo = 0; var comboText; var comboMultiplier = 1; var maxCombo = 0; var comboTimeout; // Function to calculate moves for current level function getMovesForLevel(level) { // Start with 40 moves at level 1, decrease by 3 moves per level, minimum 15 moves return Math.max(15, 40 - (level - 1) * 3); } // Combo system functions function updateCombo() { currentCombo++; maxCombo = Math.max(maxCombo, currentCombo); // Calculate multiplier based on combo (1x, 1.5x, 2x, 2.5x, 3x, max) comboMultiplier = 1 + Math.min(2, (currentCombo - 1) * 0.5); // Show combo text with animation if (currentCombo >= 2) { comboText.setText('COMBO x' + currentCombo + '\n' + comboMultiplier.toFixed(1) + 'x POINTS!'); comboText.visible = true; // Animate combo text with more prominent effects comboText.scaleX = 0.3; comboText.scaleY = 0.3; comboText.alpha = 0; tween(comboText, { scaleX: 1.4, scaleY: 1.4, alpha: 1 }, { duration: 400, easing: tween.easeOut, onFinish: function onFinish() { tween(comboText, { scaleX: 1.1, scaleY: 1.1 }, { duration: 300, easing: tween.easeOut }); } }); // Change color based on combo level var comboColor = '#FF6B6B'; // Red for 2-3 if (currentCombo >= 4 && currentCombo < 6) { comboColor = '#FF9500'; // Orange for 4-5 } else if (currentCombo >= 6 && currentCombo < 8) { comboColor = '#FFD700'; // Gold for 6-7 } else if (currentCombo >= 8) { comboColor = '#9D4EDD'; // Purple for 8+ } comboText.fill = comboColor; // Screen flash effect for high combos if (currentCombo >= 4) { var flashColor = 0xFF9500; if (currentCombo >= 6) flashColor = 0xFFD700; if (currentCombo >= 8) flashColor = 0x9D4EDD; LK.effects.flashScreen(flashColor, 300); } } // Clear existing timeout and set new one if (comboTimeout) { LK.clearTimeout(comboTimeout); } comboTimeout = LK.setTimeout(function () { resetCombo(); }, 3000); // Reset combo after 3 seconds of no matches } function resetCombo() { if (currentCombo >= 2) { // Fade out combo text tween(comboText, { alpha: 0, scaleX: 0.3, scaleY: 0.3 }, { duration: 500, easing: tween.easeIn, onFinish: function onFinish() { comboText.visible = false; } }); } currentCombo = 0; comboMultiplier = 1; if (comboTimeout) { LK.clearTimeout(comboTimeout); comboTimeout = null; } } // UI Elements var movesText = new Text2('Moves: ' + getMovesForLevel(1), { size: 80, fill: 0xFFFFFF }); movesText.anchor.set(0.5, 0); LK.gui.top.addChild(movesText); movesText.y = 150; // Timer text for time trial mode timerText = new Text2('Time: 2:00', { size: 80, fill: 0xFFFFFF }); timerText.anchor.set(0.5, 0); LK.gui.top.addChild(timerText); timerText.y = 150; timerText.visible = false; // Hidden by default // Time trial timer function function updateTimer() { timeLeft--; var minutes = Math.floor(timeLeft / 60); var seconds = timeLeft % 60; var timeString = minutes + ':' + (seconds < 10 ? '0' : '') + seconds; timerText.setText('Time: ' + timeString); if (timeLeft <= 0) { LK.clearInterval(gameTimer); LK.showGameOver(); } else if (timeLeft <= 10) { // Flash red when time is running out LK.effects.flashObject(timerText, 0xFF0000, 200); } } var scoreText = new Text2('Score: 0', { size: 80, fill: 0xFFFFFF }); scoreText.anchor.set(0, 0); LK.gui.top.addChild(scoreText); scoreText.x = 150; scoreText.y = 250; // Progress bar to show points needed for next level var progressBarBackground = LK.getAsset('gridCell', { width: 400, height: 40, anchorX: 1.0, anchorY: 0, alpha: 0.3 }); LK.gui.topRight.addChild(progressBarBackground); progressBarBackground.x = -50; progressBarBackground.y = 150; var progressBarFill = LK.getAsset('gridCell', { width: 1, height: 40, anchorX: 0, anchorY: 0, color: 0x00FF00 }); progressBarBackground.addChild(progressBarFill); var progressText = new Text2('0/400', { size: 50, fill: 0xFFFFFF }); progressText.anchor.set(1, 0); LK.gui.topRight.addChild(progressText); progressText.x = -50; progressText.y = 200; // Combo text display - moved to top left and made more prominent comboText = new Text2('', { size: 150, fill: '#FF6B6B' }); comboText.anchor.set(0, 0); LK.gui.topLeft.addChild(comboText); comboText.x = 150; comboText.y = 50; comboText.visible = false; // Initialize grid function initializeGrid() { // Get the shape for current level (freeze frenzy always uses full 8x8) if (gameMode === 'freezefrenzy') { gridShape = levelShapes[0](gridSize); // Always use full 8x8 square } else { gridShape = getCurrentGridShape(); } grid = []; for (var y = 0; y < gridSize; y++) { grid[y] = []; for (var x = 0; x < gridSize; x++) { grid[y][x] = null; } } // Fill grid with balls only in valid positions for (var y = 0; y < gridSize; y++) { for (var x = 0; x < gridSize; x++) { if (isValidGridPosition(x, y)) { createBallAt(x, y); } } } // Remove initial matches while (findMatches().length > 0) { removeMatches(); dropBalls(); fillEmptySpaces(); } } function createBallAt(x, y) { // Only create balls in valid grid positions if (!isValidGridPosition(x, y)) { return null; } // Also check if position is within grid bounds if (x < 0 || x >= gridSize || y < 0 || y >= gridSize) { return null; } // Freeze frenzy mode - no bombs, chained balls, or health boxes if (gameMode === 'freezefrenzy') { var color = Math.floor(Math.random() * ballColors); // 30% chance to create frozen ball if (Math.random() < 0.3) { var frozenBall = new FrozenBall(color); frozenBall.setGridPosition(x, y); grid[y][x] = frozenBall; game.addChild(frozenBall); // Animate frozen ball appearing frozenBall.scaleX = 0; frozenBall.scaleY = 0; frozenBall.alpha = 0; tween(frozenBall, { scaleX: 1, scaleY: 1, alpha: 1 }, { duration: 400, easing: tween.easeOut }); return frozenBall; } else { // Create regular ball var ball = new Ball(color); ball.setGridPosition(x, y); grid[y][x] = ball; game.addChild(ball); // Animate ball appearing ball.scaleX = 0; ball.scaleY = 0; ball.alpha = 0; tween(ball, { scaleX: 1, scaleY: 1, alpha: 1 }, { duration: 400, easing: tween.easeOut }); return ball; } } // Sometimes create a bomb instead of a ball if (Math.random() < bombSpawnChance) { var bomb = new Bomb(); bomb.setGridPosition(x, y); grid[y][x] = bomb; game.addChild(bomb); // Animate bomb appearing bomb.scaleX = 0; bomb.scaleY = 0; bomb.alpha = 0; tween(bomb, { scaleX: 0.8, scaleY: 0.8, alpha: 1 }, { duration: 400, easing: tween.easeOut }); return bomb; } else if (Math.random() < rocketSpawnChance) { var rocket = new Rocket(); rocket.setGridPosition(x, y); grid[y][x] = rocket; game.addChild(rocket); // Animate rocket appearing rocket.scaleX = 0; rocket.scaleY = 0; rocket.alpha = 0; tween(rocket, { scaleX: 0.9, scaleY: 0.9, alpha: 1 }, { duration: 400, easing: tween.easeOut }); return rocket; } else if (gameMode === 'classic' && Math.random() < healthBoxSpawnChance) { var healthBox = new HealthBox(); healthBox.setGridPosition(x, y); grid[y][x] = healthBox; game.addChild(healthBox); // Animate health box appearing healthBox.scaleX = 0; healthBox.scaleY = 0; healthBox.alpha = 0; tween(healthBox, { scaleX: 0.8, scaleY: 0.8, alpha: 1 }, { duration: 400, easing: tween.easeOut }); return healthBox; } else if (gameMode === 'timetrial' && Math.random() < timeBonusSpawnChance) { var timeBonus = new TimeBonus(); timeBonus.setGridPosition(x, y); grid[y][x] = timeBonus; game.addChild(timeBonus); // Animate time bonus appearing timeBonus.scaleX = 0; timeBonus.scaleY = 0; timeBonus.alpha = 0; tween(timeBonus, { scaleX: 0.9, scaleY: 0.9, alpha: 1 }, { duration: 400, easing: tween.easeOut }); return timeBonus; } else if (Math.random() < 0.05) { // 5% chance to create chained ball - use smart color selection var adjacentColors = []; // Check all 4 directions for adjacent ball colors var directions = [{ dx: -1, dy: 0 }, { dx: 1, dy: 0 }, { dx: 0, dy: -1 }, { dx: 0, dy: 1 }]; for (var d = 0; d < directions.length; d++) { var dir = directions[d]; var adjX = x + dir.dx; var adjY = y + dir.dy; if (adjX >= 0 && adjX < gridSize && adjY >= 0 && adjY < gridSize && isValidGridPosition(adjX, adjY) && grid[adjY][adjX] && grid[adjY][adjX].ballColor !== undefined) { adjacentColors.push(grid[adjY][adjX].ballColor); } } var color; if (adjacentColors.length > 0 && Math.random() < 0.4) { // 40% chance to match an adjacent color for potential future matches color = adjacentColors[Math.floor(Math.random() * adjacentColors.length)]; } else { color = Math.floor(Math.random() * ballColors); } var chainedBall = new ChainedBall(color); chainedBall.setGridPosition(x, y); grid[y][x] = chainedBall; game.addChild(chainedBall); // Animate chained ball appearing chainedBall.scaleX = 0; chainedBall.scaleY = 0; chainedBall.alpha = 0; tween(chainedBall, { scaleX: 1, scaleY: 1, alpha: 1 }, { duration: 400, easing: tween.easeOut }); return chainedBall; } else { // Smart color selection for regular balls var adjacentColors = []; var belowColors = []; // Check all 4 directions for adjacent ball colors var directions = [{ dx: -1, dy: 0 }, { dx: 1, dy: 0 }, { dx: 0, dy: -1 }, { dx: 0, dy: 1 }]; for (var d = 0; d < directions.length; d++) { var dir = directions[d]; var adjX = x + dir.dx; var adjY = y + dir.dy; if (adjX >= 0 && adjX < gridSize && adjY >= 0 && adjY < gridSize && isValidGridPosition(adjX, adjY) && grid[adjY][adjX] && grid[adjY][adjX].ballColor !== undefined) { adjacentColors.push(grid[adjY][adjX].ballColor); // Special attention to ball below for dropping compatibility if (dir.dy === 1) { belowColors.push(grid[adjY][adjX].ballColor); } } } var color; // 53% chance to be compatible with environment if adjacent balls exist if (adjacentColors.length > 0 && Math.random() < 0.53) { // 60% chance to match an adjacent color (for potential matches) if (Math.random() < 0.6) { // Prioritize matching ball below if it exists if (belowColors.length > 0) { color = belowColors[Math.floor(Math.random() * belowColors.length)]; } else { color = adjacentColors[Math.floor(Math.random() * adjacentColors.length)]; } } else { // 40% chance to choose a color that would create diversity but still be strategic var usedColors = {}; for (var i = 0; i < adjacentColors.length; i++) { usedColors[adjacentColors[i]] = true; } var availableColors = []; for (var c = 0; c < ballColors; c++) { if (!usedColors[c]) { availableColors.push(c); } } if (availableColors.length > 0) { color = availableColors[Math.floor(Math.random() * availableColors.length)]; } else { color = Math.floor(Math.random() * ballColors); } } } else { // Random color when no adjacent balls or 50% random chance color = Math.floor(Math.random() * ballColors); } var ball = new Ball(color); ball.setGridPosition(x, y); grid[y][x] = ball; game.addChild(ball); // Animate ball appearing with scale and bounce ball.scaleX = 0; ball.scaleY = 0; ball.alpha = 0; tween(ball, { scaleX: 1, scaleY: 1, alpha: 1 }, { duration: 400, easing: tween.easeOut }); return ball; } } function getBallAt(x, y) { if (x < 0 || x >= gridSize || y < 0 || y >= gridSize) { return null; } if (!grid || !grid[y]) { return null; } return grid[y][x]; } function swapBalls(ball1, ball2) { if (!ball1 || !ball2) { return false; } // Check if either ball is chained or frozen - they cannot be moved if (ball1.isChained && ball1.isChained === true || ball2.isChained && ball2.isChained === true || ball1.isFrozen && ball1.isFrozen === true || ball2.isFrozen && ball2.isFrozen === true) { // Flash red to indicate chained/frozen balls cannot move LK.effects.flashScreen(0xFF0000, 200); return false; } var x1 = ball1.gridX, y1 = ball1.gridY; var x2 = ball2.gridX, y2 = ball2.gridY; // Check if balls are adjacent var dx = Math.abs(x1 - x2); var dy = Math.abs(y1 - y2); if (dx + dy !== 1) { return false; } // Mark balls as animating ball1.isAnimating = true; ball2.isAnimating = true; // Store old positions var oldX1 = ball1.x, oldY1 = ball1.y; var oldX2 = ball2.x, oldY2 = ball2.y; // Swap positions in grid grid[y1][x1] = ball2; grid[y2][x2] = ball1; // Update ball positions ball1.setGridPosition(x2, y2); ball2.setGridPosition(x1, y1); // Store new positions var newX1 = ball1.x, newY1 = ball1.y; var newX2 = ball2.x, newY2 = ball2.y; // Reset to old positions for animation ball1.x = oldX1; ball1.y = oldY1; ball2.x = oldX2; ball2.y = oldY2; // Animate to new positions tween(ball1, { x: newX1, y: newY1 }, { duration: 250, easing: tween.easeInOut, onFinish: function onFinish() { ball1.isAnimating = false; } }); tween(ball2, { x: newX2, y: newY2 }, { duration: 250, easing: tween.easeInOut, onFinish: function onFinish() { ball2.isAnimating = false; } }); LK.getSound('swap').play(); return true; } function findMatches() { var matches = []; // Check horizontal matches for (var y = 0; y < gridSize; y++) { var count = 0; var currentColor = -1; var matchPositions = []; for (var x = 0; x < gridSize; x++) { if (!isValidGridPosition(x, y)) { // Invalid position, reset match counting if (count >= 3) { for (var i = 0; i < matchPositions.length; i++) { matches.push(matchPositions[i]); } } count = 0; currentColor = -1; matchPositions = []; continue; } var ball = grid[y][x]; if (ball && ball.ballColor !== undefined && ball.ballColor === currentColor && !ball.isFrozen) { count++; matchPositions.push({ x: x, y: y }); } else { if (count >= 3) { for (var i = 0; i < matchPositions.length; i++) { matches.push(matchPositions[i]); } } count = ball && ball.ballColor !== undefined && !ball.isFrozen ? 1 : 0; currentColor = ball && ball.ballColor !== undefined && !ball.isFrozen ? ball.ballColor : -1; matchPositions = ball && ball.ballColor !== undefined && !ball.isFrozen ? [{ x: x, y: y }] : []; } } if (count >= 3) { for (var i = 0; i < matchPositions.length; i++) { matches.push(matchPositions[i]); } } } // Check vertical matches for (var x = 0; x < gridSize; x++) { var count = 0; var currentColor = -1; var matchPositions = []; for (var y = 0; y < gridSize; y++) { if (!isValidGridPosition(x, y)) { // Invalid position, reset match counting if (count >= 3) { for (var i = 0; i < matchPositions.length; i++) { matches.push(matchPositions[i]); } } count = 0; currentColor = -1; matchPositions = []; continue; } var ball = grid[y][x]; if (ball && ball.ballColor !== undefined && ball.ballColor === currentColor && !ball.isFrozen) { count++; matchPositions.push({ x: x, y: y }); } else { if (count >= 3) { for (var i = 0; i < matchPositions.length; i++) { matches.push(matchPositions[i]); } } count = ball && ball.ballColor !== undefined && !ball.isFrozen ? 1 : 0; currentColor = ball && ball.ballColor !== undefined && !ball.isFrozen ? ball.ballColor : -1; matchPositions = ball && ball.ballColor !== undefined && !ball.isFrozen ? [{ x: x, y: y }] : []; } } if (count >= 3) { for (var i = 0; i < matchPositions.length; i++) { matches.push(matchPositions[i]); } } } // Check diagonal matches (top-left to bottom-right) for (var startY = 0; startY < gridSize; startY++) { for (var startX = 0; startX < gridSize; startX++) { if (!isValidGridPosition(startX, startY)) continue; var startBall = grid[startY][startX]; if (!startBall || startBall.ballColor === undefined) continue; var count = 1; var currentColor = startBall.ballColor; var matchPositions = [{ x: startX, y: startY }]; for (var step = 1; startX + step < gridSize && startY + step < gridSize; step++) { var x = startX + step; var y = startY + step; if (!isValidGridPosition(x, y)) break; var ball = grid[y][x]; if (ball && ball.ballColor === currentColor && !ball.isFrozen) { count++; matchPositions.push({ x: x, y: y }); } else { break; } } if (count >= 3) { for (var i = 0; i < matchPositions.length; i++) { matches.push(matchPositions[i]); } } } } // Check diagonal matches (top-right to bottom-left) for (var startY = 0; startY < gridSize; startY++) { for (var startX = gridSize - 1; startX >= 0; startX--) { if (!isValidGridPosition(startX, startY)) continue; var startBall = grid[startY][startX]; if (!startBall || startBall.ballColor === undefined) continue; var count = 1; var currentColor = startBall.ballColor; var matchPositions = [{ x: startX, y: startY }]; for (var step = 1; startX - step >= 0 && startY + step < gridSize; step++) { var x = startX - step; var y = startY + step; if (!isValidGridPosition(x, y)) break; var ball = grid[y][x]; if (ball && ball.ballColor === currentColor && !ball.isFrozen) { count++; matchPositions.push({ x: x, y: y }); } else { break; } } if (count >= 3) { for (var i = 0; i < matchPositions.length; i++) { matches.push(matchPositions[i]); } } } } return matches; } function removeMatches() { var matches = findMatches(); if (matches.length === 0) { // Reset combo when no matches found resetCombo(); return false; } // Update combo system updateCombo(); var pointsEarned = matches.length * 10; // Apply combo multiplier to points pointsEarned = Math.floor(pointsEarned * comboMultiplier); // Check if this is a 4-ball match for special rewards var isFourBallMatch = matches.length === 4; if (isFourBallMatch) { // Award bonus 10 points for 4-ball match (also affected by multiplier) pointsEarned += Math.floor(10 * comboMultiplier); } for (var i = 0; i < matches.length; i++) { var match = matches[i]; var ball = grid[match.y][match.x]; if (ball) { // If it's a chained ball, break the chain first then explode if (ball.isChained) { ball.breakChain(); // Give extra points for breaking chains pointsEarned += 5; } ball.explode(); grid[match.y][match.x] = null; } } LK.setScore(LK.getScore() + pointsEarned); scoreText.setText('Score: ' + LK.getScore()); // Update progress bar var currentScore = LK.getScore(); // Calculate score needed for current level progression function getScoreForLevel(level) { if (level <= 1) return 0; var totalScore = 0; for (var i = 1; i < level; i++) { totalScore += 400 + (i - 1) * 150; } return totalScore; } function getScoreNeededForNextLevel(level) { return 400 + (level - 1) * 150; } var scoreInCurrentLevel = currentScore - getScoreForLevel(currentLevel); var scoreNeededThisLevel = getScoreNeededForNextLevel(currentLevel); var progress = Math.min(1, scoreInCurrentLevel / scoreNeededThisLevel); progressBarFill.width = progress * 400; progressText.setText(scoreInCurrentLevel + '/' + scoreNeededThisLevel); // Spawn a bomb as reward for 4-ball match if (isFourBallMatch) { // Find a random empty valid position to place the bomb var emptyPositions = []; for (var y = 0; y < gridSize; y++) { for (var x = 0; x < gridSize; x++) { if (grid[y][x] === null && isValidGridPosition(x, y)) { emptyPositions.push({ x: x, y: y }); } } } if (emptyPositions.length > 0) { var randomPos = emptyPositions[Math.floor(Math.random() * emptyPositions.length)]; var rewardBomb = new Bomb(); rewardBomb.setGridPosition(randomPos.x, randomPos.y); grid[randomPos.y][randomPos.x] = rewardBomb; game.addChild(rewardBomb); // Animate bomb appearing with special effect rewardBomb.scaleX = 0; rewardBomb.scaleY = 0; rewardBomb.alpha = 0; tween(rewardBomb, { scaleX: 1.2, scaleY: 1.2, alpha: 1 }, { duration: 600, easing: tween.easeOut, onFinish: function onFinish() { tween(rewardBomb, { scaleX: 0.8, scaleY: 0.8 }, { duration: 200, easing: tween.easeOut }); } }); // Flash screen gold to indicate bomb reward LK.effects.flashScreen(0xFFD700, 500); } } // Check for frozen balls that should melt after matches if (gameMode === 'freezefrenzy') { for (var y = 0; y < gridSize; y++) { for (var x = 0; x < gridSize; x++) { var ball = grid[y][x]; if (ball && ball.isFrozen && ball.checkForMelting) { ball.checkForMelting(); } } } } LK.getSound('match').play(); checkLevelProgression(); return true; } function dropBalls() { for (var x = 0; x < gridSize; x++) { var writePos = gridSize - 1; // Find the lowest valid position for this column while (writePos >= 0 && !isValidGridPosition(x, writePos)) { writePos--; } for (var y = gridSize - 1; y >= 0; y--) { if (grid[y][x] !== null && isValidGridPosition(x, y)) { if (y !== writePos && writePos >= 0) { grid[writePos][x] = grid[y][x]; grid[y][x] = null; var ball = grid[writePos][x]; if (ball) { ball.isAnimating = true; var oldY = ball.y; ball.setGridPosition(x, writePos); var newY = ball.y; ball.y = oldY; // Animate ball falling down (function (ballToAnimate) { tween(ballToAnimate, { y: newY }, { duration: 300, easing: tween.easeOut, onFinish: function onFinish() { ballToAnimate.isAnimating = false; } }); })(ball); } } // Only decrement writePos if we're at a valid position if (writePos >= 0) { writePos--; // Find next valid position while (writePos >= 0 && !isValidGridPosition(x, writePos)) { writePos--; } } } } } } function fillEmptySpaces() { for (var x = 0; x < gridSize; x++) { for (var y = 0; y < gridSize; y++) { if (grid[y][x] === null && isValidGridPosition(x, y)) { createBallAt(x, y); } } } } function processMatches() { if (isProcessingMatches) { return; } isProcessingMatches = true; function processStep() { if (removeMatches()) { dropBalls(); fillEmptySpaces(); LK.setTimeout(function () { processStep(); }, 500); } else { isProcessingMatches = false; checkGameState(); } } processStep(); } function checkLevelProgression() { // Calculate cumulative score needed for each level // Level 1: 0-399 (400 points) // Level 2: 400-949 (550 points) // Level 3: 950-1649 (700 points) // And so on, increasing by 150 each level function getScoreForLevel(level) { if (level <= 1) return 0; var totalScore = 0; for (var i = 1; i < level; i++) { totalScore += 400 + (i - 1) * 150; } return totalScore; } function getScoreNeededForNextLevel(level) { return 400 + (level - 1) * 150; } var currentScore = LK.getScore(); var newLevel = currentLevel; // Check if we've reached the next level while (currentScore >= getScoreForLevel(newLevel + 1)) { newLevel++; } if (newLevel > currentLevel) { // Level up! currentLevel = newLevel; pointsToNextLevel = getScoreNeededForNextLevel(currentLevel + 1); // Increase difficulty if (ballColors < 6) { ballColors = Math.min(6, 3 + currentLevel); // Max 6 colors } // Set moves based on new level difficulty (progressively fewer moves) if (gameMode === 'classic') { movesLeft = getMovesForLevel(currentLevel); // Update UI movesText.setText('Moves: ' + movesLeft); } else { // In time trial mode, keep unlimited moves movesLeft = 999; } // Reset progress bar for new level var scoreInCurrentLevel = currentScore - getScoreForLevel(currentLevel); var scoreNeededThisLevel = getScoreNeededForNextLevel(currentLevel); var progress = Math.min(1, scoreInCurrentLevel / scoreNeededThisLevel); progressBarFill.width = progress * 400; progressText.setText(scoreInCurrentLevel + '/' + scoreNeededThisLevel); // Change grid shape for new level - clear existing balls first for (var y = 0; y < gridSize; y++) { for (var x = 0; x < gridSize; x++) { if (grid[y][x]) { grid[y][x].destroy(); grid[y][x] = null; } } } // Initialize new grid shape gridShape = getCurrentGridShape(); // Fill new shape with balls for (var y = 0; y < gridSize; y++) { for (var x = 0; x < gridSize; x++) { if (isValidGridPosition(x, y)) { createBallAt(x, y); } } } // Flash screen green for level up LK.effects.flashScreen(0x00FF00, 800); } } function checkGameState() { if (gameMode === 'classic') { checkLevelProgression(); if (movesLeft <= 0) { LK.showGameOver(); return; } } else if (gameMode === 'timetrial') { // In time trial mode, only check for time running out // (timer already handles game over when time reaches 0) if (timeLeft <= 0) { LK.showGameOver(); return; } } else if (gameMode === 'freezefrenzy') { // Freeze frenzy mode - check if all frozen balls are melted or moves run out var frozenBallsLeft = 0; for (var y = 0; y < gridSize; y++) { for (var x = 0; x < gridSize; x++) { var ball = grid[y][x]; if (ball && ball.isFrozen) { frozenBallsLeft++; } } } if (frozenBallsLeft === 0) { // All frozen balls melted - player wins! LK.showYouWin(); return; } else if (movesLeft <= 0) { // No moves left and still frozen balls - game over LK.showGameOver(); return; } } } function getClickedBall(x, y) { var gridX = Math.floor((x - gridStartX) / cellSize); var gridY = Math.floor((y - gridStartY) / cellSize); if (gridX >= 0 && gridX < gridSize && gridY >= 0 && gridY < gridSize) { return getBallAt(gridX, gridY); } return null; } game.down = function (x, y, obj) { if (isProcessingMatches) { return; } var clickedBall = getClickedBall(x, y); if (clickedBall && !clickedBall.isAnimating) { // Check if it's a health box if (clickedBall.isHealthBox) { clickedBall.activateHealthBonus(); return; } // Check if it's a time bonus if (clickedBall.isTimeBonus) { clickedBall.activateTimeBonus(); return; } // Check if it's a bomb if (clickedBall.isBomb) { draggedBomb = clickedBall; draggedBomb.isDragging = true; // Stop pulsing animation tween.stop(draggedBomb); // Make bomb slightly larger while dragging tween(draggedBomb, { scaleX: 1.0, scaleY: 1.0 }, { duration: 100, easing: tween.easeOut }); return; } // Check if it's a rocket if (clickedBall.isRocket) { draggedRocket = clickedBall; draggedRocket.isDragging = true; // Stop rocket animation tween.stop(draggedRocket); // Make rocket slightly larger while dragging tween(draggedRocket, { scaleX: 1.1, scaleY: 1.1 }, { duration: 100, easing: tween.easeOut }); return; } // Regular ball logic if (selectedBall === null) { selectedBall = clickedBall; LK.effects.flashObject(selectedBall, 0xffffff, 200); } else if (selectedBall === clickedBall) { selectedBall = null; } else { if (swapBalls(selectedBall, clickedBall)) { if (gameMode === 'classic' || gameMode === 'freezefrenzy') { movesLeft--; movesText.setText('Moves: ' + movesLeft); } LK.setTimeout(function () { var matches = findMatches(); if (matches.length > 0) { processMatches(); } else { // Wasted move - swap back and apply additional penalty swapBalls(selectedBall, clickedBall); // No additional penalty for wasted move (already lost 1 move from the attempt) movesText.setText('Moves: ' + movesLeft); // Flash screen red to indicate wasted move LK.effects.flashScreen(0xFF0000, 300); } }, 300); } selectedBall = null; } } }; game.move = function (x, y, obj) { if (draggedBomb && Date.now() - lastMoveTime > 16) { // Throttle to ~60fps draggedBomb.x = x; draggedBomb.y = y; lastMoveTime = Date.now(); } else if (draggedRocket && Date.now() - lastMoveTime > 16) { // Throttle to ~60fps draggedRocket.x = x; draggedRocket.y = y; lastMoveTime = Date.now(); } }; game.up = function (x, y, obj) { if (draggedBomb) { // Explode bomb at current position draggedBomb.explode3x3(); // Using a bomb costs 2 moves in classic and freeze frenzy mode if (gameMode === 'classic' || gameMode === 'freezefrenzy') { movesLeft -= 2; movesText.setText('Moves: ' + movesLeft); } // Process any resulting matches LK.setTimeout(function () { dropBalls(); fillEmptySpaces(); processMatches(); }, 100); draggedBomb = null; } else if (draggedRocket) { // Explode rocket at current position with 10x2 pattern draggedRocket.explode10x2(); // Using a rocket costs 5 moves in classic and freeze frenzy mode if (gameMode === 'classic' || gameMode === 'freezefrenzy') { movesLeft -= 5; movesText.setText('Moves: ' + movesLeft); } // Process any resulting matches LK.setTimeout(function () { dropBalls(); fillEmptySpaces(); processMatches(); }, 100); draggedRocket = null; } }; // Add background - set after game mode is determined var backgroundImage; function setGameBackground() { if (backgroundImage) { backgroundImage.destroy(); } if (gameMode === 'freezefrenzy') { backgroundImage = game.attachAsset('Erek', { anchorX: 0, anchorY: 0, x: 0, y: 0, scaleX: 20.48, scaleY: 27.32, alpha: 0.3 }); } else { backgroundImage = game.attachAsset('background', { anchorX: 0, anchorY: 0, x: 0, y: 0, alpha: 0.3 }); } } // Set initial background setGameBackground(); // Create main menu function createMainMenu() { menuContainer = new Container(); game.addChild(menuContainer); // Menu background image - properly scaled to fill screen var menuBackgroundImage = menuContainer.attachAsset('Menu', { anchorX: 0, anchorY: 0, x: 0, y: 0, scaleX: 20.48, scaleY: 27.32, alpha: 0.8 }); // Menu background overlay for readability var menuBg = LK.getAsset('gridCell', { width: 2048, height: 2732, anchorX: 0, anchorY: 0, color: 0x1a1a2e, alpha: 0.7 }); menuContainer.addChild(menuBg); // Game title var titleText = new Text2('Feelings\nMatch', { size: 200, fill: '#FFD700' }); titleText.anchor.set(0.5, 0.5); titleText.x = 1024; titleText.y = 800; menuContainer.addChild(titleText); // Subtitle var subtitleText = new Text2('Swap colored balls to create matches!', { size: 80, fill: '#FFFFFF' }); subtitleText.anchor.set(0.5, 0.5); subtitleText.x = 1024; subtitleText.y = 1100; menuContainer.addChild(subtitleText); // Game mode buttons var classicButton = LK.getAsset('gridCell', { width: 400, height: 120, anchorX: 0.5, anchorY: 0.5, color: 0x4CAF50 }); classicButton.x = 1024; classicButton.y = 1400; menuContainer.addChild(classicButton); var classicButtonText = new Text2('CLASSIC', { size: 80, fill: '#FFFFFF' }); classicButtonText.anchor.set(0.5, 0.5); classicButton.addChild(classicButtonText); var timeTrialButton = LK.getAsset('gridCell', { width: 400, height: 120, anchorX: 0.5, anchorY: 0.5, color: 0xFF9800 }); timeTrialButton.x = 1024; timeTrialButton.y = 1600; menuContainer.addChild(timeTrialButton); var timeTrialButtonText = new Text2('TIME TRIAL', { size: 80, fill: '#FFFFFF' }); timeTrialButtonText.anchor.set(0.5, 0.5); timeTrialButton.addChild(timeTrialButtonText); // Freeze Frenzy button var freezeFrenzyButton = LK.getAsset('gridCell', { width: 400, height: 120, anchorX: 0.5, anchorY: 0.5, color: 0x87CEEB }); freezeFrenzyButton.x = 1024; freezeFrenzyButton.y = 1800; menuContainer.addChild(freezeFrenzyButton); var freezeFrenzyButtonText = new Text2('FREEZE FRENZY', { size: 80, fill: '#FFFFFF' }); freezeFrenzyButtonText.anchor.set(0.5, 0.5); freezeFrenzyButton.addChild(freezeFrenzyButtonText); // Instructions var instructionText = new Text2('• Create lines of 3+ matching balls\n• Classic: Limited moves, unlock levels\n• Time Trial: Race against the clock!\n• Freeze Frenzy: Melt frozen balls with 2 matches!', { size: 60, fill: '#CCCCCC' }); instructionText.anchor.set(0.5, 0.5); instructionText.x = 1024; instructionText.y = 2000; menuContainer.addChild(instructionText); // Animate title entrance titleText.scaleX = 0; titleText.scaleY = 0; titleText.alpha = 0; tween(titleText, { scaleX: 1, scaleY: 1, alpha: 1 }, { duration: 800, easing: tween.easeOut }); // Animate subtitle subtitleText.alpha = 0; tween(subtitleText, { alpha: 1 }, { duration: 600, delay: 400, easing: tween.easeOut }); // Animate game mode buttons classicButton.scaleX = 0; classicButton.scaleY = 0; tween(classicButton, { scaleX: 1, scaleY: 1 }, { duration: 500, delay: 800, easing: tween.easeOut }); timeTrialButton.scaleX = 0; timeTrialButton.scaleY = 0; tween(timeTrialButton, { scaleX: 1, scaleY: 1 }, { duration: 500, delay: 900, easing: tween.easeOut }); freezeFrenzyButton.scaleX = 0; freezeFrenzyButton.scaleY = 0; tween(freezeFrenzyButton, { scaleX: 1, scaleY: 1 }, { duration: 500, delay: 1000, easing: tween.easeOut }); // Animate instructions instructionText.alpha = 0; tween(instructionText, { alpha: 1 }, { duration: 600, delay: 1200, easing: tween.easeOut }); // Add pulsing animation to game mode buttons function pulseGameModeButtons() { tween(classicButton, { scaleX: 1.1, scaleY: 1.1 }, { duration: 800, easing: tween.easeInOut, onFinish: function onFinish() { tween(classicButton, { scaleX: 1, scaleY: 1 }, { duration: 800, easing: tween.easeInOut }); } }); tween(timeTrialButton, { scaleX: 1.05, scaleY: 1.05 }, { duration: 1000, easing: tween.easeInOut, onFinish: function onFinish() { tween(timeTrialButton, { scaleX: 1, scaleY: 1 }, { duration: 1000, easing: tween.easeInOut, onFinish: function onFinish() { if (gameState === 'menu') { pulseGameModeButtons(); } } }); } }); } // Start pulsing after initial animation LK.setTimeout(function () { if (gameState === 'menu') { pulseGameModeButtons(); } }, 1500); // Game mode button click handlers classicButton.down = function (x, y, obj) { startGame('classic'); }; timeTrialButton.down = function (x, y, obj) { startGame('timetrial'); }; freezeFrenzyButton.down = function (x, y, obj) { startGame('freezefrenzy'); }; return menuContainer; } // Function to start the actual game function startGame(mode) { gameState = 'playing'; gameMode = mode || 'classic'; // Reset combo system resetCombo(); // Set background for current game mode setGameBackground(); // Animate menu out tween(menuContainer, { alpha: 0, scaleX: 0.8, scaleY: 0.8 }, { duration: 500, easing: tween.easeIn, onFinish: function onFinish() { menuContainer.destroy(); // Initialize game elements createGameContainer(); initializeGrid(); // Setup UI based on game mode setupGameModeUI(); // Start background music LK.playMusic('Ana'); } }); } // Function to setup UI based on game mode function setupGameModeUI() { if (gameMode === 'timetrial') { // Hide moves counter and show timer movesText.visible = false; timerText.visible = true; timeLeft = timeTrialDuration; updateTimer(); gameTimer = LK.setInterval(updateTimer, 1000); // In time trial, give unlimited moves movesLeft = 999; } else if (gameMode === 'freezefrenzy') { // Freeze frenzy mode - show moves counter, hide timer movesText.visible = true; timerText.visible = false; movesLeft = 40; // Fixed 40 moves for freeze frenzy movesText.setText('Moves: ' + movesLeft); } else { // Classic mode - show moves counter, hide timer movesText.visible = true; timerText.visible = false; movesLeft = getMovesForLevel(1); movesText.setText('Moves: ' + movesLeft); } } // Function to create game container function createGameContainer() { gameContainer = new Container(); game.addChild(gameContainer); } // Function to show cinematic sequence function showCinematic() { cinematicContainer = new Container(); game.addChild(cinematicContainer); // Dark background for cinematic var cinematicBg = LK.getAsset('gridCell', { width: 2048, height: 2732, anchorX: 0, anchorY: 0, color: 0x000000, alpha: 1.0 }); cinematicContainer.addChild(cinematicBg); // Create title elements for cinematic var welcomeText = new Text2('Welcome to', { size: 120, fill: '#FFFFFF' }); welcomeText.anchor.set(0.5, 0.5); welcomeText.x = 1024; welcomeText.y = 800; welcomeText.alpha = 0; cinematicContainer.addChild(welcomeText); var titleText = new Text2('Feelings Match', { size: 180, fill: '#FFD700' }); titleText.anchor.set(0.5, 0.5); titleText.x = 1024; titleText.y = 1000; titleText.alpha = 0; cinematicContainer.addChild(titleText); var taglineText = new Text2('Where emotions align in perfect harmony', { size: 80, fill: '#CCCCCC' }); taglineText.anchor.set(0.5, 0.5); taglineText.x = 1024; taglineText.y = 1200; taglineText.alpha = 0; cinematicContainer.addChild(taglineText); var continueText = new Text2('Tap to continue...', { size: 60, fill: '#AAAAAA' }); continueText.anchor.set(0.5, 0.5); continueText.x = 1024; continueText.y = 1500; continueText.alpha = 0; cinematicContainer.addChild(continueText); // Cinematic animation sequence // Fade in welcome text tween(welcomeText, { alpha: 1 }, { duration: 1000, easing: tween.easeOut, onFinish: function onFinish() { // Fade in title with scale animation titleText.scaleX = 0.5; titleText.scaleY = 0.5; tween(titleText, { alpha: 1, scaleX: 1, scaleY: 1 }, { duration: 1200, easing: tween.easeOut, onFinish: function onFinish() { // Fade in tagline tween(taglineText, { alpha: 1 }, { duration: 800, easing: tween.easeOut, onFinish: function onFinish() { // Pulsing continue text tween(continueText, { alpha: 0.8 }, { duration: 600, easing: tween.easeInOut, onFinish: function onFinish() { // Start pulsing animation for continue text function pulseContinue() { tween(continueText, { alpha: 0.3 }, { duration: 1000, easing: tween.easeInOut, onFinish: function onFinish() { tween(continueText, { alpha: 0.8 }, { duration: 1000, easing: tween.easeInOut, onFinish: function onFinish() { if (gameState === 'cinematic') { pulseContinue(); } } }); } }); } pulseContinue(); } }); } }); } }); } }); // Make cinematic clickable to continue cinematicContainer.down = function (x, y, obj) { // Fade out cinematic and transition to menu gameState = 'menu'; tween(cinematicContainer, { alpha: 0 }, { duration: 800, easing: tween.easeIn, onFinish: function onFinish() { cinematicContainer.destroy(); createMainMenu(); } }); }; } // Start with cinematic sequence if (gameState === 'cinematic') { showCinematic(); } else if (gameState === 'menu') { createMainMenu(); } else { // Initialize the game initializeGrid(); } // Reset button for swapping balls var resetButton = LK.getAsset('gridCell', { width: 200, height: 80, anchorX: 0.5, anchorY: 0.5, color: 0xFF6B6B }); LK.gui.bottom.addChild(resetButton); resetButton.y = -100; var resetButtonText = new Text2('RESET', { size: 40, fill: 0xFFFFFF }); resetButtonText.anchor.set(0.5, 0.5); resetButton.addChild(resetButtonText); resetButton.down = function (x, y, obj) { if (isProcessingMatches) { return; } // Check if player has enough score to reset (cost 100 points) var resetCost = 100; if (LK.getScore() < resetCost) { // Flash screen red to indicate insufficient score LK.effects.flashScreen(0xFF0000, 400); return; } // Clear current selection selectedBall = null; // Reset combo system resetCombo(); // Clear the grid for (var y = 0; y < gridSize; y++) { for (var x = 0; x < gridSize; x++) { if (grid[y][x]) { grid[y][x].destroy(); grid[y][x] = null; } } } // Reinitialize the grid with new balls initializeGrid(); // Cost 100 score points to reset LK.setScore(LK.getScore() - resetCost); scoreText.setText('Score: ' + LK.getScore()); // Update progress bar var currentScore = LK.getScore(); // Calculate score needed for current level progression function getScoreForLevel(level) { if (level <= 1) return 0; var totalScore = 0; for (var i = 1; i < level; i++) { totalScore += 400 + (i - 1) * 150; } return totalScore; } function getScoreNeededForNextLevel(level) { return 400 + (level - 1) * 150; } var scoreInCurrentLevel = currentScore - getScoreForLevel(currentLevel); var scoreNeededThisLevel = getScoreNeededForNextLevel(currentLevel); var progress = Math.min(1, scoreInCurrentLevel / scoreNeededThisLevel); progressBarFill.width = progress * 400; progressText.setText(scoreInCurrentLevel + '/' + scoreNeededThisLevel); // Flash screen blue to indicate reset LK.effects.flashScreen(0x0066FF, 400); }; // Background music will be started when game begins from menu ; ;
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var Ball = Container.expand(function (color) {
var self = Container.call(this);
self.ballColor = color;
self.gridX = 0;
self.gridY = 0;
self.isAnimating = false;
var ballAssets = ['redBall', 'blueBall', 'greenBall', 'yellowBall', 'purpleBall', 'orangeBall'];
var assetToUse = ballAssets[color];
var ballGraphics = self.attachAsset(assetToUse, {
anchorX: 0.5,
anchorY: 0.5
});
self.setGridPosition = function (gridX, gridY) {
self.gridX = gridX;
self.gridY = gridY;
self.x = gridStartX + gridX * cellSize + cellSize / 2;
self.y = gridStartY + gridY * cellSize + cellSize / 2;
};
self.explode = function () {
// Scale up and fade out explosion effect
tween(self, {
scaleX: 1.3,
scaleY: 1.3,
alpha: 0
}, {
duration: 600,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (self.parent) {
self.destroy();
}
}
});
};
return self;
});
var Bomb = Container.expand(function () {
var self = Container.call(this);
self.gridX = 0;
self.gridY = 0;
self.isAnimating = false;
self.isBomb = true;
self.isDragging = false;
// Create bomb visual using the Bomba asset
var bombGraphics = self.attachAsset('Bomba', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.8,
scaleY: 0.8
});
// Add pulsing animation to make it stand out
self.pulseAnimation = function () {
tween(self, {
scaleX: 1.1,
scaleY: 1.1
}, {
duration: 500,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 0.8,
scaleY: 0.8
}, {
duration: 500,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (self.parent && !self.isDragging) {
self.pulseAnimation();
}
}
});
}
});
};
self.setGridPosition = function (gridX, gridY) {
self.gridX = gridX;
self.gridY = gridY;
self.x = gridStartX + gridX * cellSize + cellSize / 2;
self.y = gridStartY + gridY * cellSize + cellSize / 2;
};
self.explode = function () {
// Simple bomb explosion effect (same as Ball explode method)
tween(self, {
scaleX: 1.3,
scaleY: 1.3,
alpha: 0
}, {
duration: 600,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (self.parent) {
self.destroy();
}
}
});
};
self.explode3x3 = function () {
// Explode all balls in a 3x3 area around the bomb
var centerX = self.gridX;
var centerY = self.gridY;
var explodedCount = 0;
for (var dx = -1; dx <= 1; dx++) {
for (var dy = -1; dy <= 1; dy++) {
var targetX = centerX + dx;
var targetY = centerY + dy;
if (targetX >= 0 && targetX < gridSize && targetY >= 0 && targetY < gridSize) {
var ball = grid[targetY][targetX];
if (ball) {
// If it's a chained ball, break the chain first
if (ball.isChained) {
ball.breakChain();
// Give extra points for breaking chains with bombs
LK.setScore(LK.getScore() + 5);
}
ball.explode();
grid[targetY][targetX] = null;
explodedCount++;
}
}
}
}
// Award points for exploded balls (apply combo multiplier)
var bombPoints = Math.floor(explodedCount * 15 * comboMultiplier);
LK.setScore(LK.getScore() + bombPoints);
scoreText.setText('Score: ' + LK.getScore());
// Update progress bar
var currentScore = LK.getScore();
// Calculate score needed for current level progression
function getScoreForLevel(level) {
if (level <= 1) return 0;
var totalScore = 0;
for (var i = 1; i < level; i++) {
totalScore += 400 + (i - 1) * 150;
}
return totalScore;
}
function getScoreNeededForNextLevel(level) {
return 400 + (level - 1) * 150;
}
var scoreInCurrentLevel = currentScore - getScoreForLevel(currentLevel);
var scoreNeededThisLevel = getScoreNeededForNextLevel(currentLevel);
var progress = Math.min(1, scoreInCurrentLevel / scoreNeededThisLevel);
progressBarFill.width = progress * 400;
progressText.setText(scoreInCurrentLevel + '/' + scoreNeededThisLevel);
checkLevelProgression();
// Remove bomb from grid
grid[self.gridY][self.gridX] = null;
// Explosion effect for bomb itself
tween(self, {
scaleX: 2,
scaleY: 2,
alpha: 0
}, {
duration: 400,
easing: tween.easeOut,
onFinish: function onFinish() {
if (self.parent) {
self.destroy();
}
}
});
};
// Start pulsing animation when created
self.pulseAnimation();
return self;
});
var ChainedBall = Container.expand(function (color) {
var self = Container.call(this);
self.ballColor = color;
self.gridX = 0;
self.gridY = 0;
self.isAnimating = false;
self.isChained = true;
// Create chain background first (larger, darker)
var chainGraphics = self.attachAsset('chain', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.7
});
// Create ball on top of chain
var ballAssets = ['redBall', 'blueBall', 'greenBall', 'yellowBall', 'purpleBall', 'orangeBall'];
var assetToUse = ballAssets[color];
var ballGraphics = self.attachAsset(assetToUse, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.85,
scaleY: 0.85
});
// Add subtle chain animation
self.chainAnimation = function () {
tween(chainGraphics, {
rotation: 0.1
}, {
duration: 1000,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(chainGraphics, {
rotation: -0.1
}, {
duration: 1000,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (self.parent && self.isChained) {
self.chainAnimation();
}
}
});
}
});
};
self.setGridPosition = function (gridX, gridY) {
self.gridX = gridX;
self.gridY = gridY;
self.x = gridStartX + gridX * cellSize + cellSize / 2;
self.y = gridStartY + gridY * cellSize + cellSize / 2;
};
self.explode = function () {
// Scale up and fade out explosion effect
tween(self, {
scaleX: 1.3,
scaleY: 1.3,
alpha: 0
}, {
duration: 600,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (self.parent) {
self.destroy();
}
}
});
};
self.breakChain = function () {
// Break the chain and convert to regular ball
self.isChained = false;
// Animate chain breaking
tween(chainGraphics, {
scaleX: 1.5,
scaleY: 1.5,
alpha: 0
}, {
duration: 300,
easing: tween.easeOut
});
// Scale ball back to normal size
tween(ballGraphics, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 300,
easing: tween.easeOut
});
// Flash effect when chain breaks
LK.effects.flashObject(self, 0xFFFFFF, 400);
};
// Start chain animation
self.chainAnimation();
return self;
});
var FrozenBall = Container.expand(function (color) {
var self = Container.call(this);
self.ballColor = color;
self.gridX = 0;
self.gridY = 0;
self.isAnimating = false;
self.isFrozen = true;
self.matchesAround = 0; // Track how many matches are around this ball
var ballAssets = ['redBall', 'blueBall', 'greenBall', 'yellowBall', 'purpleBall', 'orangeBall'];
var assetToUse = ballAssets[color];
// Create ice background first (larger, semi-transparent white/blue)
var iceGraphics = self.attachAsset('Ice', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.8,
scaleX: 1.1,
scaleY: 1.1
});
// Create ball on top of ice
var ballGraphics = self.attachAsset(assetToUse, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.85,
scaleY: 0.85,
alpha: 0.8
});
// Add subtle frost animation
self.frostAnimation = function () {
tween(iceGraphics, {
alpha: 0.4
}, {
duration: 1200,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(iceGraphics, {
alpha: 0.6
}, {
duration: 1200,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (self.parent && self.isFrozen) {
self.frostAnimation();
}
}
});
}
});
};
self.setGridPosition = function (gridX, gridY) {
self.gridX = gridX;
self.gridY = gridY;
self.x = gridStartX + gridX * cellSize + cellSize / 2;
self.y = gridStartY + gridY * cellSize + cellSize / 2;
};
self.explode = function () {
// Scale up and fade out explosion effect
tween(self, {
scaleX: 1.3,
scaleY: 1.3,
alpha: 0
}, {
duration: 600,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (self.parent) {
self.destroy();
}
}
});
};
self.melt = function () {
// Melt the ice and convert to regular ball
self.isFrozen = false;
// Animate ice melting
tween(iceGraphics, {
scaleX: 1.5,
scaleY: 1.5,
alpha: 0
}, {
duration: 400,
easing: tween.easeOut
});
// Scale ball back to normal size and opacity
tween(ballGraphics, {
scaleX: 1.0,
scaleY: 1.0,
alpha: 1.0
}, {
duration: 400,
easing: tween.easeOut
});
// Flash effect when ice melts
LK.effects.flashObject(self, 0x00FFFF, 400);
};
self.checkForMelting = function () {
if (!self.isFrozen) return;
// Count matches around this frozen ball
var matchCount = 0;
var directions = [{
dx: -1,
dy: 0
}, {
dx: 1,
dy: 0
}, {
dx: 0,
dy: -1
}, {
dx: 0,
dy: 1
}];
for (var d = 0; d < directions.length; d++) {
var dir = directions[d];
var adjX = self.gridX + dir.dx;
var adjY = self.gridY + dir.dy;
if (adjX >= 0 && adjX < gridSize && adjY >= 0 && adjY < gridSize) {
var adjacentBall = grid[adjY][adjX];
if (adjacentBall && adjacentBall.ballColor !== undefined && !adjacentBall.isFrozen) {
// Check if this adjacent ball is part of a match
if (isPartOfMatch(adjX, adjY)) {
matchCount++;
}
}
}
}
// Melt if 2 or more matches around
if (matchCount >= 2) {
self.melt();
}
};
// Start frost animation
self.frostAnimation();
return self;
});
// Helper function to check if a ball is part of a match
var HealthBox = Container.expand(function () {
var self = Container.call(this);
self.gridX = 0;
self.gridY = 0;
self.isAnimating = false;
self.isHealthBox = true;
self.isDragging = false;
// Create health box visual using the healthBox asset
var healthGraphics = self.attachAsset('healthBox', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.8,
scaleY: 0.8
});
// Add gentle glow animation
self.glowAnimation = function () {
tween(self, {
alpha: 0.7
}, {
duration: 800,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(self, {
alpha: 1.0
}, {
duration: 800,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (self.parent && !self.isDragging) {
self.glowAnimation();
}
}
});
}
});
};
self.setGridPosition = function (gridX, gridY) {
self.gridX = gridX;
self.gridY = gridY;
self.x = gridStartX + gridX * cellSize + cellSize / 2;
self.y = gridStartY + gridY * cellSize + cellSize / 2;
};
self.explode = function () {
// Simple health box explosion effect
tween(self, {
scaleX: 1.3,
scaleY: 1.3,
alpha: 0
}, {
duration: 600,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (self.parent) {
self.destroy();
}
}
});
};
self.activateHealthBonus = function () {
// Add 8 moves when health box is clicked
movesLeft += 8;
movesText.setText('Moves: ' + movesLeft);
// Remove health box from grid
grid[self.gridY][self.gridX] = null;
// Visual effect for health box activation
tween(self, {
scaleX: 1.5,
scaleY: 1.5,
alpha: 0
}, {
duration: 400,
easing: tween.easeOut,
onFinish: function onFinish() {
if (self.parent) {
self.destroy();
}
// After health box is removed, trigger ball dropping and filling
LK.setTimeout(function () {
dropBalls();
fillEmptySpaces();
// Check for any new matches that might form
processMatches();
}, 100);
}
});
// Flash screen green to indicate health bonus
LK.effects.flashScreen(0x00FF00, 500);
};
// Start glow animation when created
self.glowAnimation();
return self;
});
var Rocket = Container.expand(function () {
var self = Container.call(this);
self.gridX = 0;
self.gridY = 0;
self.isAnimating = false;
self.isRocket = true;
self.isDragging = false;
// Create rocket visual using the Roket asset
var rocketGraphics = self.attachAsset('Roket', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.9,
scaleY: 0.9
});
// Add rocket animation to make it stand out with swaying and ignition effects
self.rocketAnimation = function () {
// Swaying motion
tween(self, {
rotation: 0.3,
x: self.x + 5
}, {
duration: 600,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(self, {
rotation: -0.3,
x: self.x - 10
}, {
duration: 600,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(self, {
rotation: 0,
x: gridStartX + self.gridX * cellSize + cellSize / 2
}, {
duration: 400,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (self.parent && !self.isDragging) {
self.rocketAnimation();
}
}
});
}
});
}
});
// Add ignition effect
self.ignitionEffect();
};
// Add ignition effect method
self.ignitionEffect = function () {
// Create pulsing glow effect to simulate rocket ignition
tween(rocketGraphics, {
alpha: 0.6
}, {
duration: 200,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(rocketGraphics, {
alpha: 1.0
}, {
duration: 200,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (self.parent && !self.isDragging) {
// Random interval for ignition effect to look more natural
var nextIgnition = 800 + Math.random() * 1200;
LK.setTimeout(function () {
if (self.parent && !self.isDragging) {
self.ignitionEffect();
}
}, nextIgnition);
}
}
});
}
});
};
self.setGridPosition = function (gridX, gridY) {
self.gridX = gridX;
self.gridY = gridY;
self.x = gridStartX + gridX * cellSize + cellSize / 2;
self.y = gridStartY + gridY * cellSize + cellSize / 2;
};
self.explode = function () {
// Simple rocket explosion effect
tween(self, {
scaleX: 1.3,
scaleY: 1.3,
alpha: 0
}, {
duration: 600,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (self.parent) {
self.destroy();
}
}
});
};
self.explode10x2 = function () {
// Explode all balls in a 2x10 area around the rocket
var centerX = self.gridX;
var centerY = self.gridY;
var explodedCount = 0;
// Create 2x10 explosion pattern (2 columns, 5 cells up, rocket center, 4 cells down)
for (var dx = -1; dx <= 0; dx++) {
for (var dy = -5; dy <= 4; dy++) {
var targetX = centerX + dx;
var targetY = centerY + dy;
if (targetX >= 0 && targetX < gridSize && targetY >= 0 && targetY < gridSize) {
var ball = grid[targetY][targetX];
if (ball) {
// If it's a chained ball, break the chain first
if (ball.isChained) {
ball.breakChain();
// Give extra points for breaking chains with rockets
LK.setScore(LK.getScore() + 5);
}
ball.explode();
grid[targetY][targetX] = null;
explodedCount++;
}
}
}
}
// Award points for exploded balls (apply combo multiplier)
var rocketPoints = Math.floor(explodedCount * 20 * comboMultiplier);
LK.setScore(LK.getScore() + rocketPoints);
scoreText.setText('Score: ' + LK.getScore());
// Update progress bar
var currentScore = LK.getScore();
// Calculate score needed for current level progression
function getScoreForLevel(level) {
if (level <= 1) return 0;
var totalScore = 0;
for (var i = 1; i < level; i++) {
totalScore += 400 + (i - 1) * 150;
}
return totalScore;
}
function getScoreNeededForNextLevel(level) {
return 400 + (level - 1) * 150;
}
var scoreInCurrentLevel = currentScore - getScoreForLevel(currentLevel);
var scoreNeededThisLevel = getScoreNeededForNextLevel(currentLevel);
var progress = Math.min(1, scoreInCurrentLevel / scoreNeededThisLevel);
progressBarFill.width = progress * 400;
progressText.setText(scoreInCurrentLevel + '/' + scoreNeededThisLevel);
checkLevelProgression();
// Remove rocket from grid
grid[self.gridY][self.gridX] = null;
// Explosion effect for rocket itself
tween(self, {
scaleX: 2.5,
scaleY: 2.5,
alpha: 0
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
if (self.parent) {
self.destroy();
}
}
});
};
// Start rocket animation when created
self.rocketAnimation();
return self;
});
var TimeBonus = Container.expand(function () {
var self = Container.call(this);
self.gridX = 0;
self.gridY = 0;
self.isAnimating = false;
self.isTimeBonus = true;
self.isDragging = false;
// Create time bonus visual using the Saat asset
var timeBonusGraphics = self.attachAsset('Saat', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.9,
scaleY: 0.9
});
// Add clock-like pulsing animation
self.clockAnimation = function () {
tween(self, {
scaleX: 1.1,
scaleY: 1.1
}, {
duration: 600,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 0.9,
scaleY: 0.9
}, {
duration: 600,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (self.parent && !self.isDragging) {
self.clockAnimation();
}
}
});
}
});
};
self.setGridPosition = function (gridX, gridY) {
self.gridX = gridX;
self.gridY = gridY;
self.x = gridStartX + gridX * cellSize + cellSize / 2;
self.y = gridStartY + gridY * cellSize + cellSize / 2;
};
self.explode = function () {
// Simple time bonus explosion effect
tween(self, {
scaleX: 1.3,
scaleY: 1.3,
alpha: 0
}, {
duration: 600,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (self.parent) {
self.destroy();
}
}
});
};
self.activateTimeBonus = function () {
// Add 10 seconds when time bonus is clicked in time trial mode
if (gameMode === 'timetrial') {
timeLeft += 10;
updateTimer();
}
// Remove time bonus from grid
grid[self.gridY][self.gridX] = null;
// Visual effect for time bonus activation
tween(self, {
scaleX: 1.5,
scaleY: 1.5,
alpha: 0
}, {
duration: 400,
easing: tween.easeOut,
onFinish: function onFinish() {
if (self.parent) {
self.destroy();
}
// After time bonus is removed, trigger ball dropping and filling
LK.setTimeout(function () {
dropBalls();
fillEmptySpaces();
// Check for any new matches that might form
processMatches();
}, 100);
}
});
// Flash screen cyan to indicate time bonus
LK.effects.flashScreen(0x00FFFF, 500);
};
// Start clock animation when created
self.clockAnimation();
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x2c3e50
});
/****
* Game Code
****/
// Helper function to check if a ball is part of a match
// Game state management
function isPartOfMatch(x, y) {
var ball = grid[y][x];
if (!ball || ball.ballColor === undefined) return false;
var color = ball.ballColor;
// Check horizontal match
var horizCount = 1;
// Check left
for (var i = x - 1; i >= 0; i--) {
var leftBall = grid[y][i];
if (leftBall && leftBall.ballColor === color && !leftBall.isFrozen) {
horizCount++;
} else {
break;
}
}
// Check right
for (var i = x + 1; i < gridSize; i++) {
var rightBall = grid[y][i];
if (rightBall && rightBall.ballColor === color && !rightBall.isFrozen) {
horizCount++;
} else {
break;
}
}
if (horizCount >= 3) return true;
// Check vertical match
var vertCount = 1;
// Check up
for (var i = y - 1; i >= 0; i--) {
var upBall = grid[i][x];
if (upBall && upBall.ballColor === color && !upBall.isFrozen) {
vertCount++;
} else {
break;
}
}
// Check down
for (var i = y + 1; i < gridSize; i++) {
var downBall = grid[i][x];
if (downBall && downBall.ballColor === color && !downBall.isFrozen) {
vertCount++;
} else {
break;
}
}
return vertCount >= 3;
}
var gameState = 'cinematic'; // 'cinematic', 'menu' or 'playing'
var gameMode = 'classic'; // 'classic', 'timetrial', or 'freezefrenzy'
var menuContainer;
var gameContainer;
var cinematicContainer;
// Time trial specific variables
var timeTrialDuration = 120; // 2 minutes in seconds
var timeLeft = timeTrialDuration;
var timerText;
var gameTimer;
// Grid configuration variables needed by Ball class
var gridSize = 8;
var cellSize = 220;
var gridStartX = (2048 - gridSize * cellSize) / 2;
var gridStartY = 400;
var grid = [];
var gridShape = []; // Array to define which cells are active in current level
// Define different grid shapes for each level
var levelShapes = [
// Level 1: Full 8x8 square
function (size) {
var shape = [];
for (var y = 0; y < size; y++) {
shape[y] = [];
for (var x = 0; x < size; x++) {
shape[y][x] = true;
}
}
return shape;
},
// Level 2: Diamond shape
function (size) {
var shape = [];
var center = Math.floor(size / 2);
for (var y = 0; y < size; y++) {
shape[y] = [];
for (var x = 0; x < size; x++) {
var distanceFromCenter = Math.abs(x - center) + Math.abs(y - center);
shape[y][x] = distanceFromCenter <= center;
}
}
return shape;
},
// Level 3: Cross shape
function (size) {
var shape = [];
var center = Math.floor(size / 2);
for (var y = 0; y < size; y++) {
shape[y] = [];
for (var x = 0; x < size; x++) {
shape[y][x] = x === center || y === center;
}
}
return shape;
},
// Level 4: Hollow square (border only)
function (size) {
var shape = [];
for (var y = 0; y < size; y++) {
shape[y] = [];
for (var x = 0; x < size; x++) {
shape[y][x] = x === 0 || x === size - 1 || y === 0 || y === size - 1;
}
}
return shape;
},
// Level 5: L shape
function (size) {
var shape = [];
for (var y = 0; y < size; y++) {
shape[y] = [];
for (var x = 0; x < size; x++) {
shape[y][x] = x >= 0 && x < 4 && y >= size - 7 || y >= size - 4 && x >= 0 && x < 7;
}
}
return shape;
},
// Level 6: Circle shape
function (size) {
var shape = [];
var center = size / 2 - 0.5;
var radius = center;
for (var y = 0; y < size; y++) {
shape[y] = [];
for (var x = 0; x < size; x++) {
var dx = x - center;
var dy = y - center;
var distance = Math.sqrt(dx * dx + dy * dy);
shape[y][x] = distance <= radius;
}
}
return shape;
}];
// Function to get current level's grid shape
function getCurrentGridShape() {
var shapeIndex = (currentLevel - 1) % levelShapes.length;
return levelShapes[shapeIndex](gridSize);
}
// Function to check if a grid position is valid for current shape
function isValidGridPosition(x, y) {
return gridShape[y] && gridShape[y][x];
}
var selectedBall = null;
var movesLeft = getMovesForLevel(1); // Start with moves calculated for level 1
var isProcessingMatches = false;
var ballColors = 4; // Start with 4 colors for easier beginning
var bombSpawnChance = 0.01; // 1% chance to spawn bomb instead of ball
var rocketSpawnChance = 0.025; // 2.5% chance to spawn rocket instead of ball
var healthBoxSpawnChance = 0.04; // 4% chance to spawn health box instead of ball
var timeBonusSpawnChance = 0.03; // 3% chance to spawn time bonus instead of ball (time trial mode only)
var draggedBomb = null;
var draggedRocket = null;
var lastMoveTime = 0;
var currentLevel = 1;
var baseMovesPerLevel = 30;
var pointsToNextLevel = 400; // Points needed to advance to next level
var totalPointsEarned = 0;
// Combo system variables
var currentCombo = 0;
var comboText;
var comboMultiplier = 1;
var maxCombo = 0;
var comboTimeout;
// Function to calculate moves for current level
function getMovesForLevel(level) {
// Start with 40 moves at level 1, decrease by 3 moves per level, minimum 15 moves
return Math.max(15, 40 - (level - 1) * 3);
}
// Combo system functions
function updateCombo() {
currentCombo++;
maxCombo = Math.max(maxCombo, currentCombo);
// Calculate multiplier based on combo (1x, 1.5x, 2x, 2.5x, 3x, max)
comboMultiplier = 1 + Math.min(2, (currentCombo - 1) * 0.5);
// Show combo text with animation
if (currentCombo >= 2) {
comboText.setText('COMBO x' + currentCombo + '\n' + comboMultiplier.toFixed(1) + 'x POINTS!');
comboText.visible = true;
// Animate combo text with more prominent effects
comboText.scaleX = 0.3;
comboText.scaleY = 0.3;
comboText.alpha = 0;
tween(comboText, {
scaleX: 1.4,
scaleY: 1.4,
alpha: 1
}, {
duration: 400,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(comboText, {
scaleX: 1.1,
scaleY: 1.1
}, {
duration: 300,
easing: tween.easeOut
});
}
});
// Change color based on combo level
var comboColor = '#FF6B6B'; // Red for 2-3
if (currentCombo >= 4 && currentCombo < 6) {
comboColor = '#FF9500'; // Orange for 4-5
} else if (currentCombo >= 6 && currentCombo < 8) {
comboColor = '#FFD700'; // Gold for 6-7
} else if (currentCombo >= 8) {
comboColor = '#9D4EDD'; // Purple for 8+
}
comboText.fill = comboColor;
// Screen flash effect for high combos
if (currentCombo >= 4) {
var flashColor = 0xFF9500;
if (currentCombo >= 6) flashColor = 0xFFD700;
if (currentCombo >= 8) flashColor = 0x9D4EDD;
LK.effects.flashScreen(flashColor, 300);
}
}
// Clear existing timeout and set new one
if (comboTimeout) {
LK.clearTimeout(comboTimeout);
}
comboTimeout = LK.setTimeout(function () {
resetCombo();
}, 3000); // Reset combo after 3 seconds of no matches
}
function resetCombo() {
if (currentCombo >= 2) {
// Fade out combo text
tween(comboText, {
alpha: 0,
scaleX: 0.3,
scaleY: 0.3
}, {
duration: 500,
easing: tween.easeIn,
onFinish: function onFinish() {
comboText.visible = false;
}
});
}
currentCombo = 0;
comboMultiplier = 1;
if (comboTimeout) {
LK.clearTimeout(comboTimeout);
comboTimeout = null;
}
}
// UI Elements
var movesText = new Text2('Moves: ' + getMovesForLevel(1), {
size: 80,
fill: 0xFFFFFF
});
movesText.anchor.set(0.5, 0);
LK.gui.top.addChild(movesText);
movesText.y = 150;
// Timer text for time trial mode
timerText = new Text2('Time: 2:00', {
size: 80,
fill: 0xFFFFFF
});
timerText.anchor.set(0.5, 0);
LK.gui.top.addChild(timerText);
timerText.y = 150;
timerText.visible = false; // Hidden by default
// Time trial timer function
function updateTimer() {
timeLeft--;
var minutes = Math.floor(timeLeft / 60);
var seconds = timeLeft % 60;
var timeString = minutes + ':' + (seconds < 10 ? '0' : '') + seconds;
timerText.setText('Time: ' + timeString);
if (timeLeft <= 0) {
LK.clearInterval(gameTimer);
LK.showGameOver();
} else if (timeLeft <= 10) {
// Flash red when time is running out
LK.effects.flashObject(timerText, 0xFF0000, 200);
}
}
var scoreText = new Text2('Score: 0', {
size: 80,
fill: 0xFFFFFF
});
scoreText.anchor.set(0, 0);
LK.gui.top.addChild(scoreText);
scoreText.x = 150;
scoreText.y = 250;
// Progress bar to show points needed for next level
var progressBarBackground = LK.getAsset('gridCell', {
width: 400,
height: 40,
anchorX: 1.0,
anchorY: 0,
alpha: 0.3
});
LK.gui.topRight.addChild(progressBarBackground);
progressBarBackground.x = -50;
progressBarBackground.y = 150;
var progressBarFill = LK.getAsset('gridCell', {
width: 1,
height: 40,
anchorX: 0,
anchorY: 0,
color: 0x00FF00
});
progressBarBackground.addChild(progressBarFill);
var progressText = new Text2('0/400', {
size: 50,
fill: 0xFFFFFF
});
progressText.anchor.set(1, 0);
LK.gui.topRight.addChild(progressText);
progressText.x = -50;
progressText.y = 200;
// Combo text display - moved to top left and made more prominent
comboText = new Text2('', {
size: 150,
fill: '#FF6B6B'
});
comboText.anchor.set(0, 0);
LK.gui.topLeft.addChild(comboText);
comboText.x = 150;
comboText.y = 50;
comboText.visible = false;
// Initialize grid
function initializeGrid() {
// Get the shape for current level (freeze frenzy always uses full 8x8)
if (gameMode === 'freezefrenzy') {
gridShape = levelShapes[0](gridSize); // Always use full 8x8 square
} else {
gridShape = getCurrentGridShape();
}
grid = [];
for (var y = 0; y < gridSize; y++) {
grid[y] = [];
for (var x = 0; x < gridSize; x++) {
grid[y][x] = null;
}
}
// Fill grid with balls only in valid positions
for (var y = 0; y < gridSize; y++) {
for (var x = 0; x < gridSize; x++) {
if (isValidGridPosition(x, y)) {
createBallAt(x, y);
}
}
}
// Remove initial matches
while (findMatches().length > 0) {
removeMatches();
dropBalls();
fillEmptySpaces();
}
}
function createBallAt(x, y) {
// Only create balls in valid grid positions
if (!isValidGridPosition(x, y)) {
return null;
}
// Also check if position is within grid bounds
if (x < 0 || x >= gridSize || y < 0 || y >= gridSize) {
return null;
}
// Freeze frenzy mode - no bombs, chained balls, or health boxes
if (gameMode === 'freezefrenzy') {
var color = Math.floor(Math.random() * ballColors);
// 30% chance to create frozen ball
if (Math.random() < 0.3) {
var frozenBall = new FrozenBall(color);
frozenBall.setGridPosition(x, y);
grid[y][x] = frozenBall;
game.addChild(frozenBall);
// Animate frozen ball appearing
frozenBall.scaleX = 0;
frozenBall.scaleY = 0;
frozenBall.alpha = 0;
tween(frozenBall, {
scaleX: 1,
scaleY: 1,
alpha: 1
}, {
duration: 400,
easing: tween.easeOut
});
return frozenBall;
} else {
// Create regular ball
var ball = new Ball(color);
ball.setGridPosition(x, y);
grid[y][x] = ball;
game.addChild(ball);
// Animate ball appearing
ball.scaleX = 0;
ball.scaleY = 0;
ball.alpha = 0;
tween(ball, {
scaleX: 1,
scaleY: 1,
alpha: 1
}, {
duration: 400,
easing: tween.easeOut
});
return ball;
}
}
// Sometimes create a bomb instead of a ball
if (Math.random() < bombSpawnChance) {
var bomb = new Bomb();
bomb.setGridPosition(x, y);
grid[y][x] = bomb;
game.addChild(bomb);
// Animate bomb appearing
bomb.scaleX = 0;
bomb.scaleY = 0;
bomb.alpha = 0;
tween(bomb, {
scaleX: 0.8,
scaleY: 0.8,
alpha: 1
}, {
duration: 400,
easing: tween.easeOut
});
return bomb;
} else if (Math.random() < rocketSpawnChance) {
var rocket = new Rocket();
rocket.setGridPosition(x, y);
grid[y][x] = rocket;
game.addChild(rocket);
// Animate rocket appearing
rocket.scaleX = 0;
rocket.scaleY = 0;
rocket.alpha = 0;
tween(rocket, {
scaleX: 0.9,
scaleY: 0.9,
alpha: 1
}, {
duration: 400,
easing: tween.easeOut
});
return rocket;
} else if (gameMode === 'classic' && Math.random() < healthBoxSpawnChance) {
var healthBox = new HealthBox();
healthBox.setGridPosition(x, y);
grid[y][x] = healthBox;
game.addChild(healthBox);
// Animate health box appearing
healthBox.scaleX = 0;
healthBox.scaleY = 0;
healthBox.alpha = 0;
tween(healthBox, {
scaleX: 0.8,
scaleY: 0.8,
alpha: 1
}, {
duration: 400,
easing: tween.easeOut
});
return healthBox;
} else if (gameMode === 'timetrial' && Math.random() < timeBonusSpawnChance) {
var timeBonus = new TimeBonus();
timeBonus.setGridPosition(x, y);
grid[y][x] = timeBonus;
game.addChild(timeBonus);
// Animate time bonus appearing
timeBonus.scaleX = 0;
timeBonus.scaleY = 0;
timeBonus.alpha = 0;
tween(timeBonus, {
scaleX: 0.9,
scaleY: 0.9,
alpha: 1
}, {
duration: 400,
easing: tween.easeOut
});
return timeBonus;
} else if (Math.random() < 0.05) {
// 5% chance to create chained ball - use smart color selection
var adjacentColors = [];
// Check all 4 directions for adjacent ball colors
var directions = [{
dx: -1,
dy: 0
}, {
dx: 1,
dy: 0
}, {
dx: 0,
dy: -1
}, {
dx: 0,
dy: 1
}];
for (var d = 0; d < directions.length; d++) {
var dir = directions[d];
var adjX = x + dir.dx;
var adjY = y + dir.dy;
if (adjX >= 0 && adjX < gridSize && adjY >= 0 && adjY < gridSize && isValidGridPosition(adjX, adjY) && grid[adjY][adjX] && grid[adjY][adjX].ballColor !== undefined) {
adjacentColors.push(grid[adjY][adjX].ballColor);
}
}
var color;
if (adjacentColors.length > 0 && Math.random() < 0.4) {
// 40% chance to match an adjacent color for potential future matches
color = adjacentColors[Math.floor(Math.random() * adjacentColors.length)];
} else {
color = Math.floor(Math.random() * ballColors);
}
var chainedBall = new ChainedBall(color);
chainedBall.setGridPosition(x, y);
grid[y][x] = chainedBall;
game.addChild(chainedBall);
// Animate chained ball appearing
chainedBall.scaleX = 0;
chainedBall.scaleY = 0;
chainedBall.alpha = 0;
tween(chainedBall, {
scaleX: 1,
scaleY: 1,
alpha: 1
}, {
duration: 400,
easing: tween.easeOut
});
return chainedBall;
} else {
// Smart color selection for regular balls
var adjacentColors = [];
var belowColors = [];
// Check all 4 directions for adjacent ball colors
var directions = [{
dx: -1,
dy: 0
}, {
dx: 1,
dy: 0
}, {
dx: 0,
dy: -1
}, {
dx: 0,
dy: 1
}];
for (var d = 0; d < directions.length; d++) {
var dir = directions[d];
var adjX = x + dir.dx;
var adjY = y + dir.dy;
if (adjX >= 0 && adjX < gridSize && adjY >= 0 && adjY < gridSize && isValidGridPosition(adjX, adjY) && grid[adjY][adjX] && grid[adjY][adjX].ballColor !== undefined) {
adjacentColors.push(grid[adjY][adjX].ballColor);
// Special attention to ball below for dropping compatibility
if (dir.dy === 1) {
belowColors.push(grid[adjY][adjX].ballColor);
}
}
}
var color;
// 53% chance to be compatible with environment if adjacent balls exist
if (adjacentColors.length > 0 && Math.random() < 0.53) {
// 60% chance to match an adjacent color (for potential matches)
if (Math.random() < 0.6) {
// Prioritize matching ball below if it exists
if (belowColors.length > 0) {
color = belowColors[Math.floor(Math.random() * belowColors.length)];
} else {
color = adjacentColors[Math.floor(Math.random() * adjacentColors.length)];
}
} else {
// 40% chance to choose a color that would create diversity but still be strategic
var usedColors = {};
for (var i = 0; i < adjacentColors.length; i++) {
usedColors[adjacentColors[i]] = true;
}
var availableColors = [];
for (var c = 0; c < ballColors; c++) {
if (!usedColors[c]) {
availableColors.push(c);
}
}
if (availableColors.length > 0) {
color = availableColors[Math.floor(Math.random() * availableColors.length)];
} else {
color = Math.floor(Math.random() * ballColors);
}
}
} else {
// Random color when no adjacent balls or 50% random chance
color = Math.floor(Math.random() * ballColors);
}
var ball = new Ball(color);
ball.setGridPosition(x, y);
grid[y][x] = ball;
game.addChild(ball);
// Animate ball appearing with scale and bounce
ball.scaleX = 0;
ball.scaleY = 0;
ball.alpha = 0;
tween(ball, {
scaleX: 1,
scaleY: 1,
alpha: 1
}, {
duration: 400,
easing: tween.easeOut
});
return ball;
}
}
function getBallAt(x, y) {
if (x < 0 || x >= gridSize || y < 0 || y >= gridSize) {
return null;
}
if (!grid || !grid[y]) {
return null;
}
return grid[y][x];
}
function swapBalls(ball1, ball2) {
if (!ball1 || !ball2) {
return false;
}
// Check if either ball is chained or frozen - they cannot be moved
if (ball1.isChained && ball1.isChained === true || ball2.isChained && ball2.isChained === true || ball1.isFrozen && ball1.isFrozen === true || ball2.isFrozen && ball2.isFrozen === true) {
// Flash red to indicate chained/frozen balls cannot move
LK.effects.flashScreen(0xFF0000, 200);
return false;
}
var x1 = ball1.gridX,
y1 = ball1.gridY;
var x2 = ball2.gridX,
y2 = ball2.gridY;
// Check if balls are adjacent
var dx = Math.abs(x1 - x2);
var dy = Math.abs(y1 - y2);
if (dx + dy !== 1) {
return false;
}
// Mark balls as animating
ball1.isAnimating = true;
ball2.isAnimating = true;
// Store old positions
var oldX1 = ball1.x,
oldY1 = ball1.y;
var oldX2 = ball2.x,
oldY2 = ball2.y;
// Swap positions in grid
grid[y1][x1] = ball2;
grid[y2][x2] = ball1;
// Update ball positions
ball1.setGridPosition(x2, y2);
ball2.setGridPosition(x1, y1);
// Store new positions
var newX1 = ball1.x,
newY1 = ball1.y;
var newX2 = ball2.x,
newY2 = ball2.y;
// Reset to old positions for animation
ball1.x = oldX1;
ball1.y = oldY1;
ball2.x = oldX2;
ball2.y = oldY2;
// Animate to new positions
tween(ball1, {
x: newX1,
y: newY1
}, {
duration: 250,
easing: tween.easeInOut,
onFinish: function onFinish() {
ball1.isAnimating = false;
}
});
tween(ball2, {
x: newX2,
y: newY2
}, {
duration: 250,
easing: tween.easeInOut,
onFinish: function onFinish() {
ball2.isAnimating = false;
}
});
LK.getSound('swap').play();
return true;
}
function findMatches() {
var matches = [];
// Check horizontal matches
for (var y = 0; y < gridSize; y++) {
var count = 0;
var currentColor = -1;
var matchPositions = [];
for (var x = 0; x < gridSize; x++) {
if (!isValidGridPosition(x, y)) {
// Invalid position, reset match counting
if (count >= 3) {
for (var i = 0; i < matchPositions.length; i++) {
matches.push(matchPositions[i]);
}
}
count = 0;
currentColor = -1;
matchPositions = [];
continue;
}
var ball = grid[y][x];
if (ball && ball.ballColor !== undefined && ball.ballColor === currentColor && !ball.isFrozen) {
count++;
matchPositions.push({
x: x,
y: y
});
} else {
if (count >= 3) {
for (var i = 0; i < matchPositions.length; i++) {
matches.push(matchPositions[i]);
}
}
count = ball && ball.ballColor !== undefined && !ball.isFrozen ? 1 : 0;
currentColor = ball && ball.ballColor !== undefined && !ball.isFrozen ? ball.ballColor : -1;
matchPositions = ball && ball.ballColor !== undefined && !ball.isFrozen ? [{
x: x,
y: y
}] : [];
}
}
if (count >= 3) {
for (var i = 0; i < matchPositions.length; i++) {
matches.push(matchPositions[i]);
}
}
}
// Check vertical matches
for (var x = 0; x < gridSize; x++) {
var count = 0;
var currentColor = -1;
var matchPositions = [];
for (var y = 0; y < gridSize; y++) {
if (!isValidGridPosition(x, y)) {
// Invalid position, reset match counting
if (count >= 3) {
for (var i = 0; i < matchPositions.length; i++) {
matches.push(matchPositions[i]);
}
}
count = 0;
currentColor = -1;
matchPositions = [];
continue;
}
var ball = grid[y][x];
if (ball && ball.ballColor !== undefined && ball.ballColor === currentColor && !ball.isFrozen) {
count++;
matchPositions.push({
x: x,
y: y
});
} else {
if (count >= 3) {
for (var i = 0; i < matchPositions.length; i++) {
matches.push(matchPositions[i]);
}
}
count = ball && ball.ballColor !== undefined && !ball.isFrozen ? 1 : 0;
currentColor = ball && ball.ballColor !== undefined && !ball.isFrozen ? ball.ballColor : -1;
matchPositions = ball && ball.ballColor !== undefined && !ball.isFrozen ? [{
x: x,
y: y
}] : [];
}
}
if (count >= 3) {
for (var i = 0; i < matchPositions.length; i++) {
matches.push(matchPositions[i]);
}
}
}
// Check diagonal matches (top-left to bottom-right)
for (var startY = 0; startY < gridSize; startY++) {
for (var startX = 0; startX < gridSize; startX++) {
if (!isValidGridPosition(startX, startY)) continue;
var startBall = grid[startY][startX];
if (!startBall || startBall.ballColor === undefined) continue;
var count = 1;
var currentColor = startBall.ballColor;
var matchPositions = [{
x: startX,
y: startY
}];
for (var step = 1; startX + step < gridSize && startY + step < gridSize; step++) {
var x = startX + step;
var y = startY + step;
if (!isValidGridPosition(x, y)) break;
var ball = grid[y][x];
if (ball && ball.ballColor === currentColor && !ball.isFrozen) {
count++;
matchPositions.push({
x: x,
y: y
});
} else {
break;
}
}
if (count >= 3) {
for (var i = 0; i < matchPositions.length; i++) {
matches.push(matchPositions[i]);
}
}
}
}
// Check diagonal matches (top-right to bottom-left)
for (var startY = 0; startY < gridSize; startY++) {
for (var startX = gridSize - 1; startX >= 0; startX--) {
if (!isValidGridPosition(startX, startY)) continue;
var startBall = grid[startY][startX];
if (!startBall || startBall.ballColor === undefined) continue;
var count = 1;
var currentColor = startBall.ballColor;
var matchPositions = [{
x: startX,
y: startY
}];
for (var step = 1; startX - step >= 0 && startY + step < gridSize; step++) {
var x = startX - step;
var y = startY + step;
if (!isValidGridPosition(x, y)) break;
var ball = grid[y][x];
if (ball && ball.ballColor === currentColor && !ball.isFrozen) {
count++;
matchPositions.push({
x: x,
y: y
});
} else {
break;
}
}
if (count >= 3) {
for (var i = 0; i < matchPositions.length; i++) {
matches.push(matchPositions[i]);
}
}
}
}
return matches;
}
function removeMatches() {
var matches = findMatches();
if (matches.length === 0) {
// Reset combo when no matches found
resetCombo();
return false;
}
// Update combo system
updateCombo();
var pointsEarned = matches.length * 10;
// Apply combo multiplier to points
pointsEarned = Math.floor(pointsEarned * comboMultiplier);
// Check if this is a 4-ball match for special rewards
var isFourBallMatch = matches.length === 4;
if (isFourBallMatch) {
// Award bonus 10 points for 4-ball match (also affected by multiplier)
pointsEarned += Math.floor(10 * comboMultiplier);
}
for (var i = 0; i < matches.length; i++) {
var match = matches[i];
var ball = grid[match.y][match.x];
if (ball) {
// If it's a chained ball, break the chain first then explode
if (ball.isChained) {
ball.breakChain();
// Give extra points for breaking chains
pointsEarned += 5;
}
ball.explode();
grid[match.y][match.x] = null;
}
}
LK.setScore(LK.getScore() + pointsEarned);
scoreText.setText('Score: ' + LK.getScore());
// Update progress bar
var currentScore = LK.getScore();
// Calculate score needed for current level progression
function getScoreForLevel(level) {
if (level <= 1) return 0;
var totalScore = 0;
for (var i = 1; i < level; i++) {
totalScore += 400 + (i - 1) * 150;
}
return totalScore;
}
function getScoreNeededForNextLevel(level) {
return 400 + (level - 1) * 150;
}
var scoreInCurrentLevel = currentScore - getScoreForLevel(currentLevel);
var scoreNeededThisLevel = getScoreNeededForNextLevel(currentLevel);
var progress = Math.min(1, scoreInCurrentLevel / scoreNeededThisLevel);
progressBarFill.width = progress * 400;
progressText.setText(scoreInCurrentLevel + '/' + scoreNeededThisLevel);
// Spawn a bomb as reward for 4-ball match
if (isFourBallMatch) {
// Find a random empty valid position to place the bomb
var emptyPositions = [];
for (var y = 0; y < gridSize; y++) {
for (var x = 0; x < gridSize; x++) {
if (grid[y][x] === null && isValidGridPosition(x, y)) {
emptyPositions.push({
x: x,
y: y
});
}
}
}
if (emptyPositions.length > 0) {
var randomPos = emptyPositions[Math.floor(Math.random() * emptyPositions.length)];
var rewardBomb = new Bomb();
rewardBomb.setGridPosition(randomPos.x, randomPos.y);
grid[randomPos.y][randomPos.x] = rewardBomb;
game.addChild(rewardBomb);
// Animate bomb appearing with special effect
rewardBomb.scaleX = 0;
rewardBomb.scaleY = 0;
rewardBomb.alpha = 0;
tween(rewardBomb, {
scaleX: 1.2,
scaleY: 1.2,
alpha: 1
}, {
duration: 600,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(rewardBomb, {
scaleX: 0.8,
scaleY: 0.8
}, {
duration: 200,
easing: tween.easeOut
});
}
});
// Flash screen gold to indicate bomb reward
LK.effects.flashScreen(0xFFD700, 500);
}
}
// Check for frozen balls that should melt after matches
if (gameMode === 'freezefrenzy') {
for (var y = 0; y < gridSize; y++) {
for (var x = 0; x < gridSize; x++) {
var ball = grid[y][x];
if (ball && ball.isFrozen && ball.checkForMelting) {
ball.checkForMelting();
}
}
}
}
LK.getSound('match').play();
checkLevelProgression();
return true;
}
function dropBalls() {
for (var x = 0; x < gridSize; x++) {
var writePos = gridSize - 1;
// Find the lowest valid position for this column
while (writePos >= 0 && !isValidGridPosition(x, writePos)) {
writePos--;
}
for (var y = gridSize - 1; y >= 0; y--) {
if (grid[y][x] !== null && isValidGridPosition(x, y)) {
if (y !== writePos && writePos >= 0) {
grid[writePos][x] = grid[y][x];
grid[y][x] = null;
var ball = grid[writePos][x];
if (ball) {
ball.isAnimating = true;
var oldY = ball.y;
ball.setGridPosition(x, writePos);
var newY = ball.y;
ball.y = oldY;
// Animate ball falling down
(function (ballToAnimate) {
tween(ballToAnimate, {
y: newY
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
ballToAnimate.isAnimating = false;
}
});
})(ball);
}
}
// Only decrement writePos if we're at a valid position
if (writePos >= 0) {
writePos--;
// Find next valid position
while (writePos >= 0 && !isValidGridPosition(x, writePos)) {
writePos--;
}
}
}
}
}
}
function fillEmptySpaces() {
for (var x = 0; x < gridSize; x++) {
for (var y = 0; y < gridSize; y++) {
if (grid[y][x] === null && isValidGridPosition(x, y)) {
createBallAt(x, y);
}
}
}
}
function processMatches() {
if (isProcessingMatches) {
return;
}
isProcessingMatches = true;
function processStep() {
if (removeMatches()) {
dropBalls();
fillEmptySpaces();
LK.setTimeout(function () {
processStep();
}, 500);
} else {
isProcessingMatches = false;
checkGameState();
}
}
processStep();
}
function checkLevelProgression() {
// Calculate cumulative score needed for each level
// Level 1: 0-399 (400 points)
// Level 2: 400-949 (550 points)
// Level 3: 950-1649 (700 points)
// And so on, increasing by 150 each level
function getScoreForLevel(level) {
if (level <= 1) return 0;
var totalScore = 0;
for (var i = 1; i < level; i++) {
totalScore += 400 + (i - 1) * 150;
}
return totalScore;
}
function getScoreNeededForNextLevel(level) {
return 400 + (level - 1) * 150;
}
var currentScore = LK.getScore();
var newLevel = currentLevel;
// Check if we've reached the next level
while (currentScore >= getScoreForLevel(newLevel + 1)) {
newLevel++;
}
if (newLevel > currentLevel) {
// Level up!
currentLevel = newLevel;
pointsToNextLevel = getScoreNeededForNextLevel(currentLevel + 1);
// Increase difficulty
if (ballColors < 6) {
ballColors = Math.min(6, 3 + currentLevel); // Max 6 colors
}
// Set moves based on new level difficulty (progressively fewer moves)
if (gameMode === 'classic') {
movesLeft = getMovesForLevel(currentLevel);
// Update UI
movesText.setText('Moves: ' + movesLeft);
} else {
// In time trial mode, keep unlimited moves
movesLeft = 999;
}
// Reset progress bar for new level
var scoreInCurrentLevel = currentScore - getScoreForLevel(currentLevel);
var scoreNeededThisLevel = getScoreNeededForNextLevel(currentLevel);
var progress = Math.min(1, scoreInCurrentLevel / scoreNeededThisLevel);
progressBarFill.width = progress * 400;
progressText.setText(scoreInCurrentLevel + '/' + scoreNeededThisLevel);
// Change grid shape for new level - clear existing balls first
for (var y = 0; y < gridSize; y++) {
for (var x = 0; x < gridSize; x++) {
if (grid[y][x]) {
grid[y][x].destroy();
grid[y][x] = null;
}
}
}
// Initialize new grid shape
gridShape = getCurrentGridShape();
// Fill new shape with balls
for (var y = 0; y < gridSize; y++) {
for (var x = 0; x < gridSize; x++) {
if (isValidGridPosition(x, y)) {
createBallAt(x, y);
}
}
}
// Flash screen green for level up
LK.effects.flashScreen(0x00FF00, 800);
}
}
function checkGameState() {
if (gameMode === 'classic') {
checkLevelProgression();
if (movesLeft <= 0) {
LK.showGameOver();
return;
}
} else if (gameMode === 'timetrial') {
// In time trial mode, only check for time running out
// (timer already handles game over when time reaches 0)
if (timeLeft <= 0) {
LK.showGameOver();
return;
}
} else if (gameMode === 'freezefrenzy') {
// Freeze frenzy mode - check if all frozen balls are melted or moves run out
var frozenBallsLeft = 0;
for (var y = 0; y < gridSize; y++) {
for (var x = 0; x < gridSize; x++) {
var ball = grid[y][x];
if (ball && ball.isFrozen) {
frozenBallsLeft++;
}
}
}
if (frozenBallsLeft === 0) {
// All frozen balls melted - player wins!
LK.showYouWin();
return;
} else if (movesLeft <= 0) {
// No moves left and still frozen balls - game over
LK.showGameOver();
return;
}
}
}
function getClickedBall(x, y) {
var gridX = Math.floor((x - gridStartX) / cellSize);
var gridY = Math.floor((y - gridStartY) / cellSize);
if (gridX >= 0 && gridX < gridSize && gridY >= 0 && gridY < gridSize) {
return getBallAt(gridX, gridY);
}
return null;
}
game.down = function (x, y, obj) {
if (isProcessingMatches) {
return;
}
var clickedBall = getClickedBall(x, y);
if (clickedBall && !clickedBall.isAnimating) {
// Check if it's a health box
if (clickedBall.isHealthBox) {
clickedBall.activateHealthBonus();
return;
}
// Check if it's a time bonus
if (clickedBall.isTimeBonus) {
clickedBall.activateTimeBonus();
return;
}
// Check if it's a bomb
if (clickedBall.isBomb) {
draggedBomb = clickedBall;
draggedBomb.isDragging = true;
// Stop pulsing animation
tween.stop(draggedBomb);
// Make bomb slightly larger while dragging
tween(draggedBomb, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 100,
easing: tween.easeOut
});
return;
}
// Check if it's a rocket
if (clickedBall.isRocket) {
draggedRocket = clickedBall;
draggedRocket.isDragging = true;
// Stop rocket animation
tween.stop(draggedRocket);
// Make rocket slightly larger while dragging
tween(draggedRocket, {
scaleX: 1.1,
scaleY: 1.1
}, {
duration: 100,
easing: tween.easeOut
});
return;
}
// Regular ball logic
if (selectedBall === null) {
selectedBall = clickedBall;
LK.effects.flashObject(selectedBall, 0xffffff, 200);
} else if (selectedBall === clickedBall) {
selectedBall = null;
} else {
if (swapBalls(selectedBall, clickedBall)) {
if (gameMode === 'classic' || gameMode === 'freezefrenzy') {
movesLeft--;
movesText.setText('Moves: ' + movesLeft);
}
LK.setTimeout(function () {
var matches = findMatches();
if (matches.length > 0) {
processMatches();
} else {
// Wasted move - swap back and apply additional penalty
swapBalls(selectedBall, clickedBall);
// No additional penalty for wasted move (already lost 1 move from the attempt)
movesText.setText('Moves: ' + movesLeft);
// Flash screen red to indicate wasted move
LK.effects.flashScreen(0xFF0000, 300);
}
}, 300);
}
selectedBall = null;
}
}
};
game.move = function (x, y, obj) {
if (draggedBomb && Date.now() - lastMoveTime > 16) {
// Throttle to ~60fps
draggedBomb.x = x;
draggedBomb.y = y;
lastMoveTime = Date.now();
} else if (draggedRocket && Date.now() - lastMoveTime > 16) {
// Throttle to ~60fps
draggedRocket.x = x;
draggedRocket.y = y;
lastMoveTime = Date.now();
}
};
game.up = function (x, y, obj) {
if (draggedBomb) {
// Explode bomb at current position
draggedBomb.explode3x3();
// Using a bomb costs 2 moves in classic and freeze frenzy mode
if (gameMode === 'classic' || gameMode === 'freezefrenzy') {
movesLeft -= 2;
movesText.setText('Moves: ' + movesLeft);
}
// Process any resulting matches
LK.setTimeout(function () {
dropBalls();
fillEmptySpaces();
processMatches();
}, 100);
draggedBomb = null;
} else if (draggedRocket) {
// Explode rocket at current position with 10x2 pattern
draggedRocket.explode10x2();
// Using a rocket costs 5 moves in classic and freeze frenzy mode
if (gameMode === 'classic' || gameMode === 'freezefrenzy') {
movesLeft -= 5;
movesText.setText('Moves: ' + movesLeft);
}
// Process any resulting matches
LK.setTimeout(function () {
dropBalls();
fillEmptySpaces();
processMatches();
}, 100);
draggedRocket = null;
}
};
// Add background - set after game mode is determined
var backgroundImage;
function setGameBackground() {
if (backgroundImage) {
backgroundImage.destroy();
}
if (gameMode === 'freezefrenzy') {
backgroundImage = game.attachAsset('Erek', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
scaleX: 20.48,
scaleY: 27.32,
alpha: 0.3
});
} else {
backgroundImage = game.attachAsset('background', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
alpha: 0.3
});
}
}
// Set initial background
setGameBackground();
// Create main menu
function createMainMenu() {
menuContainer = new Container();
game.addChild(menuContainer);
// Menu background image - properly scaled to fill screen
var menuBackgroundImage = menuContainer.attachAsset('Menu', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
scaleX: 20.48,
scaleY: 27.32,
alpha: 0.8
});
// Menu background overlay for readability
var menuBg = LK.getAsset('gridCell', {
width: 2048,
height: 2732,
anchorX: 0,
anchorY: 0,
color: 0x1a1a2e,
alpha: 0.7
});
menuContainer.addChild(menuBg);
// Game title
var titleText = new Text2('Feelings\nMatch', {
size: 200,
fill: '#FFD700'
});
titleText.anchor.set(0.5, 0.5);
titleText.x = 1024;
titleText.y = 800;
menuContainer.addChild(titleText);
// Subtitle
var subtitleText = new Text2('Swap colored balls to create matches!', {
size: 80,
fill: '#FFFFFF'
});
subtitleText.anchor.set(0.5, 0.5);
subtitleText.x = 1024;
subtitleText.y = 1100;
menuContainer.addChild(subtitleText);
// Game mode buttons
var classicButton = LK.getAsset('gridCell', {
width: 400,
height: 120,
anchorX: 0.5,
anchorY: 0.5,
color: 0x4CAF50
});
classicButton.x = 1024;
classicButton.y = 1400;
menuContainer.addChild(classicButton);
var classicButtonText = new Text2('CLASSIC', {
size: 80,
fill: '#FFFFFF'
});
classicButtonText.anchor.set(0.5, 0.5);
classicButton.addChild(classicButtonText);
var timeTrialButton = LK.getAsset('gridCell', {
width: 400,
height: 120,
anchorX: 0.5,
anchorY: 0.5,
color: 0xFF9800
});
timeTrialButton.x = 1024;
timeTrialButton.y = 1600;
menuContainer.addChild(timeTrialButton);
var timeTrialButtonText = new Text2('TIME TRIAL', {
size: 80,
fill: '#FFFFFF'
});
timeTrialButtonText.anchor.set(0.5, 0.5);
timeTrialButton.addChild(timeTrialButtonText);
// Freeze Frenzy button
var freezeFrenzyButton = LK.getAsset('gridCell', {
width: 400,
height: 120,
anchorX: 0.5,
anchorY: 0.5,
color: 0x87CEEB
});
freezeFrenzyButton.x = 1024;
freezeFrenzyButton.y = 1800;
menuContainer.addChild(freezeFrenzyButton);
var freezeFrenzyButtonText = new Text2('FREEZE FRENZY', {
size: 80,
fill: '#FFFFFF'
});
freezeFrenzyButtonText.anchor.set(0.5, 0.5);
freezeFrenzyButton.addChild(freezeFrenzyButtonText);
// Instructions
var instructionText = new Text2('• Create lines of 3+ matching balls\n• Classic: Limited moves, unlock levels\n• Time Trial: Race against the clock!\n• Freeze Frenzy: Melt frozen balls with 2 matches!', {
size: 60,
fill: '#CCCCCC'
});
instructionText.anchor.set(0.5, 0.5);
instructionText.x = 1024;
instructionText.y = 2000;
menuContainer.addChild(instructionText);
// Animate title entrance
titleText.scaleX = 0;
titleText.scaleY = 0;
titleText.alpha = 0;
tween(titleText, {
scaleX: 1,
scaleY: 1,
alpha: 1
}, {
duration: 800,
easing: tween.easeOut
});
// Animate subtitle
subtitleText.alpha = 0;
tween(subtitleText, {
alpha: 1
}, {
duration: 600,
delay: 400,
easing: tween.easeOut
});
// Animate game mode buttons
classicButton.scaleX = 0;
classicButton.scaleY = 0;
tween(classicButton, {
scaleX: 1,
scaleY: 1
}, {
duration: 500,
delay: 800,
easing: tween.easeOut
});
timeTrialButton.scaleX = 0;
timeTrialButton.scaleY = 0;
tween(timeTrialButton, {
scaleX: 1,
scaleY: 1
}, {
duration: 500,
delay: 900,
easing: tween.easeOut
});
freezeFrenzyButton.scaleX = 0;
freezeFrenzyButton.scaleY = 0;
tween(freezeFrenzyButton, {
scaleX: 1,
scaleY: 1
}, {
duration: 500,
delay: 1000,
easing: tween.easeOut
});
// Animate instructions
instructionText.alpha = 0;
tween(instructionText, {
alpha: 1
}, {
duration: 600,
delay: 1200,
easing: tween.easeOut
});
// Add pulsing animation to game mode buttons
function pulseGameModeButtons() {
tween(classicButton, {
scaleX: 1.1,
scaleY: 1.1
}, {
duration: 800,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(classicButton, {
scaleX: 1,
scaleY: 1
}, {
duration: 800,
easing: tween.easeInOut
});
}
});
tween(timeTrialButton, {
scaleX: 1.05,
scaleY: 1.05
}, {
duration: 1000,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(timeTrialButton, {
scaleX: 1,
scaleY: 1
}, {
duration: 1000,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (gameState === 'menu') {
pulseGameModeButtons();
}
}
});
}
});
}
// Start pulsing after initial animation
LK.setTimeout(function () {
if (gameState === 'menu') {
pulseGameModeButtons();
}
}, 1500);
// Game mode button click handlers
classicButton.down = function (x, y, obj) {
startGame('classic');
};
timeTrialButton.down = function (x, y, obj) {
startGame('timetrial');
};
freezeFrenzyButton.down = function (x, y, obj) {
startGame('freezefrenzy');
};
return menuContainer;
}
// Function to start the actual game
function startGame(mode) {
gameState = 'playing';
gameMode = mode || 'classic';
// Reset combo system
resetCombo();
// Set background for current game mode
setGameBackground();
// Animate menu out
tween(menuContainer, {
alpha: 0,
scaleX: 0.8,
scaleY: 0.8
}, {
duration: 500,
easing: tween.easeIn,
onFinish: function onFinish() {
menuContainer.destroy();
// Initialize game elements
createGameContainer();
initializeGrid();
// Setup UI based on game mode
setupGameModeUI();
// Start background music
LK.playMusic('Ana');
}
});
}
// Function to setup UI based on game mode
function setupGameModeUI() {
if (gameMode === 'timetrial') {
// Hide moves counter and show timer
movesText.visible = false;
timerText.visible = true;
timeLeft = timeTrialDuration;
updateTimer();
gameTimer = LK.setInterval(updateTimer, 1000);
// In time trial, give unlimited moves
movesLeft = 999;
} else if (gameMode === 'freezefrenzy') {
// Freeze frenzy mode - show moves counter, hide timer
movesText.visible = true;
timerText.visible = false;
movesLeft = 40; // Fixed 40 moves for freeze frenzy
movesText.setText('Moves: ' + movesLeft);
} else {
// Classic mode - show moves counter, hide timer
movesText.visible = true;
timerText.visible = false;
movesLeft = getMovesForLevel(1);
movesText.setText('Moves: ' + movesLeft);
}
}
// Function to create game container
function createGameContainer() {
gameContainer = new Container();
game.addChild(gameContainer);
}
// Function to show cinematic sequence
function showCinematic() {
cinematicContainer = new Container();
game.addChild(cinematicContainer);
// Dark background for cinematic
var cinematicBg = LK.getAsset('gridCell', {
width: 2048,
height: 2732,
anchorX: 0,
anchorY: 0,
color: 0x000000,
alpha: 1.0
});
cinematicContainer.addChild(cinematicBg);
// Create title elements for cinematic
var welcomeText = new Text2('Welcome to', {
size: 120,
fill: '#FFFFFF'
});
welcomeText.anchor.set(0.5, 0.5);
welcomeText.x = 1024;
welcomeText.y = 800;
welcomeText.alpha = 0;
cinematicContainer.addChild(welcomeText);
var titleText = new Text2('Feelings Match', {
size: 180,
fill: '#FFD700'
});
titleText.anchor.set(0.5, 0.5);
titleText.x = 1024;
titleText.y = 1000;
titleText.alpha = 0;
cinematicContainer.addChild(titleText);
var taglineText = new Text2('Where emotions align in perfect harmony', {
size: 80,
fill: '#CCCCCC'
});
taglineText.anchor.set(0.5, 0.5);
taglineText.x = 1024;
taglineText.y = 1200;
taglineText.alpha = 0;
cinematicContainer.addChild(taglineText);
var continueText = new Text2('Tap to continue...', {
size: 60,
fill: '#AAAAAA'
});
continueText.anchor.set(0.5, 0.5);
continueText.x = 1024;
continueText.y = 1500;
continueText.alpha = 0;
cinematicContainer.addChild(continueText);
// Cinematic animation sequence
// Fade in welcome text
tween(welcomeText, {
alpha: 1
}, {
duration: 1000,
easing: tween.easeOut,
onFinish: function onFinish() {
// Fade in title with scale animation
titleText.scaleX = 0.5;
titleText.scaleY = 0.5;
tween(titleText, {
alpha: 1,
scaleX: 1,
scaleY: 1
}, {
duration: 1200,
easing: tween.easeOut,
onFinish: function onFinish() {
// Fade in tagline
tween(taglineText, {
alpha: 1
}, {
duration: 800,
easing: tween.easeOut,
onFinish: function onFinish() {
// Pulsing continue text
tween(continueText, {
alpha: 0.8
}, {
duration: 600,
easing: tween.easeInOut,
onFinish: function onFinish() {
// Start pulsing animation for continue text
function pulseContinue() {
tween(continueText, {
alpha: 0.3
}, {
duration: 1000,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(continueText, {
alpha: 0.8
}, {
duration: 1000,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (gameState === 'cinematic') {
pulseContinue();
}
}
});
}
});
}
pulseContinue();
}
});
}
});
}
});
}
});
// Make cinematic clickable to continue
cinematicContainer.down = function (x, y, obj) {
// Fade out cinematic and transition to menu
gameState = 'menu';
tween(cinematicContainer, {
alpha: 0
}, {
duration: 800,
easing: tween.easeIn,
onFinish: function onFinish() {
cinematicContainer.destroy();
createMainMenu();
}
});
};
}
// Start with cinematic sequence
if (gameState === 'cinematic') {
showCinematic();
} else if (gameState === 'menu') {
createMainMenu();
} else {
// Initialize the game
initializeGrid();
}
// Reset button for swapping balls
var resetButton = LK.getAsset('gridCell', {
width: 200,
height: 80,
anchorX: 0.5,
anchorY: 0.5,
color: 0xFF6B6B
});
LK.gui.bottom.addChild(resetButton);
resetButton.y = -100;
var resetButtonText = new Text2('RESET', {
size: 40,
fill: 0xFFFFFF
});
resetButtonText.anchor.set(0.5, 0.5);
resetButton.addChild(resetButtonText);
resetButton.down = function (x, y, obj) {
if (isProcessingMatches) {
return;
}
// Check if player has enough score to reset (cost 100 points)
var resetCost = 100;
if (LK.getScore() < resetCost) {
// Flash screen red to indicate insufficient score
LK.effects.flashScreen(0xFF0000, 400);
return;
}
// Clear current selection
selectedBall = null;
// Reset combo system
resetCombo();
// Clear the grid
for (var y = 0; y < gridSize; y++) {
for (var x = 0; x < gridSize; x++) {
if (grid[y][x]) {
grid[y][x].destroy();
grid[y][x] = null;
}
}
}
// Reinitialize the grid with new balls
initializeGrid();
// Cost 100 score points to reset
LK.setScore(LK.getScore() - resetCost);
scoreText.setText('Score: ' + LK.getScore());
// Update progress bar
var currentScore = LK.getScore();
// Calculate score needed for current level progression
function getScoreForLevel(level) {
if (level <= 1) return 0;
var totalScore = 0;
for (var i = 1; i < level; i++) {
totalScore += 400 + (i - 1) * 150;
}
return totalScore;
}
function getScoreNeededForNextLevel(level) {
return 400 + (level - 1) * 150;
}
var scoreInCurrentLevel = currentScore - getScoreForLevel(currentLevel);
var scoreNeededThisLevel = getScoreNeededForNextLevel(currentLevel);
var progress = Math.min(1, scoreInCurrentLevel / scoreNeededThisLevel);
progressBarFill.width = progress * 400;
progressText.setText(scoreInCurrentLevel + '/' + scoreNeededThisLevel);
// Flash screen blue to indicate reset
LK.effects.flashScreen(0x0066FF, 400);
};
// Background music will be started when game begins from menu
;
;
An orange round ball with a silly smile. In-Game asset. High contrast. No shadows
A clever yellow round ball with a yellow confused expression
A cool blue round ball. In-Game asset. High contrast. No shadows He has sunglasses and a cool smile
A rich, greedy green ball has a wad of cash in his hand. In-Game asset. High contrast. No shadows
A nervous red ball with an angry face. In-Game asset. High contrast. No shadows
A black bomb. In-Game asset. No shadows
A wooden box with a heart symbol on it. In-Game asset. High contrast. No shadows
Metal chains in x shape. In-Game asset. High contrast. No shadows
A forest background. In-Game asset. High contrast. No shadows
A pocket watch. In-Game asset. High contrast. No shadows
Blue sky with sparse clouds. In-Game asset. High contrast. Realistic anime style
A pink woman ball. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
A pig's face. High contrast. No shadows
Fark fence. In-Game asset. 2d. High contrast. No shadows
Ice cube. In-Game asset. High contrast. No shadows
A snowy forest. In-Game asset. 2d. High contrast. No shadows
A rocket. In-Game asset. 2d. High contrast. No shadows