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