/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ // Gem class var Gem = Container.expand(function () { var self = Container.call(this); // Properties self.gemType = null; // 'red', 'blue', etc. self.row = 0; self.col = 0; self.isMoving = false; self.isMatched = false; self.multiplier = 1; // For Zeus or special gems // Attach asset self.setType = function (type) { self.gemType = type; if (self.gemAsset) { self.removeChild(self.gemAsset); } var assetId = 'gem_' + type; self.gemAsset = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); // Zeus gem: add a glow effect (tint) if (type === 'zeus') { self.gemAsset.tint = 0xadd8ff; } }; // Animate match (fade out) self.animateMatch = function (_onFinish) { self.isMoving = true; tween(self.gemAsset, { alpha: 0 }, { duration: 250, easing: tween.easeIn, onFinish: function onFinish() { self.isMoving = false; if (_onFinish) { _onFinish(); } } }); }; // Animate swap (move to new position) self.animateMove = function (targetX, targetY, _onFinish2) { self.isMoving = true; tween(self, { x: targetX, y: targetY }, { duration: 180, easing: tween.easeInOut, onFinish: function onFinish() { self.isMoving = false; if (_onFinish2) { _onFinish2(); } } }); }; // Reset alpha after match self.resetAlpha = function () { self.gemAsset.alpha = 1; }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x1a2236 }); /**** * Game Code ****/ // Gems: 6 types + Zeus special // --- Game Constants --- var GRID_ROWS = 8; var GRID_COLS = 8; var GEM_SIZE = 200; // px, including margin var GEM_TYPES = ['red', 'blue', 'green', 'yellow', 'purple', 'orange']; var ZEUS_CHANCE = 0.07; // Chance for Zeus gem to spawn on refill var MOVES_LIMIT = 30; // --- Game State --- var grid = []; // 2D array [row][col] of Gem var selectedGem = null; var swappingGem = null; var canInput = true; var score = 0; var movesLeft = MOVES_LIMIT; var multiplier = 1; var comboCount = 0; var powerMeter = 0; var powerMeterMax = 10; var isPowerActive = false; var movesMade = 0; // Track number of moves made // --- UI Elements --- var scoreTxt = new Text2('Score: 0', { size: 90, fill: "#fff" }); scoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(scoreTxt); var powerMeterTxt = new Text2('⚡ 0/10', { size: 70, fill: 0x00E6E6 }); powerMeterTxt.anchor.set(0.5, 0); LK.gui.bottom.addChild(powerMeterTxt); // Combo text at the bottom of the screen var comboTxt = new Text2('', { size: 100, fill: "#fff" }); comboTxt.anchor.set(0.5, 1); LK.gui.bottom.addChild(comboTxt); // --- Board Position --- var boardOffsetX = (2048 - GRID_COLS * GEM_SIZE) / 2; var boardOffsetY = 300; // --- Helper Functions --- function getGemAt(row, col) { if (row < 0 || row >= GRID_ROWS || col < 0 || col >= GRID_COLS) { return null; } return grid[row][col]; } function setGemAt(row, col, gem) { grid[row][col] = gem; if (gem) { gem.row = row; gem.col = col; } } function gemWorldPos(row, col) { return { x: boardOffsetX + col * GEM_SIZE + GEM_SIZE / 2, y: boardOffsetY + row * GEM_SIZE + GEM_SIZE / 2 }; } function randomGemType() { // Zeus gems only spawn after 2 moves have been made if (movesMade >= 2 && Math.random() < ZEUS_CHANCE) { return 'zeus'; } return GEM_TYPES[Math.floor(Math.random() * GEM_TYPES.length)]; } function updateUI() { scoreTxt.setText('Score: ' + score); powerMeterTxt.setText('⚡ ' + powerMeter + '/' + powerMeterMax); } function deselectGem() { if (selectedGem && selectedGem.gemAsset) { selectedGem.gemAsset.scaleX = 1; selectedGem.gemAsset.scaleY = 1; } selectedGem = null; } function selectGem(gem) { deselectGem(); selectedGem = gem; if (gem && gem.gemAsset) { gem.gemAsset.scaleX = 1.2; gem.gemAsset.scaleY = 1.2; } } function areAdjacent(gem1, gem2) { if (!gem1 || !gem2) { return false; } var dr = Math.abs(gem1.row - gem2.row); var dc = Math.abs(gem1.col - gem2.col); return dr + dc === 1; } function swapGems(gem1, gem2, animate, onFinish) { // Swap in grid var r1 = gem1.row, c1 = gem1.col; var r2 = gem2.row, c2 = gem2.col; setGemAt(r1, c1, gem2); setGemAt(r2, c2, gem1); // Animate var pos1 = gemWorldPos(r1, c1); var pos2 = gemWorldPos(r2, c2); if (animate) { gem1.animateMove(pos2.x, pos2.y, function () { gem2.animateMove(pos1.x, pos1.y, function () { if (onFinish) { onFinish(); } }); }); } else { gem1.x = pos2.x; gem1.y = pos2.y; gem2.x = pos1.x; gem2.y = pos1.y; if (onFinish) { onFinish(); } } } function refillBoard(onFinish) { var falling = 0; for (var col = 0; col < GRID_COLS; col++) { var emptyRows = []; for (var row = GRID_ROWS - 1; row >= 0; row--) { if (!grid[row][col]) { emptyRows.push(row); } } // Improved falling logic: process from bottom up, for each cell, if empty, pull down the nearest gem above for (var row = GRID_ROWS - 1; row >= 0; row--) { if (!grid[row][col]) { // Find the nearest gem above var found = false; for (var r2 = row - 1; r2 >= 0; r2--) { var gem = grid[r2][col]; if (gem) { setGemAt(row, col, gem); setGemAt(r2, col, null); var pos = gemWorldPos(row, col); falling++; gem.animateMove(pos.x, pos.y, function () { falling--; }); found = true; break; } } // If nothing found above, spawn new gem if (!found) { var newGem = new Gem(); var type = randomGemType(); newGem.setType(type); setGemAt(row, col, newGem); var pos = gemWorldPos(row, col); newGem.x = pos.x; newGem.y = pos.y - GEM_SIZE * 2; // Drop from above game.addChild(newGem); // Award 5000 points if gem_zeus appears if (type === 'zeus') { score += 1000; updateUI(); } falling++; newGem.animateMove(pos.x, pos.y, function () { falling--; }); } } } } // Wait for all falling to finish var _wait = function wait() { if (falling > 0) { LK.setTimeout(_wait, 40); } else { if (onFinish) { onFinish(); } } }; _wait(); } function findMatches() { var matches = []; // Horizontal for (var row = 0; row < GRID_ROWS; row++) { var streak = 1; for (var col = 1; col <= GRID_COLS; col++) { var prev = getGemAt(row, col - 1); var curr = getGemAt(row, col); if (curr && prev && curr.gemType === prev.gemType && curr.gemType !== 'zeus') { streak++; } else { if (streak >= 3 && prev && prev.gemType !== 'zeus') { var match = []; for (var k = 0; k < streak; k++) { match.push(getGemAt(row, col - 1 - k)); } matches.push(match); } streak = 1; } } } // Vertical for (var col = 0; col < GRID_COLS; col++) { var streak = 1; for (var row = 1; row <= GRID_ROWS; row++) { var prev = getGemAt(row - 1, col); var curr = getGemAt(row, col); if (curr && prev && curr.gemType === prev.gemType && curr.gemType !== 'zeus') { streak++; } else { if (streak >= 3 && prev && prev.gemType !== 'zeus') { var match = []; for (var k = 0; k < streak; k++) { match.push(getGemAt(row - 1 - k, col)); } matches.push(match); } streak = 1; } } } // Zeus gems: match any adjacent gems for (var row = 0; row < GRID_ROWS; row++) { for (var col = 0; col < GRID_COLS; col++) { var gem = getGemAt(row, col); if (gem && gem.gemType === 'zeus') { var adj = []; for (var dr = -1; dr <= 1; dr++) { for (var dc = -1; dc <= 1; dc++) { if (dr === 0 && dc === 0) { continue; } var n = getGemAt(row + dr, col + dc); if (n && n.gemType !== 'zeus') { adj.push(n); } } } if (adj.length > 0) { matches.push([gem].concat(adj)); } } } } return matches; } function markMatches(matches) { for (var i = 0; i < matches.length; i++) { for (var j = 0; j < matches[i].length; j++) { var gem = matches[i][j]; if (gem) { gem.isMatched = true; } } } } function removeMatches(onFinish) { var removed = 0; for (var row = 0; row < GRID_ROWS; row++) { for (var col = 0; col < GRID_COLS; col++) { var gem = getGemAt(row, col); if (gem && gem.isMatched) { removed++; (function (gem, row, col) { gem.animateMatch(function () { game.removeChild(gem); setGemAt(row, col, null); removed--; }); })(gem, row, col); } } } // Wait for all to finish var _wait2 = function wait() { if (removed > 0) { LK.setTimeout(_wait2, 40); } else { if (onFinish) { onFinish(); } } }; _wait2(); } function resetGemFlags() { for (var row = 0; row < GRID_ROWS; row++) { for (var col = 0; col < GRID_COLS; col++) { var gem = getGemAt(row, col); if (gem) { gem.isMatched = false; gem.resetAlpha(); } } } } function triggerZeusPower(gem, onFinish) { // Randomly clear a row or column var mode = Math.random() < 0.5 ? 'row' : 'col'; var idx = mode === 'row' ? gem.row : gem.col; var gemsToClear = []; if (mode === 'row') { for (var c = 0; c < GRID_COLS; c++) { var g = getGemAt(idx, c); if (g && g !== gem) { gemsToClear.push(g); } } } else { for (var r = 0; r < GRID_ROWS; r++) { var g = getGemAt(r, idx); if (g && g !== gem) { gemsToClear.push(g); } } } // Animate Zeus gem (grow and explode) LK.getSound('zeus').play(); var originalScaleX = gem.gemAsset.scaleX; var originalScaleY = gem.gemAsset.scaleY; tween(gem.gemAsset, { scaleX: 2.5, scaleY: 2.5, alpha: 0 }, { duration: 400, easing: tween.cubicOut, onFinish: function onFinish() { gem.gemAsset.scaleX = originalScaleX; gem.gemAsset.scaleY = originalScaleY; gem.gemAsset.alpha = 1; } }); LK.effects.flashObject(gem, 0x00ffff, 400); // Mark for removal for (var i = 0; i < gemsToClear.length; i++) { gemsToClear[i].isMatched = true; } // Remove Zeus gem itself gem.isMatched = true; // Show multiplier popup var mult = 2 + Math.floor(Math.random() * 3); // 2x-4x multiplier = mult; LK.effects.flashScreen(0xadd8ff, 400); // Animate Zeus lightning (screen flash) if (onFinish) { LK.setTimeout(onFinish, 400); } } // Combo transfer state: track if we're in a combo chain (from first explosion to end) if (typeof comboTransferActive === "undefined") { var comboTransferActive = false; } function processMatches(matches, onFinish) { if (matches.length === 0) { // Combo chain ends here comboTransferActive = false; comboCount = 0; multiplier = 1; comboTxt.setText(''); updateUI(); if (onFinish) { onFinish(); } return; } // Combo chain starts on first explosion if (!comboTransferActive) { comboTransferActive = true; comboCount = 1; } else { comboCount++; } if (comboCount > 1) { LK.getSound('match').play(); LK.getSound('combo').play(); comboTxt.setText('Combo x' + comboCount); // Animate comboTxt: pop and fade in comboTxt.alpha = 0.2; comboTxt.scaleX = 1.8; comboTxt.scaleY = 1.8; tween(comboTxt, { alpha: 1, scaleX: 1, scaleY: 1 }, { duration: 350, easing: tween.cubicOut, onFinish: function onFinish() { // Vibrate effect: rapid left-right shake var originalX = comboTxt.x; var vibrateTimes = 6; var vibrateDistance = 24; var vibrateDuration = 18; var _vibrateStep = function vibrateStep(i) { if (i > vibrateTimes) { comboTxt.x = originalX; return; } var dir = i % 2 === 0 ? 1 : -1; tween(comboTxt, { x: originalX + dir * vibrateDistance }, { duration: vibrateDuration, easing: tween.linear, onFinish: function onFinish() { tween(comboTxt, { x: originalX }, { duration: vibrateDuration, easing: tween.linear, onFinish: function onFinish() { _vibrateStep(i + 1); } }); } }); }; _vibrateStep(1); } }); } else { LK.getSound('match').play(); comboTxt.setText(''); // Optionally fade out comboTxt if needed tween(comboTxt, { alpha: 0 }, { duration: 200, easing: tween.linear }); } // Increase each gem_zeus multiplier by 1 for each combo for (var i = 0; i < matches.length; i++) { for (var j = 0; j < matches[i].length; j++) { var gem = matches[i][j]; if (gem && gem.gemType === 'zeus') { if (typeof gem.multiplier !== 'number') { gem.multiplier = 1; } gem.multiplier += 1; } } } // Score: 100 per gem, * multiplier, * combo var gemsMatched = 0; var zeusTriggered = false; var zeusBonus = 0; for (var i = 0; i < matches.length; i++) { for (var j = 0; j < matches[i].length; j++) { var gem = matches[i][j]; if (gem && !gem.isMatched) { // Count all gems, including zeus, towards score gemsMatched++; if (gem.gemType === 'zeus') { zeusTriggered = true; zeusBonus += 5000; } } } } // Power meter powerMeter += gemsMatched; if (powerMeter > powerMeterMax) { powerMeter = powerMeterMax; } // If Zeus, trigger power if (zeusTriggered) { for (var i = 0; i < matches.length; i++) { for (var j = 0; j < matches[i].length; j++) { var gem = matches[i][j]; if (gem && gem.gemType === 'zeus') { triggerZeusPower(gem, function () { markMatches(matches); removeMatches(function () { refillBoard(function () { resetGemFlags(); var newMatches = findMatches(); processMatches(newMatches, onFinish); }); }); }); return; } } } } else { // No Zeus, normal match markMatches(matches); removeMatches(function () { var prevScore = score; score += gemsMatched * 100 * multiplier * comboCount; score += zeusBonus; if (prevScore < 100000 && score >= 100000) { LK.getSound('powerup').play(); } if (prevScore < 1000000 && score >= 1000000) { LK.getSound('success').play(); } updateUI(); refillBoard(function () { resetGemFlags(); var newMatches = findMatches(); processMatches(newMatches, onFinish); }); }); } } function trySwapAndMatch(gem1, gem2) { canInput = false; LK.getSound('swap').play(); swapGems(gem1, gem2, true, function () { var matches = findMatches(); if (matches.length > 0) { movesLeft--; movesMade++; // Increment movesMade after a valid move updateUI(); processMatches(matches, function () { canInput = true; checkGameEnd(); }); } else { // No match, swap back swapGems(gem1, gem2, true, function () { canInput = true; }); } }); } function checkGameEnd() { // Endless mode: do nothing, never trigger game over } // --- Power Meter Activation --- function activatePower() { if (powerMeter < powerMeterMax || isPowerActive) { return; } isPowerActive = true; LK.getSound('powerup').play(); // Randomly clear 2 rows or columns var cleared = 0; for (var i = 0; i < 2; i++) { var mode = Math.random() < 0.5 ? 'row' : 'col'; var idx = Math.floor(Math.random() * (mode === 'row' ? GRID_ROWS : GRID_COLS)); for (var j = 0; j < (mode === 'row' ? GRID_COLS : GRID_ROWS); j++) { var gem = mode === 'row' ? getGemAt(idx, j) : getGemAt(j, idx); if (gem) { gem.isMatched = true; cleared++; } } } LK.effects.flashScreen(0xffff00, 600); removeMatches(function () { score += cleared * 200 * multiplier; powerMeter = 0; isPowerActive = false; updateUI(); refillBoard(function () { resetGemFlags(); var newMatches = findMatches(); processMatches(newMatches, function () { canInput = true; checkGameEnd(); }); }); }); } // --- Board Initialization --- function fillBoardNoMatches() { // Fill board, avoid initial matches for (var row = 0; row < GRID_ROWS; row++) { grid[row] = []; for (var col = 0; col < GRID_COLS; col++) { var gem = new Gem(); var type; do { // Prevent Zeus from spawning at game start do { type = randomGemType(); } while (type === 'zeus'); gem.setType(type); setGemAt(row, col, gem); } while (col >= 2 && getGemAt(row, col - 1).gemType === type && getGemAt(row, col - 2).gemType === type || row >= 2 && getGemAt(row - 1, col).gemType === type && getGemAt(row - 2, col).gemType === type); var pos = gemWorldPos(row, col); gem.x = pos.x; gem.y = pos.y; game.addChild(gem); } } } // --- Input Handling --- game.down = function (x, y, obj) { if (!canInput) { return; } // Convert to board coordinates var bx = x - boardOffsetX; var by = y - boardOffsetY; var col = Math.floor(bx / GEM_SIZE); var row = Math.floor(by / GEM_SIZE); var gem = getGemAt(row, col); if (!gem) { return; } if (selectedGem === gem) { deselectGem(); return; } if (!selectedGem) { selectGem(gem); } else { if (areAdjacent(selectedGem, gem)) { swappingGem = gem; trySwapAndMatch(selectedGem, gem); deselectGem(); } else { selectGem(gem); } } }; game.move = function (x, y, obj) { // No drag-swap for now (tap only) }; game.up = function (x, y, obj) { // No drag-swap for now (tap only) }; // --- Power Meter Tap (activate power) --- powerMeterTxt.interactive = true; powerMeterTxt.down = function (x, y, obj) { if (powerMeter >= powerMeterMax && canInput) { canInput = false; activatePower(); } }; // No custom pause menu logic; rely on LK's built-in pause menu // --- Music Mute Button --- var musicMuted = true; var musicBtn = new Text2('🔇', { size: 110, fill: "#fff" }); musicBtn.anchor.set(1, 1); musicBtn.x = 0; // Will be positioned by LK.gui.bottomRight musicBtn.y = 0; musicBtn.interactive = true; musicBtn.down = function (x, y, obj) { musicMuted = !musicMuted; if (musicMuted) { LK.stopMusic(); musicBtn.setText('🔇'); } else { LK.playMusic('olympus_theme', { volume: 0.02, fade: { start: 0, end: 1, duration: 600 } }); musicBtn.setText('🔊'); } }; LK.gui.bottomRight.addChild(musicBtn); // --- Game Update --- game.update = function () { // Play music if not playing and not muted if (!game._musicStarted && !musicMuted) { LK.playMusic('olympus_theme', { volume: 0.02, fade: { start: 0, end: 1, duration: 1000 } }); game._musicStarted = true; } if (!game._musicStarted && musicMuted) { // Don't play music, but mark as started to prevent repeated checks game._musicStarted = true; } // Prevent input during animations // (Handled by canInput flag) }; // Add pause menu support // LK's built-in pause menu is enabled by default; no need to call LK.enablePauseMenu() // --- Game Start --- function startGame() { // Reset state grid = []; selectedGem = null; swappingGem = null; canInput = true; score = 0; movesLeft = MOVES_LIMIT; multiplier = 1; comboCount = 0; powerMeter = 0; isPowerActive = false; movesMade = 0; // Reset movesMade on game start updateUI(); // Remove all children except UI for (var i = game.children.length - 1; i >= 0; i--) { var ch = game.children[i]; game.removeChild(ch); } fillBoardNoMatches(); } startGame();
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Gem class
var Gem = Container.expand(function () {
var self = Container.call(this);
// Properties
self.gemType = null; // 'red', 'blue', etc.
self.row = 0;
self.col = 0;
self.isMoving = false;
self.isMatched = false;
self.multiplier = 1; // For Zeus or special gems
// Attach asset
self.setType = function (type) {
self.gemType = type;
if (self.gemAsset) {
self.removeChild(self.gemAsset);
}
var assetId = 'gem_' + type;
self.gemAsset = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
// Zeus gem: add a glow effect (tint)
if (type === 'zeus') {
self.gemAsset.tint = 0xadd8ff;
}
};
// Animate match (fade out)
self.animateMatch = function (_onFinish) {
self.isMoving = true;
tween(self.gemAsset, {
alpha: 0
}, {
duration: 250,
easing: tween.easeIn,
onFinish: function onFinish() {
self.isMoving = false;
if (_onFinish) {
_onFinish();
}
}
});
};
// Animate swap (move to new position)
self.animateMove = function (targetX, targetY, _onFinish2) {
self.isMoving = true;
tween(self, {
x: targetX,
y: targetY
}, {
duration: 180,
easing: tween.easeInOut,
onFinish: function onFinish() {
self.isMoving = false;
if (_onFinish2) {
_onFinish2();
}
}
});
};
// Reset alpha after match
self.resetAlpha = function () {
self.gemAsset.alpha = 1;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x1a2236
});
/****
* Game Code
****/
// Gems: 6 types + Zeus special
// --- Game Constants ---
var GRID_ROWS = 8;
var GRID_COLS = 8;
var GEM_SIZE = 200; // px, including margin
var GEM_TYPES = ['red', 'blue', 'green', 'yellow', 'purple', 'orange'];
var ZEUS_CHANCE = 0.07; // Chance for Zeus gem to spawn on refill
var MOVES_LIMIT = 30;
// --- Game State ---
var grid = []; // 2D array [row][col] of Gem
var selectedGem = null;
var swappingGem = null;
var canInput = true;
var score = 0;
var movesLeft = MOVES_LIMIT;
var multiplier = 1;
var comboCount = 0;
var powerMeter = 0;
var powerMeterMax = 10;
var isPowerActive = false;
var movesMade = 0; // Track number of moves made
// --- UI Elements ---
var scoreTxt = new Text2('Score: 0', {
size: 90,
fill: "#fff"
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
var powerMeterTxt = new Text2('⚡ 0/10', {
size: 70,
fill: 0x00E6E6
});
powerMeterTxt.anchor.set(0.5, 0);
LK.gui.bottom.addChild(powerMeterTxt);
// Combo text at the bottom of the screen
var comboTxt = new Text2('', {
size: 100,
fill: "#fff"
});
comboTxt.anchor.set(0.5, 1);
LK.gui.bottom.addChild(comboTxt);
// --- Board Position ---
var boardOffsetX = (2048 - GRID_COLS * GEM_SIZE) / 2;
var boardOffsetY = 300;
// --- Helper Functions ---
function getGemAt(row, col) {
if (row < 0 || row >= GRID_ROWS || col < 0 || col >= GRID_COLS) {
return null;
}
return grid[row][col];
}
function setGemAt(row, col, gem) {
grid[row][col] = gem;
if (gem) {
gem.row = row;
gem.col = col;
}
}
function gemWorldPos(row, col) {
return {
x: boardOffsetX + col * GEM_SIZE + GEM_SIZE / 2,
y: boardOffsetY + row * GEM_SIZE + GEM_SIZE / 2
};
}
function randomGemType() {
// Zeus gems only spawn after 2 moves have been made
if (movesMade >= 2 && Math.random() < ZEUS_CHANCE) {
return 'zeus';
}
return GEM_TYPES[Math.floor(Math.random() * GEM_TYPES.length)];
}
function updateUI() {
scoreTxt.setText('Score: ' + score);
powerMeterTxt.setText('⚡ ' + powerMeter + '/' + powerMeterMax);
}
function deselectGem() {
if (selectedGem && selectedGem.gemAsset) {
selectedGem.gemAsset.scaleX = 1;
selectedGem.gemAsset.scaleY = 1;
}
selectedGem = null;
}
function selectGem(gem) {
deselectGem();
selectedGem = gem;
if (gem && gem.gemAsset) {
gem.gemAsset.scaleX = 1.2;
gem.gemAsset.scaleY = 1.2;
}
}
function areAdjacent(gem1, gem2) {
if (!gem1 || !gem2) {
return false;
}
var dr = Math.abs(gem1.row - gem2.row);
var dc = Math.abs(gem1.col - gem2.col);
return dr + dc === 1;
}
function swapGems(gem1, gem2, animate, onFinish) {
// Swap in grid
var r1 = gem1.row,
c1 = gem1.col;
var r2 = gem2.row,
c2 = gem2.col;
setGemAt(r1, c1, gem2);
setGemAt(r2, c2, gem1);
// Animate
var pos1 = gemWorldPos(r1, c1);
var pos2 = gemWorldPos(r2, c2);
if (animate) {
gem1.animateMove(pos2.x, pos2.y, function () {
gem2.animateMove(pos1.x, pos1.y, function () {
if (onFinish) {
onFinish();
}
});
});
} else {
gem1.x = pos2.x;
gem1.y = pos2.y;
gem2.x = pos1.x;
gem2.y = pos1.y;
if (onFinish) {
onFinish();
}
}
}
function refillBoard(onFinish) {
var falling = 0;
for (var col = 0; col < GRID_COLS; col++) {
var emptyRows = [];
for (var row = GRID_ROWS - 1; row >= 0; row--) {
if (!grid[row][col]) {
emptyRows.push(row);
}
}
// Improved falling logic: process from bottom up, for each cell, if empty, pull down the nearest gem above
for (var row = GRID_ROWS - 1; row >= 0; row--) {
if (!grid[row][col]) {
// Find the nearest gem above
var found = false;
for (var r2 = row - 1; r2 >= 0; r2--) {
var gem = grid[r2][col];
if (gem) {
setGemAt(row, col, gem);
setGemAt(r2, col, null);
var pos = gemWorldPos(row, col);
falling++;
gem.animateMove(pos.x, pos.y, function () {
falling--;
});
found = true;
break;
}
}
// If nothing found above, spawn new gem
if (!found) {
var newGem = new Gem();
var type = randomGemType();
newGem.setType(type);
setGemAt(row, col, newGem);
var pos = gemWorldPos(row, col);
newGem.x = pos.x;
newGem.y = pos.y - GEM_SIZE * 2; // Drop from above
game.addChild(newGem);
// Award 5000 points if gem_zeus appears
if (type === 'zeus') {
score += 1000;
updateUI();
}
falling++;
newGem.animateMove(pos.x, pos.y, function () {
falling--;
});
}
}
}
}
// Wait for all falling to finish
var _wait = function wait() {
if (falling > 0) {
LK.setTimeout(_wait, 40);
} else {
if (onFinish) {
onFinish();
}
}
};
_wait();
}
function findMatches() {
var matches = [];
// Horizontal
for (var row = 0; row < GRID_ROWS; row++) {
var streak = 1;
for (var col = 1; col <= GRID_COLS; col++) {
var prev = getGemAt(row, col - 1);
var curr = getGemAt(row, col);
if (curr && prev && curr.gemType === prev.gemType && curr.gemType !== 'zeus') {
streak++;
} else {
if (streak >= 3 && prev && prev.gemType !== 'zeus') {
var match = [];
for (var k = 0; k < streak; k++) {
match.push(getGemAt(row, col - 1 - k));
}
matches.push(match);
}
streak = 1;
}
}
}
// Vertical
for (var col = 0; col < GRID_COLS; col++) {
var streak = 1;
for (var row = 1; row <= GRID_ROWS; row++) {
var prev = getGemAt(row - 1, col);
var curr = getGemAt(row, col);
if (curr && prev && curr.gemType === prev.gemType && curr.gemType !== 'zeus') {
streak++;
} else {
if (streak >= 3 && prev && prev.gemType !== 'zeus') {
var match = [];
for (var k = 0; k < streak; k++) {
match.push(getGemAt(row - 1 - k, col));
}
matches.push(match);
}
streak = 1;
}
}
}
// Zeus gems: match any adjacent gems
for (var row = 0; row < GRID_ROWS; row++) {
for (var col = 0; col < GRID_COLS; col++) {
var gem = getGemAt(row, col);
if (gem && gem.gemType === 'zeus') {
var adj = [];
for (var dr = -1; dr <= 1; dr++) {
for (var dc = -1; dc <= 1; dc++) {
if (dr === 0 && dc === 0) {
continue;
}
var n = getGemAt(row + dr, col + dc);
if (n && n.gemType !== 'zeus') {
adj.push(n);
}
}
}
if (adj.length > 0) {
matches.push([gem].concat(adj));
}
}
}
}
return matches;
}
function markMatches(matches) {
for (var i = 0; i < matches.length; i++) {
for (var j = 0; j < matches[i].length; j++) {
var gem = matches[i][j];
if (gem) {
gem.isMatched = true;
}
}
}
}
function removeMatches(onFinish) {
var removed = 0;
for (var row = 0; row < GRID_ROWS; row++) {
for (var col = 0; col < GRID_COLS; col++) {
var gem = getGemAt(row, col);
if (gem && gem.isMatched) {
removed++;
(function (gem, row, col) {
gem.animateMatch(function () {
game.removeChild(gem);
setGemAt(row, col, null);
removed--;
});
})(gem, row, col);
}
}
}
// Wait for all to finish
var _wait2 = function wait() {
if (removed > 0) {
LK.setTimeout(_wait2, 40);
} else {
if (onFinish) {
onFinish();
}
}
};
_wait2();
}
function resetGemFlags() {
for (var row = 0; row < GRID_ROWS; row++) {
for (var col = 0; col < GRID_COLS; col++) {
var gem = getGemAt(row, col);
if (gem) {
gem.isMatched = false;
gem.resetAlpha();
}
}
}
}
function triggerZeusPower(gem, onFinish) {
// Randomly clear a row or column
var mode = Math.random() < 0.5 ? 'row' : 'col';
var idx = mode === 'row' ? gem.row : gem.col;
var gemsToClear = [];
if (mode === 'row') {
for (var c = 0; c < GRID_COLS; c++) {
var g = getGemAt(idx, c);
if (g && g !== gem) {
gemsToClear.push(g);
}
}
} else {
for (var r = 0; r < GRID_ROWS; r++) {
var g = getGemAt(r, idx);
if (g && g !== gem) {
gemsToClear.push(g);
}
}
}
// Animate Zeus gem (grow and explode)
LK.getSound('zeus').play();
var originalScaleX = gem.gemAsset.scaleX;
var originalScaleY = gem.gemAsset.scaleY;
tween(gem.gemAsset, {
scaleX: 2.5,
scaleY: 2.5,
alpha: 0
}, {
duration: 400,
easing: tween.cubicOut,
onFinish: function onFinish() {
gem.gemAsset.scaleX = originalScaleX;
gem.gemAsset.scaleY = originalScaleY;
gem.gemAsset.alpha = 1;
}
});
LK.effects.flashObject(gem, 0x00ffff, 400);
// Mark for removal
for (var i = 0; i < gemsToClear.length; i++) {
gemsToClear[i].isMatched = true;
}
// Remove Zeus gem itself
gem.isMatched = true;
// Show multiplier popup
var mult = 2 + Math.floor(Math.random() * 3); // 2x-4x
multiplier = mult;
LK.effects.flashScreen(0xadd8ff, 400);
// Animate Zeus lightning (screen flash)
if (onFinish) {
LK.setTimeout(onFinish, 400);
}
}
// Combo transfer state: track if we're in a combo chain (from first explosion to end)
if (typeof comboTransferActive === "undefined") {
var comboTransferActive = false;
}
function processMatches(matches, onFinish) {
if (matches.length === 0) {
// Combo chain ends here
comboTransferActive = false;
comboCount = 0;
multiplier = 1;
comboTxt.setText('');
updateUI();
if (onFinish) {
onFinish();
}
return;
}
// Combo chain starts on first explosion
if (!comboTransferActive) {
comboTransferActive = true;
comboCount = 1;
} else {
comboCount++;
}
if (comboCount > 1) {
LK.getSound('match').play();
LK.getSound('combo').play();
comboTxt.setText('Combo x' + comboCount);
// Animate comboTxt: pop and fade in
comboTxt.alpha = 0.2;
comboTxt.scaleX = 1.8;
comboTxt.scaleY = 1.8;
tween(comboTxt, {
alpha: 1,
scaleX: 1,
scaleY: 1
}, {
duration: 350,
easing: tween.cubicOut,
onFinish: function onFinish() {
// Vibrate effect: rapid left-right shake
var originalX = comboTxt.x;
var vibrateTimes = 6;
var vibrateDistance = 24;
var vibrateDuration = 18;
var _vibrateStep = function vibrateStep(i) {
if (i > vibrateTimes) {
comboTxt.x = originalX;
return;
}
var dir = i % 2 === 0 ? 1 : -1;
tween(comboTxt, {
x: originalX + dir * vibrateDistance
}, {
duration: vibrateDuration,
easing: tween.linear,
onFinish: function onFinish() {
tween(comboTxt, {
x: originalX
}, {
duration: vibrateDuration,
easing: tween.linear,
onFinish: function onFinish() {
_vibrateStep(i + 1);
}
});
}
});
};
_vibrateStep(1);
}
});
} else {
LK.getSound('match').play();
comboTxt.setText('');
// Optionally fade out comboTxt if needed
tween(comboTxt, {
alpha: 0
}, {
duration: 200,
easing: tween.linear
});
}
// Increase each gem_zeus multiplier by 1 for each combo
for (var i = 0; i < matches.length; i++) {
for (var j = 0; j < matches[i].length; j++) {
var gem = matches[i][j];
if (gem && gem.gemType === 'zeus') {
if (typeof gem.multiplier !== 'number') {
gem.multiplier = 1;
}
gem.multiplier += 1;
}
}
}
// Score: 100 per gem, * multiplier, * combo
var gemsMatched = 0;
var zeusTriggered = false;
var zeusBonus = 0;
for (var i = 0; i < matches.length; i++) {
for (var j = 0; j < matches[i].length; j++) {
var gem = matches[i][j];
if (gem && !gem.isMatched) {
// Count all gems, including zeus, towards score
gemsMatched++;
if (gem.gemType === 'zeus') {
zeusTriggered = true;
zeusBonus += 5000;
}
}
}
}
// Power meter
powerMeter += gemsMatched;
if (powerMeter > powerMeterMax) {
powerMeter = powerMeterMax;
}
// If Zeus, trigger power
if (zeusTriggered) {
for (var i = 0; i < matches.length; i++) {
for (var j = 0; j < matches[i].length; j++) {
var gem = matches[i][j];
if (gem && gem.gemType === 'zeus') {
triggerZeusPower(gem, function () {
markMatches(matches);
removeMatches(function () {
refillBoard(function () {
resetGemFlags();
var newMatches = findMatches();
processMatches(newMatches, onFinish);
});
});
});
return;
}
}
}
} else {
// No Zeus, normal match
markMatches(matches);
removeMatches(function () {
var prevScore = score;
score += gemsMatched * 100 * multiplier * comboCount;
score += zeusBonus;
if (prevScore < 100000 && score >= 100000) {
LK.getSound('powerup').play();
}
if (prevScore < 1000000 && score >= 1000000) {
LK.getSound('success').play();
}
updateUI();
refillBoard(function () {
resetGemFlags();
var newMatches = findMatches();
processMatches(newMatches, onFinish);
});
});
}
}
function trySwapAndMatch(gem1, gem2) {
canInput = false;
LK.getSound('swap').play();
swapGems(gem1, gem2, true, function () {
var matches = findMatches();
if (matches.length > 0) {
movesLeft--;
movesMade++; // Increment movesMade after a valid move
updateUI();
processMatches(matches, function () {
canInput = true;
checkGameEnd();
});
} else {
// No match, swap back
swapGems(gem1, gem2, true, function () {
canInput = true;
});
}
});
}
function checkGameEnd() {
// Endless mode: do nothing, never trigger game over
}
// --- Power Meter Activation ---
function activatePower() {
if (powerMeter < powerMeterMax || isPowerActive) {
return;
}
isPowerActive = true;
LK.getSound('powerup').play();
// Randomly clear 2 rows or columns
var cleared = 0;
for (var i = 0; i < 2; i++) {
var mode = Math.random() < 0.5 ? 'row' : 'col';
var idx = Math.floor(Math.random() * (mode === 'row' ? GRID_ROWS : GRID_COLS));
for (var j = 0; j < (mode === 'row' ? GRID_COLS : GRID_ROWS); j++) {
var gem = mode === 'row' ? getGemAt(idx, j) : getGemAt(j, idx);
if (gem) {
gem.isMatched = true;
cleared++;
}
}
}
LK.effects.flashScreen(0xffff00, 600);
removeMatches(function () {
score += cleared * 200 * multiplier;
powerMeter = 0;
isPowerActive = false;
updateUI();
refillBoard(function () {
resetGemFlags();
var newMatches = findMatches();
processMatches(newMatches, function () {
canInput = true;
checkGameEnd();
});
});
});
}
// --- Board Initialization ---
function fillBoardNoMatches() {
// Fill board, avoid initial matches
for (var row = 0; row < GRID_ROWS; row++) {
grid[row] = [];
for (var col = 0; col < GRID_COLS; col++) {
var gem = new Gem();
var type;
do {
// Prevent Zeus from spawning at game start
do {
type = randomGemType();
} while (type === 'zeus');
gem.setType(type);
setGemAt(row, col, gem);
} while (col >= 2 && getGemAt(row, col - 1).gemType === type && getGemAt(row, col - 2).gemType === type || row >= 2 && getGemAt(row - 1, col).gemType === type && getGemAt(row - 2, col).gemType === type);
var pos = gemWorldPos(row, col);
gem.x = pos.x;
gem.y = pos.y;
game.addChild(gem);
}
}
}
// --- Input Handling ---
game.down = function (x, y, obj) {
if (!canInput) {
return;
}
// Convert to board coordinates
var bx = x - boardOffsetX;
var by = y - boardOffsetY;
var col = Math.floor(bx / GEM_SIZE);
var row = Math.floor(by / GEM_SIZE);
var gem = getGemAt(row, col);
if (!gem) {
return;
}
if (selectedGem === gem) {
deselectGem();
return;
}
if (!selectedGem) {
selectGem(gem);
} else {
if (areAdjacent(selectedGem, gem)) {
swappingGem = gem;
trySwapAndMatch(selectedGem, gem);
deselectGem();
} else {
selectGem(gem);
}
}
};
game.move = function (x, y, obj) {
// No drag-swap for now (tap only)
};
game.up = function (x, y, obj) {
// No drag-swap for now (tap only)
};
// --- Power Meter Tap (activate power) ---
powerMeterTxt.interactive = true;
powerMeterTxt.down = function (x, y, obj) {
if (powerMeter >= powerMeterMax && canInput) {
canInput = false;
activatePower();
}
};
// No custom pause menu logic; rely on LK's built-in pause menu
// --- Music Mute Button ---
var musicMuted = true;
var musicBtn = new Text2('🔇', {
size: 110,
fill: "#fff"
});
musicBtn.anchor.set(1, 1);
musicBtn.x = 0; // Will be positioned by LK.gui.bottomRight
musicBtn.y = 0;
musicBtn.interactive = true;
musicBtn.down = function (x, y, obj) {
musicMuted = !musicMuted;
if (musicMuted) {
LK.stopMusic();
musicBtn.setText('🔇');
} else {
LK.playMusic('olympus_theme', {
volume: 0.02,
fade: {
start: 0,
end: 1,
duration: 600
}
});
musicBtn.setText('🔊');
}
};
LK.gui.bottomRight.addChild(musicBtn);
// --- Game Update ---
game.update = function () {
// Play music if not playing and not muted
if (!game._musicStarted && !musicMuted) {
LK.playMusic('olympus_theme', {
volume: 0.02,
fade: {
start: 0,
end: 1,
duration: 1000
}
});
game._musicStarted = true;
}
if (!game._musicStarted && musicMuted) {
// Don't play music, but mark as started to prevent repeated checks
game._musicStarted = true;
}
// Prevent input during animations
// (Handled by canInput flag)
};
// Add pause menu support
// LK's built-in pause menu is enabled by default; no need to call LK.enablePauseMenu()
// --- Game Start ---
function startGame() {
// Reset state
grid = [];
selectedGem = null;
swappingGem = null;
canInput = true;
score = 0;
movesLeft = MOVES_LIMIT;
multiplier = 1;
comboCount = 0;
powerMeter = 0;
isPowerActive = false;
movesMade = 0; // Reset movesMade on game start
updateUI();
// Remove all children except UI
for (var i = game.children.length - 1; i >= 0; i--) {
var ch = game.children[i];
game.removeChild(ch);
}
fillBoardNoMatches();
}
startGame();
diamond. In-Game asset. 2d. High contrast. No shadows
strawberry. In-Game asset. 2d. High contrast. No shadows
banana. In-Game asset. 2d. High contrast. No shadows
Emerald. In-Game asset. 2d. High contrast. No shadows
eggplant. In-Game asset. 2d. High contrast. No shadows
Orange. In-Game asset. 2d. High contrast. No shadows
white lightning but in a yellow neon frame. In-Game asset. 2d. High contrast. No shadows