/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
// Floating text for win popups
var FloatingText = Container.expand(function () {
var self = Container.call(this);
self.txt = new Text2('', {
size: 90,
fill: '#fff',
font: "'GillSans-Bold',Impact,'Arial Black',Tahoma"
});
self.txt.anchor.set(0.5, 0.5);
self.addChild(self.txt);
self.show = function (text, x, y, color) {
self.txt.setText(text);
// Defensive: Only set fill if style exists
if (self.txt && self.txt.style) {
self.txt.style.fill = color || '#fff';
}
self.x = x;
self.y = y;
self.alpha = 1;
tween(self, {
y: y - 120,
alpha: 0
}, {
duration: 900,
easing: tween.easeOut,
onFinish: function onFinish() {
self.visible = false;
}
});
self.visible = true;
};
self.visible = false;
return self;
});
// Symbol class for grid symbols
var _Symbol = Container.expand(function () {
var self = Container.call(this);
self.type = null; // e.g. 'candy_red', 'scatter_lollipop', 'bomb_multi'
self.asset = null;
self.row = 0;
self.col = 0;
self.multiplier = 0; // for bomb_multi only
self.setSymbol = function (type, multiplier) {
self.type = type;
if (self.asset) {
self.removeChild(self.asset);
}
var anchor = 0.5;
// For bomb_multi, always use the 'bomb_multi' asset, which can be an image or shape as configured in Assets
self.asset = self.attachAsset(type, {
anchorX: anchor,
anchorY: anchor
});
if (type === 'bomb_multi') {
self.multiplier = multiplier || 2;
if (!self.multiTxt) {
self.multiTxt = new Text2('x' + self.multiplier, {
size: 60,
fill: '#fff'
});
self.multiTxt.anchor.set(0.5, 0.5);
self.addChild(self.multiTxt);
}
self.multiTxt.setText('x' + self.multiplier);
self.multiTxt.visible = true;
} else {
if (self.multiTxt) self.multiTxt.visible = false;
self.multiplier = 0;
}
};
self.flash = function () {
tween(self.asset, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 120,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self.asset, {
scaleX: 1,
scaleY: 1
}, {
duration: 120,
easing: tween.easeIn
});
}
});
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x2d1a3a
});
/****
* Game Code
****/
// --- Game constants ---
// Candy symbols (6 types), lollipop scatter, multiplier bomb
// You can swap the image id here to change the bomb multiplier symbol's appearance
// Wallpaper image asset
var GRID_COLS = 6;
var GRID_ROWS = 5;
var SYMBOL_SIZE = 300; // px, including margin
var SYMBOL_MARGIN = 20;
var SYMBOLS = ['candy_red', 'candy_blue', 'candy_green', 'candy_purple', 'fruit_banana', 'fruit_grape'];
var SCATTER = 'scatter_lollipop';
var BOMB = 'bomb_multi';
var BOMB_MULTIPLIERS = [2, 4, 6, 8, 10, 12, 15, 20, 25, 50, 100];
// --- Game state ---
var grid = []; // 2D array [row][col] of Symbol
var gridContainer = null;
var floatingText = null;
var spinBtn = null;
var buyBtn = null;
var anteBtn = null;
var balanceTxt = null;
var betTxt = null;
var winTxt = null;
var freeSpinTxt = null;
var isSpinning = false;
var isTumbling = false;
var isFreeSpins = false;
var freeSpinsLeft = 0;
var totalFreeSpinWin = 0;
var bet = 20;
var balance = 1000;
var anteBet = false;
var lastWin = 0;
var scatterCount = 0;
var bombs = [];
var totalBombMultiplier = 0;
// Add wallpaper image to the background
var wallpaper = LK.getAsset('wallpaper', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
width: 2048,
height: 2732,
alpha: 0.18 // Set transparency for the wallpaper (more transparent)
});
game.addChild(wallpaper);
// Add a small wallpaper image to the top left corner (do not overlap menu, so offset by 10px)
var wallpaperSmall = LK.getAsset('wallpaper', {
anchorX: 0,
anchorY: 0,
x: 10,
y: 10,
scaleX: 0.08,
scaleY: 0.08
});
game.addChild(wallpaperSmall);
// Add a semi-transparent panel behind the spin grid
var gridPanel = LK.getAsset('bomb_multi', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 8.5,
scaleY: 4.2,
alpha: 0.22
});
game.addChild(gridPanel);
// --- UI setup ---
function setupUI() {
// Balance
balanceTxt = new Text2('Balance: ' + balance, {
size: 70,
fill: '#fff'
});
balanceTxt.anchor.set(0, 0);
LK.gui.top.addChild(balanceTxt);
balanceTxt.x = 120;
balanceTxt.y = 10;
// Bet
betTxt = new Text2('Bet: ' + bet, {
size: 70,
fill: '#fff'
});
betTxt.anchor.set(0, 0);
LK.gui.top.addChild(betTxt);
betTxt.x = 220;
betTxt.y = 90;
// Bet decrease button (top left, vertical layout)
var betDecBtn = new Container();
// Use the bomb_multi asset, which can be swapped to an image in the Assets section
var betDecBg = betDecBtn.attachAsset('bomb_multi', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.25,
scaleY: 0.25
});
var betDecTxt = new Text2('-', {
size: 80,
fill: '#fff'
});
betDecTxt.anchor.set(0.5, 0.5);
betDecBtn.addChild(betDecTxt);
// Place at top left, with margin from edge, vertical stack
betDecBtn.x = 120;
betDecBtn.y = 120;
betDecBtn.interactive = true;
betDecBtn.buttonMode = true;
betDecBtn.down = function (x, y, obj) {
if (isSpinning || isTumbling) return;
var minBet = 10;
if (bet > minBet) {
bet -= 10;
updateBalance();
}
};
// Bet increase button (top left, above dec)
var betIncBtn = new Container();
// Use the bomb_multi asset, which can be swapped to an image in the Assets section
var betIncBg = betIncBtn.attachAsset('bomb_multi', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.25,
scaleY: 0.25
});
var betIncTxt = new Text2('+', {
size: 80,
fill: '#fff'
});
betIncTxt.anchor.set(0.5, 0.5);
betIncBtn.addChild(betIncTxt);
// Place directly above dec button, with same x, y offset by -120
betIncBtn.x = 120;
betIncBtn.y = 0;
betIncBtn.interactive = true;
betIncBtn.buttonMode = true;
betIncBtn.down = function (x, y, obj) {
if (isSpinning || isTumbling) return;
var maxBet = 1000;
if (bet < maxBet && balance >= bet + 10) {
bet += 10;
updateBalance();
}
};
// Add to top left GUI
LK.gui.top.addChild(betDecBtn);
LK.gui.top.addChild(betIncBtn);
// Win
winTxt = new Text2('', {
size: 90,
fill: '#ffe066'
});
winTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(winTxt);
winTxt.x = LK.gui.top.width / 2;
winTxt.y = 10;
// Free Spin
freeSpinTxt = new Text2('', {
size: 80,
fill: '#ffb3e6'
});
freeSpinTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(freeSpinTxt);
freeSpinTxt.x = LK.gui.top.width / 2;
freeSpinTxt.y = 110;
// Spin button
spinBtn = new Text2('SPIN', {
size: 120,
fill: '#fff',
font: "'GillSans-Bold',Impact,'Arial Black',Tahoma"
});
spinBtn.anchor.set(0.5, 0.5);
LK.gui.bottom.addChild(spinBtn);
spinBtn.x = LK.gui.bottom.width / 2;
spinBtn.y = -180;
spinBtn.interactive = true;
spinBtn.buttonMode = true;
spinBtn.down = function (x, y, obj) {
// Block tap if waiting for free spin overlay tap
if (game._freeSpinAwaitingTap) return;
if (!isSpinning && !isTumbling) {
startSpin();
}
};
// Buy Free Spins button
buyBtn = new Text2('BUY 10 FS (' + bet * 100 + ')', {
size: 70,
fill: '#ffb3e6'
});
buyBtn.anchor.set(0.5, 0.5);
LK.gui.bottom.addChild(buyBtn);
buyBtn.x = LK.gui.bottom.width / 2 + 350;
buyBtn.y = -120;
buyBtn.interactive = true;
buyBtn.buttonMode = true;
buyBtn.down = function (x, y, obj) {
var price = bet * 100;
if (!isSpinning && !isTumbling && balance >= price) {
balance -= price;
updateBalance();
triggerFreeSpins(10);
}
};
// Ante Bet button
anteBtn = new Text2('ANTE OFF', {
size: 70,
fill: '#b3e6ff'
});
anteBtn.anchor.set(0.5, 0.5);
LK.gui.bottom.addChild(anteBtn);
anteBtn.x = LK.gui.bottom.width / 2 - 350;
anteBtn.y = -120;
anteBtn.interactive = true;
anteBtn.buttonMode = true;
anteBtn.down = function (x, y, obj) {
anteBet = !anteBet;
anteBtn.setText(anteBet ? 'ANTE ON' : 'ANTE OFF');
// Defensive: Only set fill if style exists
if (anteBtn && anteBtn.style) {
anteBtn.style.fill = anteBet ? '#ffe066' : '#b3e6ff';
}
};
// Floating win text
floatingText = new FloatingText();
game.addChild(floatingText);
}
// --- Grid setup ---
function setupGrid() {
if (gridContainer) {
game.removeChild(gridContainer);
}
gridContainer = new Container();
game.addChild(gridContainer);
// Center grid
var gridW = GRID_COLS * SYMBOL_SIZE + (GRID_COLS - 1) * SYMBOL_MARGIN;
var gridH = GRID_ROWS * SYMBOL_SIZE + (GRID_ROWS - 1) * SYMBOL_MARGIN;
gridContainer.x = (2048 - gridW) / 2 + SYMBOL_SIZE / 2;
gridContainer.y = (2732 - gridH) / 2 + SYMBOL_SIZE / 2 + 60;
// Position the panel behind the grid
if (typeof gridPanel !== "undefined" && gridPanel) {
gridPanel.x = gridContainer.x + gridW / 2 - SYMBOL_SIZE / 2;
gridPanel.y = gridContainer.y + gridH / 2 - SYMBOL_SIZE / 2;
// Ensure panel is behind grid
game.setChildIndex(gridPanel, 0);
}
// Create grid
grid = [];
for (var r = 0; r < GRID_ROWS; r++) {
grid[r] = [];
for (var c = 0; c < GRID_COLS; c++) {
var sym = new _Symbol();
sym.row = r;
sym.col = c;
sym.x = c * (SYMBOL_SIZE + SYMBOL_MARGIN);
sym.y = r * (SYMBOL_SIZE + SYMBOL_MARGIN);
gridContainer.addChild(sym);
grid[r][c] = sym;
}
}
}
// --- Utility: random symbol ---
function getRandomSymbol(scatterChance, bombChance) {
var roll = Math.random();
// 20% chance for bomb
if (bombChance && roll < 0.20) {
// 5% of bombs are 20x or higher
var highMultiRoll = Math.random();
var multi;
if (highMultiRoll < 0.05) {
// Only pick from multipliers >= 20
var highMultis = [];
for (var i = 0; i < BOMB_MULTIPLIERS.length; i++) {
if (BOMB_MULTIPLIERS[i] >= 20) highMultis.push(BOMB_MULTIPLIERS[i]);
}
// Defensive: fallback to max if none found
if (highMultis.length > 0) {
multi = highMultis[Math.floor(Math.random() * highMultis.length)];
} else {
multi = 20;
}
} else {
// Pick from multipliers < 20
var lowMultis = [];
for (var i = 0; i < BOMB_MULTIPLIERS.length; i++) {
if (BOMB_MULTIPLIERS[i] < 20) lowMultis.push(BOMB_MULTIPLIERS[i]);
}
// Defensive: fallback to min if none found
if (lowMultis.length > 0) {
multi = lowMultis[Math.floor(Math.random() * lowMultis.length)];
} else {
multi = 2;
}
}
return {
type: BOMB,
multiplier: multi
};
}
if (scatterChance && roll < scatterChance) {
return {
type: SCATTER
};
}
var idx = Math.floor(Math.random() * SYMBOLS.length);
return {
type: SYMBOLS[idx]
};
}
// --- Utility: count symbol types ---
function countSymbols(flatGrid) {
var counts = {};
for (var i = 0; i < flatGrid.length; i++) {
var t = flatGrid[i].type;
if (!counts[t]) counts[t] = 0;
counts[t]++;
}
return counts;
}
// --- Utility: flatten grid ---
function flattenGrid() {
var arr = [];
for (var r = 0; r < GRID_ROWS; r++) {
for (var c = 0; c < GRID_COLS; c++) {
arr.push(grid[r][c]);
}
}
return arr;
}
// --- Utility: find all matching symbols (8+) ---
function findMatches() {
var flat = flattenGrid();
var counts = countSymbols(flat);
var matches = [];
for (var t in counts) {
if (t === SCATTER || t === BOMB) continue;
if (counts[t] >= 8) {
// collect all symbols of this type
for (var i = 0; i < flat.length; i++) {
if (flat[i].type === t) matches.push(flat[i]);
}
}
}
return matches;
}
// --- Utility: find scatters ---
function findScatters() {
var flat = flattenGrid();
var scatters = [];
for (var i = 0; i < flat.length; i++) {
if (flat[i].type === SCATTER) scatters.push(flat[i]);
}
return scatters;
}
// --- Utility: find bombs ---
function findBombs() {
var flat = flattenGrid();
var bombs = [];
for (var i = 0; i < flat.length; i++) {
if (flat[i].type === BOMB) bombs.push(flat[i]);
}
return bombs;
}
// --- Utility: payout table ---
function getPayout(type, count) {
// Example payout table (per symbol, per count)
// (values are for MVP, can be tuned)
var table = {
'candy_red': [0, 0, 0, 0, 0, 0, 0, 0, 10, 25, 50, 100, 200, 400, 800, 1600, 3200, 6400, 12800, 25600, 51200],
'candy_blue': [0, 0, 0, 0, 0, 0, 0, 0, 8, 20, 40, 80, 160, 320, 640, 1280, 2560, 5120, 10240, 20480, 40960],
'candy_green': [0, 0, 0, 0, 0, 0, 0, 0, 6, 15, 30, 60, 120, 240, 480, 960, 1920, 3840, 7680, 15360, 30720],
'candy_purple': [0, 0, 0, 0, 0, 0, 0, 0, 5, 12, 24, 48, 96, 192, 384, 768, 1536, 3072, 6144, 12288, 24576],
'fruit_banana': [0, 0, 0, 0, 0, 0, 0, 0, 4, 10, 20, 40, 80, 160, 320, 640, 1280, 2560, 5120, 10240, 20480],
'fruit_grape': [0, 0, 0, 0, 0, 0, 0, 0, 3, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384]
};
if (!table[type]) return 0;
if (count >= table[type].length) count = table[type].length - 1;
return table[type][count];
}
// --- Utility: update balance display ---
function updateBalance() {
balanceTxt.setText('Balance: ' + balance);
betTxt.setText('Bet: ' + bet * (anteBet ? 1.25 : 1));
if (buyBtn) {
buyBtn.setText('BUY 10 FS (' + bet * 100 + ')');
}
}
// --- Utility: update win display ---
function updateWin(win) {
if (win > 0) {
winTxt.setText('WIN: ' + win);
} else {
winTxt.setText('');
}
}
// --- Utility: update free spin display ---
function updateFreeSpinTxt() {
if (isFreeSpins) {
freeSpinTxt.setText('FREE SPINS: ' + freeSpinsLeft);
} else {
freeSpinTxt.setText('');
}
}
// --- Start a spin ---
function startSpin() {
if (isSpinning || isTumbling) return;
if (!isFreeSpins && balance < bet * (anteBet ? 1.25 : 1)) return;
isSpinning = true;
updateWin(0);
lastWin = 0;
scatterCount = 0;
bombs = [];
totalBombMultiplier = 0;
if (!isFreeSpins) {
balance -= bet * (anteBet ? 1.25 : 1);
updateBalance();
}
LK.getSound('spin').play();
// Fill grid with new random symbols
for (var r = 0; r < GRID_ROWS; r++) {
for (var c = 0; c < GRID_COLS; c++) {
var scatterChance = isFreeSpins ? 0 : anteBet ? 0.045 : 0.03; // ~3% or 4.5% chance per cell
var bombChance = isFreeSpins ? 0.08 : 0;
var sym = getRandomSymbol(scatterChance, bombChance);
grid[r][c].setSymbol(sym.type, sym.multiplier);
grid[r][c].alpha = 1;
grid[r][c].visible = true;
}
}
// Animate grid in
for (var r = 0; r < GRID_ROWS; r++) {
for (var c = 0; c < GRID_COLS; c++) {
grid[r][c].y = r * (SYMBOL_SIZE + SYMBOL_MARGIN) - 400;
tween(grid[r][c], {
y: r * (SYMBOL_SIZE + SYMBOL_MARGIN)
}, {
duration: 220 + Math.random() * 120,
easing: tween.bounceOut
});
}
}
LK.setTimeout(function () {
isSpinning = false;
checkWinCascade();
}, 420);
}
// --- Check for wins and cascade ---
function checkWinCascade() {
if (isSpinning || isTumbling) return;
var matches = findMatches();
var scatters = findScatters();
var bombsHere = findBombs();
// Handle scatters
if (!isFreeSpins && scatters.length >= 4) {
triggerFreeSpins(10);
LK.getSound('freespin').play();
}
// Handle bombs (only in free spins)
if (isFreeSpins && bombsHere.length > 0) {
for (var i = 0; i < bombsHere.length; i++) {
bombs.push(bombsHere[i]);
totalBombMultiplier += bombsHere[i].multiplier;
LK.getSound('bomb').play();
bombsHere[i].flash();
}
}
if (matches.length > 0) {
isTumbling = true;
LK.getSound('win').play();
// Flash and float win
var matchType = matches[0].type;
var payout = getPayout(matchType, matches.length);
var winAmount = payout * bet / 20;
lastWin += winAmount;
// Show floating text at center of grid
var gridW = GRID_COLS * SYMBOL_SIZE + (GRID_COLS - 1) * SYMBOL_MARGIN;
var gridH = GRID_ROWS * SYMBOL_SIZE + (GRID_ROWS - 1) * SYMBOL_MARGIN;
floatingText.show('+' + winAmount, gridContainer.x + gridW / 2 - SYMBOL_SIZE / 2, gridContainer.y + gridH / 2 - SYMBOL_SIZE / 2, '#ffe066');
// Animate matched symbols out
for (var i = 0; i < matches.length; i++) {
matches[i].flash();
tween(matches[i], {
alpha: 0
}, {
duration: 180,
easing: tween.linear
});
}
// After animation, remove matched symbols and tumble
LK.setTimeout(function () {
for (var i = 0; i < matches.length; i++) {
matches[i].visible = false;
}
tumbleSymbols(function () {
// After tumble, check for more matches
isTumbling = false;
checkWinCascade();
});
}, 220);
updateWin(lastWin);
} else {
// No more matches, finish spin
if (lastWin > 0) {
// --- Win overlay ---
// Remove previous win overlay if exists
if (game.winOverlay) {
game.removeChild(game.winOverlay);
game.winOverlay = null;
}
// Create overlay container
var overlay = new Container();
// Transparent background
var overlayBg = LK.getAsset('bomb_multi', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 7,
scaleY: 2.5,
alpha: 0.38
});
overlay.addChild(overlayBg);
// Win text
var overlayTxt = new Text2('WIN: ' + lastWin, {
size: 180,
fill: '#ffe066',
font: "'GillSans-Bold',Impact,'Arial Black',Tahoma"
});
overlayTxt.anchor.set(0.5, 0.5);
overlay.addChild(overlayTxt);
// Center overlay above grid
var gridW = GRID_COLS * SYMBOL_SIZE + (GRID_COLS - 1) * SYMBOL_MARGIN;
var gridH = GRID_ROWS * SYMBOL_SIZE + (GRID_ROWS - 1) * SYMBOL_MARGIN;
overlay.x = gridContainer.x + gridW / 2 - SYMBOL_SIZE / 2;
overlay.y = gridContainer.y + gridH / 2 - SYMBOL_SIZE / 2;
overlay.zIndex = 9999;
game.addChild(overlay);
game.winOverlay = overlay;
// Fade out overlay after 1.5s
tween(overlay, {
alpha: 0
}, {
duration: 1500,
onFinish: function onFinish() {
if (game.winOverlay) {
game.removeChild(game.winOverlay);
game.winOverlay = null;
}
}
});
// Apply bomb multipliers (free spins only)
if (isFreeSpins && totalBombMultiplier > 0) {
lastWin = Math.floor(lastWin * (1 + totalBombMultiplier / 100));
floatingText.show('x' + (1 + totalBombMultiplier / 100), gridContainer.x + GRID_COLS * SYMBOL_SIZE / 2, gridContainer.y + GRID_ROWS * SYMBOL_SIZE / 2, '#ffb3e6');
LK.getSound('bomb').play();
}
balance += lastWin;
updateBalance();
updateWin(lastWin);
if (isFreeSpins) {
totalFreeSpinWin += lastWin;
}
}
if (isFreeSpins) {
freeSpinsLeft--;
updateFreeSpinTxt();
if (freeSpinsLeft > 0) {
LK.setTimeout(function () {
startSpin();
}, 900);
} else {
// End free spins
isFreeSpins = false;
updateFreeSpinTxt();
if (totalFreeSpinWin > 0) {
floatingText.show('FS WIN: ' + totalFreeSpinWin, gridContainer.x + GRID_COLS * SYMBOL_SIZE / 2, gridContainer.y + GRID_ROWS * SYMBOL_SIZE / 2, '#ffe066');
}
}
}
}
}
// --- Tumble (cascade) symbols ---
function tumbleSymbols(onFinish) {
LK.getSound('tumble').play();
// For each column, move down visible symbols to fill gaps, then add new symbols at top
for (var c = 0; c < GRID_COLS; c++) {
var colSyms = [];
for (var r = 0; r < GRID_ROWS; r++) {
if (grid[r][c].visible) colSyms.push(grid[r][c]);
}
// Fill from bottom up
for (var r = GRID_ROWS - 1; r >= 0; r--) {
if (colSyms.length > 0) {
var sym = colSyms.pop();
grid[r][c].setSymbol(sym.type, sym.multiplier);
grid[r][c].visible = true;
grid[r][c].alpha = 1;
} else {
// New symbol
var scatterChance = isFreeSpins ? 0 : anteBet ? 0.045 : 0.03;
var bombChance = isFreeSpins ? 0.08 : 0;
var symData = getRandomSymbol(scatterChance, bombChance);
grid[r][c].setSymbol(symData.type, symData.multiplier);
grid[r][c].alpha = 0;
grid[r][c].visible = true;
tween(grid[r][c], {
alpha: 1
}, {
duration: 180,
easing: tween.linear
});
}
}
}
LK.setTimeout(function () {
if (onFinish) onFinish();
}, 220);
}
// --- Trigger free spins ---
function triggerFreeSpins(count) {
isFreeSpins = true;
freeSpinsLeft = count;
totalFreeSpinWin = 0;
updateFreeSpinTxt();
LK.getSound('freespin').play();
// --- Animate all lollipops (scatter_lollipop) to shake ---
var flat = flattenGrid();
for (var i = 0; i < flat.length; i++) {
if (flat[i].type === SCATTER) {
// Shake animation: quick left-right movement, repeat a few times
(function (sym) {
var shakeTimes = 6;
var shakeDist = 24;
var origX = sym.x;
var seq = [];
for (var s = 0; s < shakeTimes; s++) {
seq.push({
x: origX + (s % 2 === 0 ? shakeDist : -shakeDist),
duration: 40
});
}
seq.push({
x: origX,
duration: 40
});
// Chain tweens
var _doTween = function doTween(idx) {
if (idx >= seq.length) return;
tween(sym, {
x: seq[idx].x
}, {
duration: seq[idx].duration,
onFinish: function onFinish() {
_doTween(idx + 1);
}
});
};
_doTween(0);
})(flat[i]);
}
}
// --- Show overlay text: "FREE SPINS! Tap to continue" ---
// Remove previous overlay if exists
if (game.freeSpinOverlay) {
game.removeChild(game.freeSpinOverlay);
game.freeSpinOverlay = null;
}
var overlay = new Container();
// Transparent background
var overlayBg = LK.getAsset('bomb_multi', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 7,
scaleY: 2.5,
alpha: 0.38
});
overlay.addChild(overlayBg);
// Overlay text
var overlayTxt = new Text2('FREE SPINS!\nTap to continue', {
size: 140,
fill: '#ffb3e6',
font: "'GillSans-Bold',Impact,'Arial Black',Tahoma",
align: 'center'
});
overlayTxt.anchor.set(0.5, 0.5);
overlay.addChild(overlayTxt);
// Center overlay above grid
var gridW = GRID_COLS * SYMBOL_SIZE + (GRID_COLS - 1) * SYMBOL_MARGIN;
var gridH = GRID_ROWS * SYMBOL_SIZE + (GRID_ROWS - 1) * SYMBOL_MARGIN;
overlay.x = gridContainer.x + gridW / 2 - SYMBOL_SIZE / 2;
overlay.y = gridContainer.y + gridH / 2 - SYMBOL_SIZE / 2;
overlay.zIndex = 9999;
game.addChild(overlay);
game.freeSpinOverlay = overlay;
// Block game.down and spinBtn while overlay is up
game._freeSpinAwaitingTap = true;
// Add a one-time tap handler to remove overlay and start spins
overlay.interactive = true;
overlay.buttonMode = true;
overlay.down = function (x, y, obj) {
if (!game._freeSpinAwaitingTap) return;
game._freeSpinAwaitingTap = false;
if (game.freeSpinOverlay) {
game.removeChild(game.freeSpinOverlay);
game.freeSpinOverlay = null;
}
// Start free spins after tap
startSpin();
};
}
// --- Game update loop (not used for logic, but can be used for future animations) ---
game.update = function () {
// No per-frame logic needed for MVP
};
// --- Game start ---
setupUI();
setupGrid();
updateBalance();
updateWin(0);
updateFreeSpinTxt();
LK.playMusic('bgm', {
fade: {
start: 0,
end: 1,
duration: 1200
}
});
// --- Touch anywhere on grid to spin (for mobile) ---
game.down = function (x, y, obj) {
// Block tap if waiting for free spin overlay tap
if (game._freeSpinAwaitingTap) return;
if (!isSpinning && !isTumbling) {
startSpin();
}
}; /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
// Floating text for win popups
var FloatingText = Container.expand(function () {
var self = Container.call(this);
self.txt = new Text2('', {
size: 90,
fill: '#fff',
font: "'GillSans-Bold',Impact,'Arial Black',Tahoma"
});
self.txt.anchor.set(0.5, 0.5);
self.addChild(self.txt);
self.show = function (text, x, y, color) {
self.txt.setText(text);
// Defensive: Only set fill if style exists
if (self.txt && self.txt.style) {
self.txt.style.fill = color || '#fff';
}
self.x = x;
self.y = y;
self.alpha = 1;
tween(self, {
y: y - 120,
alpha: 0
}, {
duration: 900,
easing: tween.easeOut,
onFinish: function onFinish() {
self.visible = false;
}
});
self.visible = true;
};
self.visible = false;
return self;
});
// Symbol class for grid symbols
var _Symbol = Container.expand(function () {
var self = Container.call(this);
self.type = null; // e.g. 'candy_red', 'scatter_lollipop', 'bomb_multi'
self.asset = null;
self.row = 0;
self.col = 0;
self.multiplier = 0; // for bomb_multi only
self.setSymbol = function (type, multiplier) {
self.type = type;
if (self.asset) {
self.removeChild(self.asset);
}
var anchor = 0.5;
// For bomb_multi, always use the 'bomb_multi' asset, which can be an image or shape as configured in Assets
self.asset = self.attachAsset(type, {
anchorX: anchor,
anchorY: anchor
});
if (type === 'bomb_multi') {
self.multiplier = multiplier || 2;
if (!self.multiTxt) {
self.multiTxt = new Text2('x' + self.multiplier, {
size: 60,
fill: '#fff'
});
self.multiTxt.anchor.set(0.5, 0.5);
self.addChild(self.multiTxt);
}
self.multiTxt.setText('x' + self.multiplier);
self.multiTxt.visible = true;
} else {
if (self.multiTxt) self.multiTxt.visible = false;
self.multiplier = 0;
}
};
self.flash = function () {
tween(self.asset, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 120,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self.asset, {
scaleX: 1,
scaleY: 1
}, {
duration: 120,
easing: tween.easeIn
});
}
});
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x2d1a3a
});
/****
* Game Code
****/
// --- Game constants ---
// Candy symbols (6 types), lollipop scatter, multiplier bomb
// You can swap the image id here to change the bomb multiplier symbol's appearance
// Wallpaper image asset
var GRID_COLS = 6;
var GRID_ROWS = 5;
var SYMBOL_SIZE = 300; // px, including margin
var SYMBOL_MARGIN = 20;
var SYMBOLS = ['candy_red', 'candy_blue', 'candy_green', 'candy_purple', 'fruit_banana', 'fruit_grape'];
var SCATTER = 'scatter_lollipop';
var BOMB = 'bomb_multi';
var BOMB_MULTIPLIERS = [2, 4, 6, 8, 10, 12, 15, 20, 25, 50, 100];
// --- Game state ---
var grid = []; // 2D array [row][col] of Symbol
var gridContainer = null;
var floatingText = null;
var spinBtn = null;
var buyBtn = null;
var anteBtn = null;
var balanceTxt = null;
var betTxt = null;
var winTxt = null;
var freeSpinTxt = null;
var isSpinning = false;
var isTumbling = false;
var isFreeSpins = false;
var freeSpinsLeft = 0;
var totalFreeSpinWin = 0;
var bet = 20;
var balance = 1000;
var anteBet = false;
var lastWin = 0;
var scatterCount = 0;
var bombs = [];
var totalBombMultiplier = 0;
// Add wallpaper image to the background
var wallpaper = LK.getAsset('wallpaper', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
width: 2048,
height: 2732,
alpha: 0.18 // Set transparency for the wallpaper (more transparent)
});
game.addChild(wallpaper);
// Add a small wallpaper image to the top left corner (do not overlap menu, so offset by 10px)
var wallpaperSmall = LK.getAsset('wallpaper', {
anchorX: 0,
anchorY: 0,
x: 10,
y: 10,
scaleX: 0.08,
scaleY: 0.08
});
game.addChild(wallpaperSmall);
// Add a semi-transparent panel behind the spin grid
var gridPanel = LK.getAsset('bomb_multi', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 8.5,
scaleY: 4.2,
alpha: 0.22
});
game.addChild(gridPanel);
// --- UI setup ---
function setupUI() {
// Balance
balanceTxt = new Text2('Balance: ' + balance, {
size: 70,
fill: '#fff'
});
balanceTxt.anchor.set(0, 0);
LK.gui.top.addChild(balanceTxt);
balanceTxt.x = 120;
balanceTxt.y = 10;
// Bet
betTxt = new Text2('Bet: ' + bet, {
size: 70,
fill: '#fff'
});
betTxt.anchor.set(0, 0);
LK.gui.top.addChild(betTxt);
betTxt.x = 220;
betTxt.y = 90;
// Bet decrease button (top left, vertical layout)
var betDecBtn = new Container();
// Use the bomb_multi asset, which can be swapped to an image in the Assets section
var betDecBg = betDecBtn.attachAsset('bomb_multi', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.25,
scaleY: 0.25
});
var betDecTxt = new Text2('-', {
size: 80,
fill: '#fff'
});
betDecTxt.anchor.set(0.5, 0.5);
betDecBtn.addChild(betDecTxt);
// Place at top left, with margin from edge, vertical stack
betDecBtn.x = 120;
betDecBtn.y = 120;
betDecBtn.interactive = true;
betDecBtn.buttonMode = true;
betDecBtn.down = function (x, y, obj) {
if (isSpinning || isTumbling) return;
var minBet = 10;
if (bet > minBet) {
bet -= 10;
updateBalance();
}
};
// Bet increase button (top left, above dec)
var betIncBtn = new Container();
// Use the bomb_multi asset, which can be swapped to an image in the Assets section
var betIncBg = betIncBtn.attachAsset('bomb_multi', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.25,
scaleY: 0.25
});
var betIncTxt = new Text2('+', {
size: 80,
fill: '#fff'
});
betIncTxt.anchor.set(0.5, 0.5);
betIncBtn.addChild(betIncTxt);
// Place directly above dec button, with same x, y offset by -120
betIncBtn.x = 120;
betIncBtn.y = 0;
betIncBtn.interactive = true;
betIncBtn.buttonMode = true;
betIncBtn.down = function (x, y, obj) {
if (isSpinning || isTumbling) return;
var maxBet = 1000;
if (bet < maxBet && balance >= bet + 10) {
bet += 10;
updateBalance();
}
};
// Add to top left GUI
LK.gui.top.addChild(betDecBtn);
LK.gui.top.addChild(betIncBtn);
// Win
winTxt = new Text2('', {
size: 90,
fill: '#ffe066'
});
winTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(winTxt);
winTxt.x = LK.gui.top.width / 2;
winTxt.y = 10;
// Free Spin
freeSpinTxt = new Text2('', {
size: 80,
fill: '#ffb3e6'
});
freeSpinTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(freeSpinTxt);
freeSpinTxt.x = LK.gui.top.width / 2;
freeSpinTxt.y = 110;
// Spin button
spinBtn = new Text2('SPIN', {
size: 120,
fill: '#fff',
font: "'GillSans-Bold',Impact,'Arial Black',Tahoma"
});
spinBtn.anchor.set(0.5, 0.5);
LK.gui.bottom.addChild(spinBtn);
spinBtn.x = LK.gui.bottom.width / 2;
spinBtn.y = -180;
spinBtn.interactive = true;
spinBtn.buttonMode = true;
spinBtn.down = function (x, y, obj) {
// Block tap if waiting for free spin overlay tap
if (game._freeSpinAwaitingTap) return;
if (!isSpinning && !isTumbling) {
startSpin();
}
};
// Buy Free Spins button
buyBtn = new Text2('BUY 10 FS (' + bet * 100 + ')', {
size: 70,
fill: '#ffb3e6'
});
buyBtn.anchor.set(0.5, 0.5);
LK.gui.bottom.addChild(buyBtn);
buyBtn.x = LK.gui.bottom.width / 2 + 350;
buyBtn.y = -120;
buyBtn.interactive = true;
buyBtn.buttonMode = true;
buyBtn.down = function (x, y, obj) {
var price = bet * 100;
if (!isSpinning && !isTumbling && balance >= price) {
balance -= price;
updateBalance();
triggerFreeSpins(10);
}
};
// Ante Bet button
anteBtn = new Text2('ANTE OFF', {
size: 70,
fill: '#b3e6ff'
});
anteBtn.anchor.set(0.5, 0.5);
LK.gui.bottom.addChild(anteBtn);
anteBtn.x = LK.gui.bottom.width / 2 - 350;
anteBtn.y = -120;
anteBtn.interactive = true;
anteBtn.buttonMode = true;
anteBtn.down = function (x, y, obj) {
anteBet = !anteBet;
anteBtn.setText(anteBet ? 'ANTE ON' : 'ANTE OFF');
// Defensive: Only set fill if style exists
if (anteBtn && anteBtn.style) {
anteBtn.style.fill = anteBet ? '#ffe066' : '#b3e6ff';
}
};
// Floating win text
floatingText = new FloatingText();
game.addChild(floatingText);
}
// --- Grid setup ---
function setupGrid() {
if (gridContainer) {
game.removeChild(gridContainer);
}
gridContainer = new Container();
game.addChild(gridContainer);
// Center grid
var gridW = GRID_COLS * SYMBOL_SIZE + (GRID_COLS - 1) * SYMBOL_MARGIN;
var gridH = GRID_ROWS * SYMBOL_SIZE + (GRID_ROWS - 1) * SYMBOL_MARGIN;
gridContainer.x = (2048 - gridW) / 2 + SYMBOL_SIZE / 2;
gridContainer.y = (2732 - gridH) / 2 + SYMBOL_SIZE / 2 + 60;
// Position the panel behind the grid
if (typeof gridPanel !== "undefined" && gridPanel) {
gridPanel.x = gridContainer.x + gridW / 2 - SYMBOL_SIZE / 2;
gridPanel.y = gridContainer.y + gridH / 2 - SYMBOL_SIZE / 2;
// Ensure panel is behind grid
game.setChildIndex(gridPanel, 0);
}
// Create grid
grid = [];
for (var r = 0; r < GRID_ROWS; r++) {
grid[r] = [];
for (var c = 0; c < GRID_COLS; c++) {
var sym = new _Symbol();
sym.row = r;
sym.col = c;
sym.x = c * (SYMBOL_SIZE + SYMBOL_MARGIN);
sym.y = r * (SYMBOL_SIZE + SYMBOL_MARGIN);
gridContainer.addChild(sym);
grid[r][c] = sym;
}
}
}
// --- Utility: random symbol ---
function getRandomSymbol(scatterChance, bombChance) {
var roll = Math.random();
// 20% chance for bomb
if (bombChance && roll < 0.20) {
// 5% of bombs are 20x or higher
var highMultiRoll = Math.random();
var multi;
if (highMultiRoll < 0.05) {
// Only pick from multipliers >= 20
var highMultis = [];
for (var i = 0; i < BOMB_MULTIPLIERS.length; i++) {
if (BOMB_MULTIPLIERS[i] >= 20) highMultis.push(BOMB_MULTIPLIERS[i]);
}
// Defensive: fallback to max if none found
if (highMultis.length > 0) {
multi = highMultis[Math.floor(Math.random() * highMultis.length)];
} else {
multi = 20;
}
} else {
// Pick from multipliers < 20
var lowMultis = [];
for (var i = 0; i < BOMB_MULTIPLIERS.length; i++) {
if (BOMB_MULTIPLIERS[i] < 20) lowMultis.push(BOMB_MULTIPLIERS[i]);
}
// Defensive: fallback to min if none found
if (lowMultis.length > 0) {
multi = lowMultis[Math.floor(Math.random() * lowMultis.length)];
} else {
multi = 2;
}
}
return {
type: BOMB,
multiplier: multi
};
}
if (scatterChance && roll < scatterChance) {
return {
type: SCATTER
};
}
var idx = Math.floor(Math.random() * SYMBOLS.length);
return {
type: SYMBOLS[idx]
};
}
// --- Utility: count symbol types ---
function countSymbols(flatGrid) {
var counts = {};
for (var i = 0; i < flatGrid.length; i++) {
var t = flatGrid[i].type;
if (!counts[t]) counts[t] = 0;
counts[t]++;
}
return counts;
}
// --- Utility: flatten grid ---
function flattenGrid() {
var arr = [];
for (var r = 0; r < GRID_ROWS; r++) {
for (var c = 0; c < GRID_COLS; c++) {
arr.push(grid[r][c]);
}
}
return arr;
}
// --- Utility: find all matching symbols (8+) ---
function findMatches() {
var flat = flattenGrid();
var counts = countSymbols(flat);
var matches = [];
for (var t in counts) {
if (t === SCATTER || t === BOMB) continue;
if (counts[t] >= 8) {
// collect all symbols of this type
for (var i = 0; i < flat.length; i++) {
if (flat[i].type === t) matches.push(flat[i]);
}
}
}
return matches;
}
// --- Utility: find scatters ---
function findScatters() {
var flat = flattenGrid();
var scatters = [];
for (var i = 0; i < flat.length; i++) {
if (flat[i].type === SCATTER) scatters.push(flat[i]);
}
return scatters;
}
// --- Utility: find bombs ---
function findBombs() {
var flat = flattenGrid();
var bombs = [];
for (var i = 0; i < flat.length; i++) {
if (flat[i].type === BOMB) bombs.push(flat[i]);
}
return bombs;
}
// --- Utility: payout table ---
function getPayout(type, count) {
// Example payout table (per symbol, per count)
// (values are for MVP, can be tuned)
var table = {
'candy_red': [0, 0, 0, 0, 0, 0, 0, 0, 10, 25, 50, 100, 200, 400, 800, 1600, 3200, 6400, 12800, 25600, 51200],
'candy_blue': [0, 0, 0, 0, 0, 0, 0, 0, 8, 20, 40, 80, 160, 320, 640, 1280, 2560, 5120, 10240, 20480, 40960],
'candy_green': [0, 0, 0, 0, 0, 0, 0, 0, 6, 15, 30, 60, 120, 240, 480, 960, 1920, 3840, 7680, 15360, 30720],
'candy_purple': [0, 0, 0, 0, 0, 0, 0, 0, 5, 12, 24, 48, 96, 192, 384, 768, 1536, 3072, 6144, 12288, 24576],
'fruit_banana': [0, 0, 0, 0, 0, 0, 0, 0, 4, 10, 20, 40, 80, 160, 320, 640, 1280, 2560, 5120, 10240, 20480],
'fruit_grape': [0, 0, 0, 0, 0, 0, 0, 0, 3, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384]
};
if (!table[type]) return 0;
if (count >= table[type].length) count = table[type].length - 1;
return table[type][count];
}
// --- Utility: update balance display ---
function updateBalance() {
balanceTxt.setText('Balance: ' + balance);
betTxt.setText('Bet: ' + bet * (anteBet ? 1.25 : 1));
if (buyBtn) {
buyBtn.setText('BUY 10 FS (' + bet * 100 + ')');
}
}
// --- Utility: update win display ---
function updateWin(win) {
if (win > 0) {
winTxt.setText('WIN: ' + win);
} else {
winTxt.setText('');
}
}
// --- Utility: update free spin display ---
function updateFreeSpinTxt() {
if (isFreeSpins) {
freeSpinTxt.setText('FREE SPINS: ' + freeSpinsLeft);
} else {
freeSpinTxt.setText('');
}
}
// --- Start a spin ---
function startSpin() {
if (isSpinning || isTumbling) return;
if (!isFreeSpins && balance < bet * (anteBet ? 1.25 : 1)) return;
isSpinning = true;
updateWin(0);
lastWin = 0;
scatterCount = 0;
bombs = [];
totalBombMultiplier = 0;
if (!isFreeSpins) {
balance -= bet * (anteBet ? 1.25 : 1);
updateBalance();
}
LK.getSound('spin').play();
// Fill grid with new random symbols
for (var r = 0; r < GRID_ROWS; r++) {
for (var c = 0; c < GRID_COLS; c++) {
var scatterChance = isFreeSpins ? 0 : anteBet ? 0.045 : 0.03; // ~3% or 4.5% chance per cell
var bombChance = isFreeSpins ? 0.08 : 0;
var sym = getRandomSymbol(scatterChance, bombChance);
grid[r][c].setSymbol(sym.type, sym.multiplier);
grid[r][c].alpha = 1;
grid[r][c].visible = true;
}
}
// Animate grid in
for (var r = 0; r < GRID_ROWS; r++) {
for (var c = 0; c < GRID_COLS; c++) {
grid[r][c].y = r * (SYMBOL_SIZE + SYMBOL_MARGIN) - 400;
tween(grid[r][c], {
y: r * (SYMBOL_SIZE + SYMBOL_MARGIN)
}, {
duration: 220 + Math.random() * 120,
easing: tween.bounceOut
});
}
}
LK.setTimeout(function () {
isSpinning = false;
checkWinCascade();
}, 420);
}
// --- Check for wins and cascade ---
function checkWinCascade() {
if (isSpinning || isTumbling) return;
var matches = findMatches();
var scatters = findScatters();
var bombsHere = findBombs();
// Handle scatters
if (!isFreeSpins && scatters.length >= 4) {
triggerFreeSpins(10);
LK.getSound('freespin').play();
}
// Handle bombs (only in free spins)
if (isFreeSpins && bombsHere.length > 0) {
for (var i = 0; i < bombsHere.length; i++) {
bombs.push(bombsHere[i]);
totalBombMultiplier += bombsHere[i].multiplier;
LK.getSound('bomb').play();
bombsHere[i].flash();
}
}
if (matches.length > 0) {
isTumbling = true;
LK.getSound('win').play();
// Flash and float win
var matchType = matches[0].type;
var payout = getPayout(matchType, matches.length);
var winAmount = payout * bet / 20;
lastWin += winAmount;
// Show floating text at center of grid
var gridW = GRID_COLS * SYMBOL_SIZE + (GRID_COLS - 1) * SYMBOL_MARGIN;
var gridH = GRID_ROWS * SYMBOL_SIZE + (GRID_ROWS - 1) * SYMBOL_MARGIN;
floatingText.show('+' + winAmount, gridContainer.x + gridW / 2 - SYMBOL_SIZE / 2, gridContainer.y + gridH / 2 - SYMBOL_SIZE / 2, '#ffe066');
// Animate matched symbols out
for (var i = 0; i < matches.length; i++) {
matches[i].flash();
tween(matches[i], {
alpha: 0
}, {
duration: 180,
easing: tween.linear
});
}
// After animation, remove matched symbols and tumble
LK.setTimeout(function () {
for (var i = 0; i < matches.length; i++) {
matches[i].visible = false;
}
tumbleSymbols(function () {
// After tumble, check for more matches
isTumbling = false;
checkWinCascade();
});
}, 220);
updateWin(lastWin);
} else {
// No more matches, finish spin
if (lastWin > 0) {
// --- Win overlay ---
// Remove previous win overlay if exists
if (game.winOverlay) {
game.removeChild(game.winOverlay);
game.winOverlay = null;
}
// Create overlay container
var overlay = new Container();
// Transparent background
var overlayBg = LK.getAsset('bomb_multi', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 7,
scaleY: 2.5,
alpha: 0.38
});
overlay.addChild(overlayBg);
// Win text
var overlayTxt = new Text2('WIN: ' + lastWin, {
size: 180,
fill: '#ffe066',
font: "'GillSans-Bold',Impact,'Arial Black',Tahoma"
});
overlayTxt.anchor.set(0.5, 0.5);
overlay.addChild(overlayTxt);
// Center overlay above grid
var gridW = GRID_COLS * SYMBOL_SIZE + (GRID_COLS - 1) * SYMBOL_MARGIN;
var gridH = GRID_ROWS * SYMBOL_SIZE + (GRID_ROWS - 1) * SYMBOL_MARGIN;
overlay.x = gridContainer.x + gridW / 2 - SYMBOL_SIZE / 2;
overlay.y = gridContainer.y + gridH / 2 - SYMBOL_SIZE / 2;
overlay.zIndex = 9999;
game.addChild(overlay);
game.winOverlay = overlay;
// Fade out overlay after 1.5s
tween(overlay, {
alpha: 0
}, {
duration: 1500,
onFinish: function onFinish() {
if (game.winOverlay) {
game.removeChild(game.winOverlay);
game.winOverlay = null;
}
}
});
// Apply bomb multipliers (free spins only)
if (isFreeSpins && totalBombMultiplier > 0) {
lastWin = Math.floor(lastWin * (1 + totalBombMultiplier / 100));
floatingText.show('x' + (1 + totalBombMultiplier / 100), gridContainer.x + GRID_COLS * SYMBOL_SIZE / 2, gridContainer.y + GRID_ROWS * SYMBOL_SIZE / 2, '#ffb3e6');
LK.getSound('bomb').play();
}
balance += lastWin;
updateBalance();
updateWin(lastWin);
if (isFreeSpins) {
totalFreeSpinWin += lastWin;
}
}
if (isFreeSpins) {
freeSpinsLeft--;
updateFreeSpinTxt();
if (freeSpinsLeft > 0) {
LK.setTimeout(function () {
startSpin();
}, 900);
} else {
// End free spins
isFreeSpins = false;
updateFreeSpinTxt();
if (totalFreeSpinWin > 0) {
floatingText.show('FS WIN: ' + totalFreeSpinWin, gridContainer.x + GRID_COLS * SYMBOL_SIZE / 2, gridContainer.y + GRID_ROWS * SYMBOL_SIZE / 2, '#ffe066');
}
}
}
}
}
// --- Tumble (cascade) symbols ---
function tumbleSymbols(onFinish) {
LK.getSound('tumble').play();
// For each column, move down visible symbols to fill gaps, then add new symbols at top
for (var c = 0; c < GRID_COLS; c++) {
var colSyms = [];
for (var r = 0; r < GRID_ROWS; r++) {
if (grid[r][c].visible) colSyms.push(grid[r][c]);
}
// Fill from bottom up
for (var r = GRID_ROWS - 1; r >= 0; r--) {
if (colSyms.length > 0) {
var sym = colSyms.pop();
grid[r][c].setSymbol(sym.type, sym.multiplier);
grid[r][c].visible = true;
grid[r][c].alpha = 1;
} else {
// New symbol
var scatterChance = isFreeSpins ? 0 : anteBet ? 0.045 : 0.03;
var bombChance = isFreeSpins ? 0.08 : 0;
var symData = getRandomSymbol(scatterChance, bombChance);
grid[r][c].setSymbol(symData.type, symData.multiplier);
grid[r][c].alpha = 0;
grid[r][c].visible = true;
tween(grid[r][c], {
alpha: 1
}, {
duration: 180,
easing: tween.linear
});
}
}
}
LK.setTimeout(function () {
if (onFinish) onFinish();
}, 220);
}
// --- Trigger free spins ---
function triggerFreeSpins(count) {
isFreeSpins = true;
freeSpinsLeft = count;
totalFreeSpinWin = 0;
updateFreeSpinTxt();
LK.getSound('freespin').play();
// --- Animate all lollipops (scatter_lollipop) to shake ---
var flat = flattenGrid();
for (var i = 0; i < flat.length; i++) {
if (flat[i].type === SCATTER) {
// Shake animation: quick left-right movement, repeat a few times
(function (sym) {
var shakeTimes = 6;
var shakeDist = 24;
var origX = sym.x;
var seq = [];
for (var s = 0; s < shakeTimes; s++) {
seq.push({
x: origX + (s % 2 === 0 ? shakeDist : -shakeDist),
duration: 40
});
}
seq.push({
x: origX,
duration: 40
});
// Chain tweens
var _doTween = function doTween(idx) {
if (idx >= seq.length) return;
tween(sym, {
x: seq[idx].x
}, {
duration: seq[idx].duration,
onFinish: function onFinish() {
_doTween(idx + 1);
}
});
};
_doTween(0);
})(flat[i]);
}
}
// --- Show overlay text: "FREE SPINS! Tap to continue" ---
// Remove previous overlay if exists
if (game.freeSpinOverlay) {
game.removeChild(game.freeSpinOverlay);
game.freeSpinOverlay = null;
}
var overlay = new Container();
// Transparent background
var overlayBg = LK.getAsset('bomb_multi', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 7,
scaleY: 2.5,
alpha: 0.38
});
overlay.addChild(overlayBg);
// Overlay text
var overlayTxt = new Text2('FREE SPINS!\nTap to continue', {
size: 140,
fill: '#ffb3e6',
font: "'GillSans-Bold',Impact,'Arial Black',Tahoma",
align: 'center'
});
overlayTxt.anchor.set(0.5, 0.5);
overlay.addChild(overlayTxt);
// Center overlay above grid
var gridW = GRID_COLS * SYMBOL_SIZE + (GRID_COLS - 1) * SYMBOL_MARGIN;
var gridH = GRID_ROWS * SYMBOL_SIZE + (GRID_ROWS - 1) * SYMBOL_MARGIN;
overlay.x = gridContainer.x + gridW / 2 - SYMBOL_SIZE / 2;
overlay.y = gridContainer.y + gridH / 2 - SYMBOL_SIZE / 2;
overlay.zIndex = 9999;
game.addChild(overlay);
game.freeSpinOverlay = overlay;
// Block game.down and spinBtn while overlay is up
game._freeSpinAwaitingTap = true;
// Add a one-time tap handler to remove overlay and start spins
overlay.interactive = true;
overlay.buttonMode = true;
overlay.down = function (x, y, obj) {
if (!game._freeSpinAwaitingTap) return;
game._freeSpinAwaitingTap = false;
if (game.freeSpinOverlay) {
game.removeChild(game.freeSpinOverlay);
game.freeSpinOverlay = null;
}
// Start free spins after tap
startSpin();
};
}
// --- Game update loop (not used for logic, but can be used for future animations) ---
game.update = function () {
// No per-frame logic needed for MVP
};
// --- Game start ---
setupUI();
setupGrid();
updateBalance();
updateWin(0);
updateFreeSpinTxt();
LK.playMusic('bgm', {
fade: {
start: 0,
end: 1,
duration: 1200
}
});
// --- Touch anywhere on grid to spin (for mobile) ---
game.down = function (x, y, obj) {
// Block tap if waiting for free spin overlay tap
if (game._freeSpinAwaitingTap) return;
if (!isSpinning && !isTumbling) {
startSpin();
}
};
scatter_lollipop. In-Game asset. 2d. High contrast. No shadows
fruit_grape. In-Game asset. 2d. High contrast. No shadows
fruit_banana. In-Game asset. 2d. High contrast. No shadows
candy_red. In-Game asset. 2d. High contrast. No shadows
candy_purple. In-Game asset. 2d. High contrast. No shadows
candy_green. In-Game asset. 2d. High contrast. No shadows
candy_blue. In-Game asset. 2d. High contrast. No shadows
bomb_multi. In-Game asset. 2d. High contrast. No shadows
"Meyve Ul Cima" text logo. In-Game asset. 2d. High contrast. No shadows