/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ // Passive Bug class (background animation) var Bug = Container.expand(function () { var self = Container.call(this); // Pick a random bug type var bugTypes = [{ id: 'bocek_orumcek', name: 'örümcek' }, { id: 'bocek_kirkayak', name: 'kırkayak' }, { id: 'bocek_karafatma', name: 'karafatma' }, { id: 'bocek_hamamb', name: 'hamamböceği' }]; var bugType = bugTypes[Math.floor(Math.random() * bugTypes.length)]; self.bugType = bugType.name; // Attach bug asset self.asset = self.attachAsset(bugType.id, { anchorX: 0.5, anchorY: 0.5 }); // State self.active = false; self.targetX = 0; self.targetY = 0; self.speed = 2 + Math.random() * 2; // px per frame self.hideTimeout = null; self.showTimeout = null; // Helper: set random position (offscreen or edge) self.setRandomStart = function () { // Appear from a random edge var edge = Math.floor(Math.random() * 4); // 0: left, 1: right, 2: top, 3: bottom var margin = 100; if (edge === 0) { // left self.x = -margin; self.y = margin + Math.random() * (2732 - 2 * margin); } else if (edge === 1) { // right self.x = 2048 + margin; self.y = margin + Math.random() * (2732 - 2 * margin); } else if (edge === 2) { // top self.x = margin + Math.random() * (2048 - 2 * margin); self.y = -margin; } else { // bottom self.x = margin + Math.random() * (2048 - 2 * margin); self.y = 2732 + margin; } }; // Helper: set random target inside screen self.setRandomTarget = function () { var margin = 200; self.targetX = margin + Math.random() * (2048 - 2 * margin); self.targetY = margin + Math.random() * (2732 - 2 * margin); }; // Show bug: appear and start moving self.showBug = function () { self.setRandomStart(); self.setRandomTarget(); self.visible = true; self.active = true; self.alpha = 0.7 + Math.random() * 0.3; self.scaleX = self.scaleY = 0.7 + Math.random() * 0.5; // Set random speed self.speed = 2 + Math.random() * 2; // Set random zIndex so bugs can overlap self.zIndex = 0; // Set random rotation self.rotation = Math.random() * Math.PI * 2; // Set how long bug will stay visible var minShow = 2000, maxShow = 5000; if (self.hideTimeout) LK.clearTimeout(self.hideTimeout); self.hideTimeout = LK.setTimeout(function () { self.hideBug(); }, minShow + Math.random() * (maxShow - minShow)); }; // Hide bug: disappear and schedule next appearance self.hideBug = function () { self.visible = false; self.active = false; // Schedule next appearance var minDelay = 1500, maxDelay = 4000; if (self.showTimeout) LK.clearTimeout(self.showTimeout); self.showTimeout = LK.setTimeout(function () { self.showBug(); }, minDelay + Math.random() * (maxDelay - minDelay)); }; // Per-frame update self.update = function () { if (!self.active) return; // --- Jitter: bugs shake a little as they move --- // Add a small random offset to position for jitter var jitterAmount = 2 + Math.random() * 2; var jitterAngle = Math.random() * Math.PI * 2; var jitterX = Math.cos(jitterAngle) * jitterAmount; var jitterY = Math.sin(jitterAngle) * jitterAmount; // --- Sudden direction change: sometimes pick a new target suddenly --- if (typeof self._framesToNextTurn === "undefined") { self._framesToNextTurn = 30 + Math.floor(Math.random() * 60); } self._framesToNextTurn--; if (self._framesToNextTurn <= 0) { // 40% chance to pick a new random target (simulate sudden turn) if (Math.random() < 0.4) { self.setRandomTarget(); // Also randomize speed and rotation for realism self.speed = 2 + Math.random() * 2; self.rotation = Math.random() * Math.PI * 2; } self._framesToNextTurn = 30 + Math.floor(Math.random() * 60); } // Move towards target var dx = self.targetX - self.x; var dy = self.targetY - self.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < self.speed) { // Arrived at target, pick a new target self.setRandomTarget(); } else { // Move with a little jitter self.x += dx / dist * self.speed + jitterX; self.y += dy / dist * self.speed + jitterY; // Optionally, face the direction of movement self.rotation = Math.atan2(dy, dx) + (Math.random() - 0.5) * 0.2; } }; // Start hidden, then schedule first appearance self.visible = false; self.active = false; self.setRandomStart(); self.setRandomTarget(); // Schedule first appearance self.showTimeout = LK.setTimeout(function () { self.showBug(); }, 1000 + Math.random() * 2000); return self; }); // Crystal class var Crystal = Container.expand(function () { var self = Container.call(this); // type: 'blue', 'yellow', 'red', 'purple', 'white', 'joker' self.type = 'blue'; self.row = 0; self.col = 0; self.popping = false; // Attach correct asset function setAsset() { if (self.asset) self.removeChild(self.asset); var assetId = 'crystal_' + self.type; self.asset = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); } self.setType = function (type) { self.type = type; setAsset(); }; // Animate pop self.pop = function (_onFinish) { if (self.popping) return; self.popping = true; tween(self, { scaleX: 1.4, scaleY: 1.4, alpha: 0 }, { duration: 350, easing: tween.easeOut, onFinish: function onFinish() { if (_onFinish) _onFinish(); } }); LK.getSound('crystal_pop').play(); }; // Reset visual state self.resetVisual = function () { self.scaleX = 1; self.scaleY = 1; self.alpha = 1; self.popping = false; // Remove freeze frame if present if (self.freezeFrame) { self.removeChild(self.freezeFrame); self.freezeFrame = null; } }; // Add or remove freeze frame self.setFreezeFrame = function (show) { if (show) { if (!self.freezeFrame) { // Create a square frame using a box shape, slightly larger than the crystal var frame = LK.getAsset('spin_btn_shape', { anchorX: 0.5, anchorY: 0.5 }); frame.width = self.asset.width * 1.18; frame.height = self.asset.height * 1.18; frame.alpha = 0.7; frame.tint = 0x00e6b8; frame.zIndex = -1; self.addChildAt(frame, 0); self.freezeFrame = frame; } if (self.freezeFrame) self.freezeFrame.visible = true; } else { if (self.freezeFrame) { self.removeChild(self.freezeFrame); self.freezeFrame = null; } } }; setAsset(); return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x181c2c }); /**** * Game Code ****/ // Böcek görselleri (örümcek, kırkayak, karafatma, hamamböceği) // Add background image behind all elements var backgroundImage = LK.getAsset('background_main', { anchorX: 0.5, anchorY: 0.5, x: 2048 / 2, y: 2732 / 2 }); game.addChildAt(backgroundImage, 0); // Always at the back // --- Passive Bugs (background animation) --- var bugs = []; // Ensure each bug type is present and moving var bugTypeIds = ['bocek_orumcek', 'bocek_kirkayak', 'bocek_karafatma', 'bocek_hamamb']; for (var i = 0; i < bugTypeIds.length; i++) { var bug = new Bug(); // Force the bug to use a specific type for each instance var bugTypes = [{ id: 'bocek_orumcek', name: 'örümcek' }, { id: 'bocek_kirkayak', name: 'kırkayak' }, { id: 'bocek_karafatma', name: 'karafatma' }, { id: 'bocek_hamamb', name: 'hamamböceği' }]; // Set bug type and asset bug.bugType = bugTypes[i].name; if (bug.asset) bug.removeChild(bug.asset); bug.asset = bug.attachAsset(bugTypeIds[i], { anchorX: 0.5, anchorY: 0.5 }); // Place bugs behind all game elements but above background game.addChildAt(bug, 1); bugs.push(bug); } // --- Game Constants --- // Crystal shapes (box for simplicity, different colors for each type) // Sound effects (placeholders, actual sound assets will be loaded by LK) var BOARD_COLS = 4; var BOARD_ROWS = 6; var CRYSTAL_SIZE = 300; // px var CRYSTAL_SPACING = 24; // px var BOARD_WIDTH = BOARD_COLS * CRYSTAL_SIZE + (BOARD_COLS - 1) * CRYSTAL_SPACING; var BOARD_HEIGHT = BOARD_ROWS * CRYSTAL_SIZE + (BOARD_ROWS - 1) * CRYSTAL_SPACING; var BOARD_X = (2048 - BOARD_WIDTH) / 2; var BOARD_Y = (2732 - BOARD_HEIGHT) / 2 - 180; // move board up to make space for spin button var CRYSTAL_TYPES = ['blue', 'yellow', 'red', 'purple', 'white', 'joker']; var CRYSTAL_COLORS = { blue: 0x3a8ee6, yellow: 0xf7e14a, red: 0xe64a3a, purple: 0xb44ae6, white: 0xffffff, joker: 0x00e6b8 }; // Score values per crystal type var CRYSTAL_SCORES = { blue: 10, yellow: 12, red: 14, purple: 16, white: 18, joker: 25 }; // --- Game State --- var board = []; // 2D array [row][col] of Crystal or null var poppingCrystals = []; // Crystals currently popping var isCascading = false; var scoreTxt; var cascadeTimeout = null; // --- Level and Joker Probability State --- var level = 1; var jokerProbability = 0.10; // Start at 10% // --- UI: Score --- scoreTxt = new Text2('0', { size: 120, fill: 0xFFFFFF }); scoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(scoreTxt); // --- Board Initialization --- function createEmptyBoard() { board = []; for (var row = 0; row < BOARD_ROWS; row++) { var rowArr = []; for (var col = 0; col < BOARD_COLS; col++) { rowArr.push(null); } board.push(rowArr); } } function randomCrystalType() { // Use current jokerProbability (decreases as level increases) var r = Math.random(); if (r < jokerProbability) return 'joker'; var idx = Math.floor(Math.random() * 5); return CRYSTAL_TYPES[idx]; } function spawnCrystal(row, col, type) { var crystal = new Crystal(); crystal.setType(type); crystal.row = row; crystal.col = col; // Position var x = BOARD_X + col * (CRYSTAL_SIZE + CRYSTAL_SPACING) + CRYSTAL_SIZE / 2; var y = BOARD_Y + row * (CRYSTAL_SIZE + CRYSTAL_SPACING) + CRYSTAL_SIZE / 2; crystal.x = x; crystal.y = y; crystal.resetVisual(); game.addChild(crystal); // Make crystal fixed: prevent any movement or change // If this crystal is frozen, make sure it cannot be replaced or destroyed if (crystal.frozen) { crystal.interactive = false; crystal.buttonMode = false; crystal.down = function () {}; crystal.pop = function () {}; crystal.setType = function (type) {}; crystal.resetVisual = function () {}; crystal.setFreezeFrame = function () {}; } return crystal; } function fillBoardRandom() { for (var row = 0; row < BOARD_ROWS; row++) { for (var col = 0; col < BOARD_COLS; col++) { var type = randomCrystalType(); var crystal = spawnCrystal(row, col, type); board[row][col] = crystal; } } refreshCrystalInteractivity(); } // --- Board Utility --- function getNeighbors(row, col) { // 4-directional neighbors var n = []; if (row > 0) n.push([row - 1, col]); if (row < BOARD_ROWS - 1) n.push([row + 1, col]); if (col > 0) n.push([row, col - 1]); if (col < BOARD_COLS - 1) n.push([row, col + 1]); return n; } // --- Find Groups for Popping --- function findPopGroups() { // Returns: [{type, crystals: [Crystal, ...], jokerCount: n}] var groups = []; // Count all crystals by type (excluding jokers) var typeCounts = {}; var typeCrystals = {}; var jokerCrystals = []; for (var i = 0; i < CRYSTAL_TYPES.length; i++) { var t = CRYSTAL_TYPES[i]; if (t !== 'joker') { typeCounts[t] = 0; typeCrystals[t] = []; } } for (var row = 0; row < BOARD_ROWS; row++) { for (var col = 0; col < BOARD_COLS; col++) { var crystal = board[row][col]; if (!crystal) continue; if (crystal.type === 'joker') { // Frozen jokers can pop if they are part of a valid pop group, so always count them jokerCrystals.push(crystal); } else { // Frozen crystals can pop if they are part of a valid pop group, so always count them typeCounts[crystal.type]++; typeCrystals[crystal.type].push(crystal); } } } // For each color, see if color+joker >= 10, if so, pop all of that color and enough jokers for (var t in typeCounts) { var total = typeCounts[t] + jokerCrystals.length; if (total >= 10) { // Add all of this color var groupCrystals = []; for (var i = 0; i < typeCrystals[t].length; i++) { groupCrystals.push(typeCrystals[t][i]); } // Add jokers for (var j = 0; j < jokerCrystals.length; j++) { groupCrystals.push(jokerCrystals[j]); } groups.push({ type: t, crystals: groupCrystals, jokerCount: jokerCrystals.length }); } } return groups; } // --- Pop Groups --- function popGroups(groups, onFinish) { if (groups.length === 0) { if (onFinish) onFinish(); return; } isCascading = true; poppingCrystals = []; var popped = {}; // --- Combo Multiplier Logic --- if (typeof comboMultiplier === "undefined") { comboMultiplier = 1; } if (typeof lastCascadeMoveId === "undefined") { lastCascadeMoveId = 0; } if (typeof currentCascadeMoveId === "undefined") { currentCascadeMoveId = 0; } // If this is a new cascade move, reset comboMultiplier if (typeof cascadeMoveActive === "undefined" || !cascadeMoveActive) { comboMultiplier = 1; currentCascadeMoveId = Date.now(); lastCascadeMoveId = currentCascadeMoveId; cascadeMoveActive = true; } else { // If still in the same move, increment comboMultiplier if (currentCascadeMoveId === lastCascadeMoveId) { comboMultiplier++; } else { comboMultiplier = 1; currentCascadeMoveId = Date.now(); lastCascadeMoveId = currentCascadeMoveId; } } // Mark all crystals to pop (avoid double pop) for (var g = 0; g < groups.length; g++) { var group = groups[g]; for (var i = 0; i < group.crystals.length; i++) { var c = group.crystals[i]; var key = c.row + ',' + c.col; if (!popped[key]) { popped[key] = true; poppingCrystals.push(c); } } } // Animate pop with 2s shake before popping var popCount = poppingCrystals.length; var finished = 0; for (var i = 0; i < poppingCrystals.length; i++) { var c = poppingCrystals[i]; (function (crystal) { // Save original position var origX = crystal.x; var origY = crystal.y; var shakeDuration = 2000; var shakeInterval = 30; var elapsed = 0; var shakeTimer = null; function startShake() { shakeTimer = LK.setInterval(function () { // Random shake offset, smaller amplitude for subtle effect var dx = (Math.random() - 0.5) * 24; var dy = (Math.random() - 0.5) * 24; crystal.x = origX + dx; crystal.y = origY + dy; elapsed += shakeInterval; if (elapsed >= shakeDuration) { // End shake, restore position LK.clearInterval(shakeTimer); crystal.x = origX; crystal.y = origY; // Now pop crystal.pop(function () { finished++; if (finished === popCount) { // Remove from board for (var j = 0; j < poppingCrystals.length; j++) { var pc = poppingCrystals[j]; if (board[pc.row][pc.col] === pc) { board[pc.row][pc.col] = null; pc.destroy(); } } poppingCrystals = []; if (onFinish) onFinish(); } }); } }, shakeInterval); } startShake(); })(c); } // Score with combo multiplier for (var g = 0; g < groups.length; g++) { var group = groups[g]; var baseScore = 0; for (var i = 0; i < group.crystals.length; i++) { var c = group.crystals[i]; var t = c.type === 'joker' ? group.type : c.type; baseScore += CRYSTAL_SCORES[t]; } LK.setScore(LK.getScore() + baseScore * comboMultiplier); } scoreTxt.setText(LK.getScore()); LK.getSound('cascade').play(); LK.effects.flashObject(scoreTxt, 0xffff00, 400); // --- Level Up Logic --- var newLevel = Math.floor(LK.getScore() / 1000) + 1; if (newLevel > level) { level = newLevel; // Decrease jokerProbability by 1% per level up, but not below 1% jokerProbability = Math.max(0.01, 0.10 - (level - 1) * 0.01); // Optional: flash score text or give feedback for level up LK.effects.flashObject(scoreTxt, 0x00ff00, 600); } } // --- Cascade: Drop Crystals Down and Fill --- function cascadeBoard(onFinish) { // For each column, drop crystals down to fill empty spaces var changed = false; for (var col = 0; col < BOARD_COLS; col++) { var emptyRows = []; for (var row = BOARD_ROWS - 1; row >= 0; row--) { if (!board[row][col]) { emptyRows.push(row); } else if (emptyRows.length > 0) { // Move crystal down var targetRow = emptyRows.shift(); var crystal = board[row][col]; board[targetRow][col] = crystal; board[row][col] = null; crystal.row = targetRow; crystal.col = col; // Animate drop var newY = BOARD_Y + targetRow * (CRYSTAL_SIZE + CRYSTAL_SPACING) + CRYSTAL_SIZE / 2; tween(crystal, { y: newY }, { duration: 220, easing: tween.cubicOut }); emptyRows.push(row); changed = true; } } } // Fill empty spaces at the top for (var col = 0; col < BOARD_COLS; col++) { for (var row = 0; row < BOARD_ROWS; row++) { if (!board[row][col]) { var type = randomCrystalType(); var crystal = spawnCrystal(row, col, type); board[row][col] = crystal; // Drop from above var startY = BOARD_Y - CRYSTAL_SIZE; crystal.y = startY; var targetY = BOARD_Y + row * (CRYSTAL_SIZE + CRYSTAL_SPACING) + CRYSTAL_SIZE / 2; tween(crystal, { y: targetY }, { duration: 220, easing: tween.cubicOut }); changed = true; } } } // Wait for animation, then call onFinish LK.setTimeout(function () { refreshCrystalInteractivity(); if (onFinish) onFinish(); }, 250); } // --- Main Game Loop: Cascade, Pop, Repeat --- function startCascadeLoop() { if (cascadeTimeout) { LK.clearTimeout(cascadeTimeout); cascadeTimeout = null; } var groups = findPopGroups(); if (groups.length === 0) { isCascading = false; return; } popGroups(groups, function () { cascadeBoard(function () { // Chain reaction! cascadeTimeout = LK.setTimeout(function () { startCascadeLoop(); }, 120); }); }); } // --- Start New Game --- function startGame() { LK.setScore(2000); scoreTxt.setText('2000'); // Reset combo multiplier state for new game comboMultiplier = 1; cascadeMoveActive = false; // Reset bomb usage for new game bombUsed = false; // Reset swap usage for new game swapUsedCount = 0; if (swapBtnText) swapBtnText.setText('Swap (' + (swapMaxCount - swapUsedCount) + ')'); if (swapBtnBg) swapBtnBg.alpha = 1; if (swapBtnContainer) { swapBtnContainer.interactive = true; swapBtnContainer.buttonMode = true; } // Reset freeze usage for new game freezeUsedCount = 0; freezeBtnText.setText('Freeze (' + (freezeMaxCount - freezeUsedCount) + ')'); freezeBtnBg.alpha = 1; freezeBtnContainer.interactive = true; freezeBtnContainer.buttonMode = true; // Reset level and jokerProbability level = 1; jokerProbability = 0.10; // Remove all crystals for (var row = 0; row < BOARD_ROWS; row++) { for (var col = 0; col < BOARD_COLS; col++) { if (board[row] && board[row][col]) { board[row][col].frozen = false; board[row][col].alpha = 1; // Remove freeze frame if present if (typeof board[row][col].setFreezeFrame === "function") { board[row][col].setFreezeFrame(false); } board[row][col].destroy(); } } } createEmptyBoard(); fillBoardRandom(); refreshCrystalInteractivity(); LK.setTimeout(function () { startCascadeLoop(); }, 400); } // --- Karıştır Button --- var shuffleBtn = new Text2('Karıştır', { size: 110, fill: 0x00E6B8, font: "Arial Black" }); shuffleBtn.anchor.set(0.5, 1); shuffleBtn.x = LK.gui.width / 2; shuffleBtn.y = LK.gui.height - 60 - 140; // Move up to make space for spin button shuffleBtn.interactive = true; shuffleBtn.buttonMode = true; LK.gui.bottom.addChild(shuffleBtn); function reshuffleBoardWithFall() { if (isCascading) return; // Reset combo multiplier state for new move comboMultiplier = 1; cascadeMoveActive = false; // Remove all crystals for (var row = 0; row < BOARD_ROWS; row++) { for (var col = 0; col < BOARD_COLS; col++) { if (board[row][col]) { // Only destroy if not a frozen crystal if (!board[row][col].frozen) { board[row][col].frozen = false; board[row][col].alpha = 1; // Remove freeze frame if present if (typeof board[row][col].setFreezeFrame === "function") { board[row][col].setFreezeFrame(false); } board[row][col].destroy(); board[row][col] = null; } } } } // Do not recreate the board, just refill non-frozen spots // Fill board with new crystals, but spawn them above and animate falling for (var row = 0; row < BOARD_ROWS; row++) { for (var col = 0; col < BOARD_COLS; col++) { // Eğer kristal donmuş ve justFrozen ise, bu turda silme, sadece işaretle (removeNextTurn) if (board[row][col] && board[row][col].frozen && board[row][col].justFrozen) { // Bir tur bekle, bu turda sadece işaretle board[row][col].justFrozen = false; board[row][col].removeNextTurn = true; if (typeof board[row][col].setFreezeFrame === "function") { board[row][col].setFreezeFrame(false); } tween(board[row][col], { alpha: 1 }, { duration: 300, easing: tween.easeOut }); } // Eğer kristal donmuş ve removeNextTurn işaretli ise, bu turda sil else if (board[row][col] && board[row][col].frozen && board[row][col].removeNextTurn) { board[row][col].frozen = false; board[row][col].removeNextTurn = false; if (typeof board[row][col].setFreezeFrame === "function") { board[row][col].setFreezeFrame(false); } // Kristali sil board[row][col].destroy(); board[row][col] = null; } // Eğer kristal donmuş ve justFrozen değilse, donmayı kaldır (yani ikinci turda donma kalksın) else if (board[row][col] && board[row][col].frozen && !board[row][col].justFrozen) { board[row][col].frozen = false; if (typeof board[row][col].setFreezeFrame === "function") { board[row][col].setFreezeFrame(false); } tween(board[row][col], { alpha: 1 }, { duration: 300, easing: tween.easeOut }); } // If the cell is empty, fill with a new crystal if (!board[row][col]) { var type = randomCrystalType(); var crystal = spawnCrystal(row, col, type); board[row][col] = crystal; // Start above the board var targetY = crystal.y; crystal.y = BOARD_Y - CRYSTAL_SIZE * 2; tween(crystal, { y: targetY }, { duration: 3000, easing: tween.cubicOut }); } else if (board[row][col].frozen) { // Animate frozen crystal to its own position for effect var targetY = BOARD_Y + row * (CRYSTAL_SIZE + CRYSTAL_SPACING) + CRYSTAL_SIZE / 2; tween(board[row][col], { y: targetY }, { duration: 3000, easing: tween.cubicOut }); // Ensure freeze frame is visible if (typeof board[row][col].setFreezeFrame === "function") { board[row][col].setFreezeFrame(true); } board[row][col].alpha = 0.5; } else if (!(board[row][col].type === "joker" && board[row][col].frozen)) { // For all non-frozen crystals (including non-jokers and non-frozen jokers), replace with a new random crystal var prev = board[row][col]; // If this is a frozen joker, skip replacing it if (prev.type === "joker" && prev.frozen) { // Animate frozen joker to its own position for effect var targetY = BOARD_Y + row * (CRYSTAL_SIZE + CRYSTAL_SPACING) + CRYSTAL_SIZE / 2; tween(prev, { y: targetY }, { duration: 3000, easing: tween.cubicOut }); // Ensure freeze frame is visible if (typeof prev.setFreezeFrame === "function") { prev.setFreezeFrame(true); } prev.alpha = 0.5; continue; } var type = randomCrystalType(); // Remove old crystal prev.destroy(); // Spawn new crystal var crystal = spawnCrystal(row, col, type); board[row][col] = crystal; // Start above the board var targetY = crystal.y; crystal.y = BOARD_Y - CRYSTAL_SIZE * 2; tween(crystal, { y: targetY }, { duration: 3000, easing: tween.cubicOut }); } } } refreshCrystalInteractivity(); LK.setTimeout(function () { startCascadeLoop(); }, 3100); } // Button event shuffleBtn.down = function (x, y, obj) { reshuffleBoardWithFall(); }; // --- Spin Button (visible, below crystals) --- var spinBtnContainer = new Container(); var spinBtnBg = LK.getAsset('spin_btn_shape', { anchorX: 0.5, anchorY: 0.5 }); spinBtnContainer.addChild(spinBtnBg); var spinBtnText = new Text2('Spin', { size: 90, fill: 0xffffff, font: "Arial Black" }); spinBtnText.anchor.set(0.5, 0.5); spinBtnText.x = 0; spinBtnText.y = 0; spinBtnContainer.addChild(spinBtnText); // Position: just below the board, centered horizontally spinBtnContainer.x = 2048 / 2; spinBtnContainer.y = BOARD_Y + BOARD_HEIGHT + 100 + spinBtnBg.height / 2; // Make interactive spinBtnContainer.interactive = true; spinBtnContainer.buttonMode = true; spinBtnContainer.down = function (x, y, obj) { if (isCascading) return; var currentCoins = LK.getScore(); if (currentCoins < 100) { // Not enough coins, flash red for feedback LK.effects.flashObject(spinBtnBg, 0xff0000, 300); return; } // Reset combo multiplier state for new move comboMultiplier = 1; cascadeMoveActive = false; LK.setScore(currentCoins - 100); scoreTxt.setText(LK.getScore()); reshuffleBoardWithFall(); // Optional: flash the button for feedback LK.effects.flashObject(spinBtnBg, 0xffffff, 200); }; // Add to game scene (not GUI, so it stays relative to board) game.addChild(spinBtnContainer); // --- Special Power Buttons --- // State for power selection var powerMode = null; // "bomb", "freeze", "swap" or null // --- Swap Button State --- var swapBtnContainer; var swapBtnBg; var swapBtnText; var swapUsedCount = 0; var swapMaxCount = 5; // Helper to clear power mode UI state function clearPowerMode() { powerMode = null; swapFirstCrystal = null; bombBtnBg.alpha = 1; freezeBtnBg.alpha = 1; if (swapBtnBg) swapBtnBg.alpha = 1; } // --- Bomb Button --- var bombBtnContainer = new Container(); var bombBtnBg = LK.getAsset('spin_btn_shape', { anchorX: 0.5, anchorY: 0.5 }); bombBtnBg.tint = 0xff4444; bombBtnContainer.addChild(bombBtnBg); var bombBtnText = new Text2('Bomb (1)', { size: 70, fill: 0xffffff, font: "Arial Black" }); bombBtnText.anchor.set(0.5, 0.5); bombBtnText.x = 0; bombBtnText.y = 0; bombBtnContainer.addChild(bombBtnText); bombBtnContainer.x = spinBtnContainer.x - 500; bombBtnContainer.y = spinBtnContainer.y; bombBtnContainer.interactive = true; bombBtnContainer.buttonMode = true; bombBtnContainer.down = function () { clearPowerMode(); powerMode = "bomb"; bombBtnBg.alpha = 0.7; }; // --- Freeze Button --- var freezeBtnContainer = new Container(); var freezeBtnBg = LK.getAsset('spin_btn_shape', { anchorX: 0.5, anchorY: 0.5 }); freezeBtnBg.tint = 0x44ffd0; freezeBtnContainer.addChild(freezeBtnBg); var freezeBtnText = new Text2('Freeze (' + (freezeMaxCount - freezeUsedCount) + ')', { size: 70, fill: 0xffffff, font: "Arial Black" }); freezeBtnText.anchor.set(0.5, 0.5); freezeBtnText.x = 0; freezeBtnText.y = 0; freezeBtnContainer.addChild(freezeBtnText); freezeBtnContainer.x = spinBtnContainer.x + 500; freezeBtnContainer.y = spinBtnContainer.y; freezeBtnContainer.interactive = true; freezeBtnContainer.buttonMode = true; // Track freeze usage if (typeof freezeUsedCount === "undefined") { var freezeUsedCount = 0; } var freezeMaxCount = 3; freezeBtnContainer.down = function () { clearPowerMode(); if (freezeUsedCount >= freezeMaxCount) { // Visually disable freeze button if limit reached freezeBtnBg.alpha = 0.3; freezeBtnContainer.interactive = false; freezeBtnContainer.buttonMode = false; return; } powerMode = "freeze"; freezeBtnBg.alpha = 0.7; }; // Add to game game.addChild(bombBtnContainer); game.addChild(freezeBtnContainer); // --- Swap Button --- swapBtnContainer = new Container(); swapBtnBg = LK.getAsset('spin_btn_shape', { anchorX: 0.5, anchorY: 0.5 }); swapBtnBg.tint = 0x4488ff; swapBtnContainer.addChild(swapBtnBg); swapBtnText = new Text2('Swap (' + (swapMaxCount - swapUsedCount) + ')', { size: 70, fill: 0xffffff, font: "Arial Black" }); swapBtnText.anchor.set(0.5, 0.5); swapBtnText.x = 0; swapBtnText.y = 0; swapBtnContainer.addChild(swapBtnText); // Position: just below the spin button, centered horizontally swapBtnContainer.x = spinBtnContainer.x; swapBtnContainer.y = spinBtnContainer.y + spinBtnBg.height + 40; swapBtnContainer.interactive = true; swapBtnContainer.buttonMode = true; swapBtnContainer.down = function () { clearPowerMode(); if (swapUsedCount >= swapMaxCount) { swapBtnBg.alpha = 0.3; swapBtnContainer.interactive = false; swapBtnContainer.buttonMode = false; return; } powerMode = "swap"; swapBtnBg.alpha = 0.7; }; // Add to game game.addChild(swapBtnContainer); // --- Power Button Crystal Selection Logic --- // Track bomb usage if (typeof bombUsed === "undefined") { var bombUsed = false; } // Helper to re-apply crystal interactivity for powers function applyPowerCrystalInteractivity() { for (var row = 0; row < BOARD_ROWS; row++) { if (!board[row]) continue; for (var col = 0; col < BOARD_COLS; col++) { (function (r, c) { if (!board[r]) return; var crystal = board[r][c]; if (!crystal) return; crystal.interactive = true; crystal.buttonMode = true; crystal.down = function () { if (isCascading) return; // Bomb: destroy selected and neighbors, only if not used yet if (powerMode === "bomb" && !bombUsed) { var finishBombPop = function finishBombPop() { clearPowerMode(); LK.setTimeout(function () { startCascadeLoop(); }, 400); }; bombUsed = true; bombBtnText.setText('Bomb (0)'); var targets = [[r, c]]; var neighbors = getNeighbors(r, c); for (var i = 0; i < neighbors.length; i++) { var nr = neighbors[i][0], nc = neighbors[i][1]; if (board[nr][nc]) targets.push([nr, nc]); } var poppedCount = 0; var toPop = []; for (var i = 0; i < targets.length; i++) { var tr = targets[i][0], tc = targets[i][1]; if (board[tr][tc]) { toPop.push([tr, tc]); } } if (toPop.length === 0) { finishBombPop(); return; } var finished = 0; for (var i = 0; i < toPop.length; i++) { var tr = toPop[i][0], tc = toPop[i][1]; if (board[tr][tc]) { board[tr][tc].pop(function () { finished++; if (finished === toPop.length) { // Remove all popped for (var j = 0; j < toPop.length; j++) { var rr = toPop[j][0], cc = toPop[j][1]; if (board[rr][cc]) { board[rr][cc].destroy(); board[rr][cc] = null; } } // After bomb pop, trigger cascadeBoard to fill empty spaces cascadeBoard(function () { finishBombPop(); }); } }); } } // Visually disable bomb button bombBtnBg.alpha = 0.3; bombBtnContainer.interactive = false; bombBtnContainer.buttonMode = false; return; } // Freeze: after clicking Freeze, the clicked crystal stays fixed in place and is not replaced on spin/reshuffle else if (powerMode === "freeze") { var crystal = board[r][c]; if (crystal && !crystal.frozen && freezeUsedCount < freezeMaxCount) { crystal.frozen = true; crystal.justFrozen = true; // Sadece bir tur için işaretle // Visually indicate frozen state tween(crystal, { alpha: 0.5 }, { duration: 300, easing: tween.easeOut }); // Show freeze frame if (typeof crystal.setFreezeFrame === "function") { crystal.setFreezeFrame(true); } freezeUsedCount++; freezeBtnText.setText('Freeze (' + (freezeMaxCount - freezeUsedCount) + ')'); // If reached max, disable freeze button if (freezeUsedCount >= freezeMaxCount) { freezeBtnBg.alpha = 0.3; freezeBtnContainer.interactive = false; freezeBtnContainer.buttonMode = false; } // Always clear selection and power mode after one freeze clearPowerMode(); } } // --- Swap Power Logic --- else if (powerMode === "swap") { var crystal = board[r][c]; if (crystal && !crystal.frozen && swapUsedCount < swapMaxCount) { // Only allow swap on non-frozen crystals var oldType = crystal.type; // Pick a new type different from current var possibleTypes = []; for (var i = 0; i < CRYSTAL_TYPES.length; i++) { var t = CRYSTAL_TYPES[i]; if (t !== oldType) possibleTypes.push(t); } var newType = possibleTypes[Math.floor(Math.random() * possibleTypes.length)]; // Animate pop, then replace crystal.pop(function () { // Remove old crystal from board and scene if (board[r][c] === crystal) { crystal.destroy(); // Spawn new crystal of newType at same position var newCrystal = spawnCrystal(r, c, newType); board[r][c] = newCrystal; newCrystal.x = crystal.x; newCrystal.y = crystal.y; newCrystal.resetVisual(); // Re-apply interactivity refreshCrystalInteractivity(); } }); swapUsedCount++; swapBtnText.setText('Swap (' + (swapMaxCount - swapUsedCount) + ')'); // If reached max, disable swap button if (swapUsedCount >= swapMaxCount) { swapBtnBg.alpha = 0.3; swapBtnContainer.interactive = false; swapBtnContainer.buttonMode = false; } // Always clear selection and power mode after one swap clearPowerMode(); } } }; })(row, col); } } } // Call this after board is filled or refreshed applyPowerCrystalInteractivity(); // --- Patch: When filling board, re-apply crystal interactivity for powers --- function refreshCrystalInteractivity() { for (var row = 0; row < BOARD_ROWS; row++) { if (!board[row]) continue; for (var col = 0; col < BOARD_COLS; col++) { var crystal = board[row][col]; if (!crystal) continue; crystal.interactive = true; crystal.buttonMode = true; crystal.down = function () {}; // Restore freeze frame if frozen if (crystal.type === "joker" && crystal.frozen && typeof crystal.setFreezeFrame === "function") { crystal.setFreezeFrame(true); crystal.alpha = 0.5; } else if (typeof crystal.setFreezeFrame === "function") { crystal.setFreezeFrame(false); crystal.alpha = 1; } } } // Re-apply power crystal interactivity applyPowerCrystalInteractivity(); } // --- Touch to Spin/Reshuffle (tap anywhere else, fallback for desktop) --- // Removed global tap-to-reshuffle so only Spin button restarts the game // --- Game Over/Win (not used, slot is endless) --- // --- Game Update (not needed, all logic is event/cascade driven) --- game.update = function () { // Update bugs (background animation) for (var i = 0; i < bugs.length; i++) { if (bugs[i] && typeof bugs[i].update === "function") { bugs[i].update(); } } }; // --- Start the game --- startGame(); ;
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Passive Bug class (background animation)
var Bug = Container.expand(function () {
var self = Container.call(this);
// Pick a random bug type
var bugTypes = [{
id: 'bocek_orumcek',
name: 'örümcek'
}, {
id: 'bocek_kirkayak',
name: 'kırkayak'
}, {
id: 'bocek_karafatma',
name: 'karafatma'
}, {
id: 'bocek_hamamb',
name: 'hamamböceği'
}];
var bugType = bugTypes[Math.floor(Math.random() * bugTypes.length)];
self.bugType = bugType.name;
// Attach bug asset
self.asset = self.attachAsset(bugType.id, {
anchorX: 0.5,
anchorY: 0.5
});
// State
self.active = false;
self.targetX = 0;
self.targetY = 0;
self.speed = 2 + Math.random() * 2; // px per frame
self.hideTimeout = null;
self.showTimeout = null;
// Helper: set random position (offscreen or edge)
self.setRandomStart = function () {
// Appear from a random edge
var edge = Math.floor(Math.random() * 4); // 0: left, 1: right, 2: top, 3: bottom
var margin = 100;
if (edge === 0) {
// left
self.x = -margin;
self.y = margin + Math.random() * (2732 - 2 * margin);
} else if (edge === 1) {
// right
self.x = 2048 + margin;
self.y = margin + Math.random() * (2732 - 2 * margin);
} else if (edge === 2) {
// top
self.x = margin + Math.random() * (2048 - 2 * margin);
self.y = -margin;
} else {
// bottom
self.x = margin + Math.random() * (2048 - 2 * margin);
self.y = 2732 + margin;
}
};
// Helper: set random target inside screen
self.setRandomTarget = function () {
var margin = 200;
self.targetX = margin + Math.random() * (2048 - 2 * margin);
self.targetY = margin + Math.random() * (2732 - 2 * margin);
};
// Show bug: appear and start moving
self.showBug = function () {
self.setRandomStart();
self.setRandomTarget();
self.visible = true;
self.active = true;
self.alpha = 0.7 + Math.random() * 0.3;
self.scaleX = self.scaleY = 0.7 + Math.random() * 0.5;
// Set random speed
self.speed = 2 + Math.random() * 2;
// Set random zIndex so bugs can overlap
self.zIndex = 0;
// Set random rotation
self.rotation = Math.random() * Math.PI * 2;
// Set how long bug will stay visible
var minShow = 2000,
maxShow = 5000;
if (self.hideTimeout) LK.clearTimeout(self.hideTimeout);
self.hideTimeout = LK.setTimeout(function () {
self.hideBug();
}, minShow + Math.random() * (maxShow - minShow));
};
// Hide bug: disappear and schedule next appearance
self.hideBug = function () {
self.visible = false;
self.active = false;
// Schedule next appearance
var minDelay = 1500,
maxDelay = 4000;
if (self.showTimeout) LK.clearTimeout(self.showTimeout);
self.showTimeout = LK.setTimeout(function () {
self.showBug();
}, minDelay + Math.random() * (maxDelay - minDelay));
};
// Per-frame update
self.update = function () {
if (!self.active) return;
// --- Jitter: bugs shake a little as they move ---
// Add a small random offset to position for jitter
var jitterAmount = 2 + Math.random() * 2;
var jitterAngle = Math.random() * Math.PI * 2;
var jitterX = Math.cos(jitterAngle) * jitterAmount;
var jitterY = Math.sin(jitterAngle) * jitterAmount;
// --- Sudden direction change: sometimes pick a new target suddenly ---
if (typeof self._framesToNextTurn === "undefined") {
self._framesToNextTurn = 30 + Math.floor(Math.random() * 60);
}
self._framesToNextTurn--;
if (self._framesToNextTurn <= 0) {
// 40% chance to pick a new random target (simulate sudden turn)
if (Math.random() < 0.4) {
self.setRandomTarget();
// Also randomize speed and rotation for realism
self.speed = 2 + Math.random() * 2;
self.rotation = Math.random() * Math.PI * 2;
}
self._framesToNextTurn = 30 + Math.floor(Math.random() * 60);
}
// Move towards target
var dx = self.targetX - self.x;
var dy = self.targetY - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < self.speed) {
// Arrived at target, pick a new target
self.setRandomTarget();
} else {
// Move with a little jitter
self.x += dx / dist * self.speed + jitterX;
self.y += dy / dist * self.speed + jitterY;
// Optionally, face the direction of movement
self.rotation = Math.atan2(dy, dx) + (Math.random() - 0.5) * 0.2;
}
};
// Start hidden, then schedule first appearance
self.visible = false;
self.active = false;
self.setRandomStart();
self.setRandomTarget();
// Schedule first appearance
self.showTimeout = LK.setTimeout(function () {
self.showBug();
}, 1000 + Math.random() * 2000);
return self;
});
// Crystal class
var Crystal = Container.expand(function () {
var self = Container.call(this);
// type: 'blue', 'yellow', 'red', 'purple', 'white', 'joker'
self.type = 'blue';
self.row = 0;
self.col = 0;
self.popping = false;
// Attach correct asset
function setAsset() {
if (self.asset) self.removeChild(self.asset);
var assetId = 'crystal_' + self.type;
self.asset = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
}
self.setType = function (type) {
self.type = type;
setAsset();
};
// Animate pop
self.pop = function (_onFinish) {
if (self.popping) return;
self.popping = true;
tween(self, {
scaleX: 1.4,
scaleY: 1.4,
alpha: 0
}, {
duration: 350,
easing: tween.easeOut,
onFinish: function onFinish() {
if (_onFinish) _onFinish();
}
});
LK.getSound('crystal_pop').play();
};
// Reset visual state
self.resetVisual = function () {
self.scaleX = 1;
self.scaleY = 1;
self.alpha = 1;
self.popping = false;
// Remove freeze frame if present
if (self.freezeFrame) {
self.removeChild(self.freezeFrame);
self.freezeFrame = null;
}
};
// Add or remove freeze frame
self.setFreezeFrame = function (show) {
if (show) {
if (!self.freezeFrame) {
// Create a square frame using a box shape, slightly larger than the crystal
var frame = LK.getAsset('spin_btn_shape', {
anchorX: 0.5,
anchorY: 0.5
});
frame.width = self.asset.width * 1.18;
frame.height = self.asset.height * 1.18;
frame.alpha = 0.7;
frame.tint = 0x00e6b8;
frame.zIndex = -1;
self.addChildAt(frame, 0);
self.freezeFrame = frame;
}
if (self.freezeFrame) self.freezeFrame.visible = true;
} else {
if (self.freezeFrame) {
self.removeChild(self.freezeFrame);
self.freezeFrame = null;
}
}
};
setAsset();
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x181c2c
});
/****
* Game Code
****/
// Böcek görselleri (örümcek, kırkayak, karafatma, hamamböceği)
// Add background image behind all elements
var backgroundImage = LK.getAsset('background_main', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2732 / 2
});
game.addChildAt(backgroundImage, 0); // Always at the back
// --- Passive Bugs (background animation) ---
var bugs = [];
// Ensure each bug type is present and moving
var bugTypeIds = ['bocek_orumcek', 'bocek_kirkayak', 'bocek_karafatma', 'bocek_hamamb'];
for (var i = 0; i < bugTypeIds.length; i++) {
var bug = new Bug();
// Force the bug to use a specific type for each instance
var bugTypes = [{
id: 'bocek_orumcek',
name: 'örümcek'
}, {
id: 'bocek_kirkayak',
name: 'kırkayak'
}, {
id: 'bocek_karafatma',
name: 'karafatma'
}, {
id: 'bocek_hamamb',
name: 'hamamböceği'
}];
// Set bug type and asset
bug.bugType = bugTypes[i].name;
if (bug.asset) bug.removeChild(bug.asset);
bug.asset = bug.attachAsset(bugTypeIds[i], {
anchorX: 0.5,
anchorY: 0.5
});
// Place bugs behind all game elements but above background
game.addChildAt(bug, 1);
bugs.push(bug);
}
// --- Game Constants ---
// Crystal shapes (box for simplicity, different colors for each type)
// Sound effects (placeholders, actual sound assets will be loaded by LK)
var BOARD_COLS = 4;
var BOARD_ROWS = 6;
var CRYSTAL_SIZE = 300; // px
var CRYSTAL_SPACING = 24; // px
var BOARD_WIDTH = BOARD_COLS * CRYSTAL_SIZE + (BOARD_COLS - 1) * CRYSTAL_SPACING;
var BOARD_HEIGHT = BOARD_ROWS * CRYSTAL_SIZE + (BOARD_ROWS - 1) * CRYSTAL_SPACING;
var BOARD_X = (2048 - BOARD_WIDTH) / 2;
var BOARD_Y = (2732 - BOARD_HEIGHT) / 2 - 180; // move board up to make space for spin button
var CRYSTAL_TYPES = ['blue', 'yellow', 'red', 'purple', 'white', 'joker'];
var CRYSTAL_COLORS = {
blue: 0x3a8ee6,
yellow: 0xf7e14a,
red: 0xe64a3a,
purple: 0xb44ae6,
white: 0xffffff,
joker: 0x00e6b8
};
// Score values per crystal type
var CRYSTAL_SCORES = {
blue: 10,
yellow: 12,
red: 14,
purple: 16,
white: 18,
joker: 25
};
// --- Game State ---
var board = []; // 2D array [row][col] of Crystal or null
var poppingCrystals = []; // Crystals currently popping
var isCascading = false;
var scoreTxt;
var cascadeTimeout = null;
// --- Level and Joker Probability State ---
var level = 1;
var jokerProbability = 0.10; // Start at 10%
// --- UI: Score ---
scoreTxt = new Text2('0', {
size: 120,
fill: 0xFFFFFF
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// --- Board Initialization ---
function createEmptyBoard() {
board = [];
for (var row = 0; row < BOARD_ROWS; row++) {
var rowArr = [];
for (var col = 0; col < BOARD_COLS; col++) {
rowArr.push(null);
}
board.push(rowArr);
}
}
function randomCrystalType() {
// Use current jokerProbability (decreases as level increases)
var r = Math.random();
if (r < jokerProbability) return 'joker';
var idx = Math.floor(Math.random() * 5);
return CRYSTAL_TYPES[idx];
}
function spawnCrystal(row, col, type) {
var crystal = new Crystal();
crystal.setType(type);
crystal.row = row;
crystal.col = col;
// Position
var x = BOARD_X + col * (CRYSTAL_SIZE + CRYSTAL_SPACING) + CRYSTAL_SIZE / 2;
var y = BOARD_Y + row * (CRYSTAL_SIZE + CRYSTAL_SPACING) + CRYSTAL_SIZE / 2;
crystal.x = x;
crystal.y = y;
crystal.resetVisual();
game.addChild(crystal);
// Make crystal fixed: prevent any movement or change
// If this crystal is frozen, make sure it cannot be replaced or destroyed
if (crystal.frozen) {
crystal.interactive = false;
crystal.buttonMode = false;
crystal.down = function () {};
crystal.pop = function () {};
crystal.setType = function (type) {};
crystal.resetVisual = function () {};
crystal.setFreezeFrame = function () {};
}
return crystal;
}
function fillBoardRandom() {
for (var row = 0; row < BOARD_ROWS; row++) {
for (var col = 0; col < BOARD_COLS; col++) {
var type = randomCrystalType();
var crystal = spawnCrystal(row, col, type);
board[row][col] = crystal;
}
}
refreshCrystalInteractivity();
}
// --- Board Utility ---
function getNeighbors(row, col) {
// 4-directional neighbors
var n = [];
if (row > 0) n.push([row - 1, col]);
if (row < BOARD_ROWS - 1) n.push([row + 1, col]);
if (col > 0) n.push([row, col - 1]);
if (col < BOARD_COLS - 1) n.push([row, col + 1]);
return n;
}
// --- Find Groups for Popping ---
function findPopGroups() {
// Returns: [{type, crystals: [Crystal, ...], jokerCount: n}]
var groups = [];
// Count all crystals by type (excluding jokers)
var typeCounts = {};
var typeCrystals = {};
var jokerCrystals = [];
for (var i = 0; i < CRYSTAL_TYPES.length; i++) {
var t = CRYSTAL_TYPES[i];
if (t !== 'joker') {
typeCounts[t] = 0;
typeCrystals[t] = [];
}
}
for (var row = 0; row < BOARD_ROWS; row++) {
for (var col = 0; col < BOARD_COLS; col++) {
var crystal = board[row][col];
if (!crystal) continue;
if (crystal.type === 'joker') {
// Frozen jokers can pop if they are part of a valid pop group, so always count them
jokerCrystals.push(crystal);
} else {
// Frozen crystals can pop if they are part of a valid pop group, so always count them
typeCounts[crystal.type]++;
typeCrystals[crystal.type].push(crystal);
}
}
}
// For each color, see if color+joker >= 10, if so, pop all of that color and enough jokers
for (var t in typeCounts) {
var total = typeCounts[t] + jokerCrystals.length;
if (total >= 10) {
// Add all of this color
var groupCrystals = [];
for (var i = 0; i < typeCrystals[t].length; i++) {
groupCrystals.push(typeCrystals[t][i]);
}
// Add jokers
for (var j = 0; j < jokerCrystals.length; j++) {
groupCrystals.push(jokerCrystals[j]);
}
groups.push({
type: t,
crystals: groupCrystals,
jokerCount: jokerCrystals.length
});
}
}
return groups;
}
// --- Pop Groups ---
function popGroups(groups, onFinish) {
if (groups.length === 0) {
if (onFinish) onFinish();
return;
}
isCascading = true;
poppingCrystals = [];
var popped = {};
// --- Combo Multiplier Logic ---
if (typeof comboMultiplier === "undefined") {
comboMultiplier = 1;
}
if (typeof lastCascadeMoveId === "undefined") {
lastCascadeMoveId = 0;
}
if (typeof currentCascadeMoveId === "undefined") {
currentCascadeMoveId = 0;
}
// If this is a new cascade move, reset comboMultiplier
if (typeof cascadeMoveActive === "undefined" || !cascadeMoveActive) {
comboMultiplier = 1;
currentCascadeMoveId = Date.now();
lastCascadeMoveId = currentCascadeMoveId;
cascadeMoveActive = true;
} else {
// If still in the same move, increment comboMultiplier
if (currentCascadeMoveId === lastCascadeMoveId) {
comboMultiplier++;
} else {
comboMultiplier = 1;
currentCascadeMoveId = Date.now();
lastCascadeMoveId = currentCascadeMoveId;
}
}
// Mark all crystals to pop (avoid double pop)
for (var g = 0; g < groups.length; g++) {
var group = groups[g];
for (var i = 0; i < group.crystals.length; i++) {
var c = group.crystals[i];
var key = c.row + ',' + c.col;
if (!popped[key]) {
popped[key] = true;
poppingCrystals.push(c);
}
}
}
// Animate pop with 2s shake before popping
var popCount = poppingCrystals.length;
var finished = 0;
for (var i = 0; i < poppingCrystals.length; i++) {
var c = poppingCrystals[i];
(function (crystal) {
// Save original position
var origX = crystal.x;
var origY = crystal.y;
var shakeDuration = 2000;
var shakeInterval = 30;
var elapsed = 0;
var shakeTimer = null;
function startShake() {
shakeTimer = LK.setInterval(function () {
// Random shake offset, smaller amplitude for subtle effect
var dx = (Math.random() - 0.5) * 24;
var dy = (Math.random() - 0.5) * 24;
crystal.x = origX + dx;
crystal.y = origY + dy;
elapsed += shakeInterval;
if (elapsed >= shakeDuration) {
// End shake, restore position
LK.clearInterval(shakeTimer);
crystal.x = origX;
crystal.y = origY;
// Now pop
crystal.pop(function () {
finished++;
if (finished === popCount) {
// Remove from board
for (var j = 0; j < poppingCrystals.length; j++) {
var pc = poppingCrystals[j];
if (board[pc.row][pc.col] === pc) {
board[pc.row][pc.col] = null;
pc.destroy();
}
}
poppingCrystals = [];
if (onFinish) onFinish();
}
});
}
}, shakeInterval);
}
startShake();
})(c);
}
// Score with combo multiplier
for (var g = 0; g < groups.length; g++) {
var group = groups[g];
var baseScore = 0;
for (var i = 0; i < group.crystals.length; i++) {
var c = group.crystals[i];
var t = c.type === 'joker' ? group.type : c.type;
baseScore += CRYSTAL_SCORES[t];
}
LK.setScore(LK.getScore() + baseScore * comboMultiplier);
}
scoreTxt.setText(LK.getScore());
LK.getSound('cascade').play();
LK.effects.flashObject(scoreTxt, 0xffff00, 400);
// --- Level Up Logic ---
var newLevel = Math.floor(LK.getScore() / 1000) + 1;
if (newLevel > level) {
level = newLevel;
// Decrease jokerProbability by 1% per level up, but not below 1%
jokerProbability = Math.max(0.01, 0.10 - (level - 1) * 0.01);
// Optional: flash score text or give feedback for level up
LK.effects.flashObject(scoreTxt, 0x00ff00, 600);
}
}
// --- Cascade: Drop Crystals Down and Fill ---
function cascadeBoard(onFinish) {
// For each column, drop crystals down to fill empty spaces
var changed = false;
for (var col = 0; col < BOARD_COLS; col++) {
var emptyRows = [];
for (var row = BOARD_ROWS - 1; row >= 0; row--) {
if (!board[row][col]) {
emptyRows.push(row);
} else if (emptyRows.length > 0) {
// Move crystal down
var targetRow = emptyRows.shift();
var crystal = board[row][col];
board[targetRow][col] = crystal;
board[row][col] = null;
crystal.row = targetRow;
crystal.col = col;
// Animate drop
var newY = BOARD_Y + targetRow * (CRYSTAL_SIZE + CRYSTAL_SPACING) + CRYSTAL_SIZE / 2;
tween(crystal, {
y: newY
}, {
duration: 220,
easing: tween.cubicOut
});
emptyRows.push(row);
changed = true;
}
}
}
// Fill empty spaces at the top
for (var col = 0; col < BOARD_COLS; col++) {
for (var row = 0; row < BOARD_ROWS; row++) {
if (!board[row][col]) {
var type = randomCrystalType();
var crystal = spawnCrystal(row, col, type);
board[row][col] = crystal;
// Drop from above
var startY = BOARD_Y - CRYSTAL_SIZE;
crystal.y = startY;
var targetY = BOARD_Y + row * (CRYSTAL_SIZE + CRYSTAL_SPACING) + CRYSTAL_SIZE / 2;
tween(crystal, {
y: targetY
}, {
duration: 220,
easing: tween.cubicOut
});
changed = true;
}
}
}
// Wait for animation, then call onFinish
LK.setTimeout(function () {
refreshCrystalInteractivity();
if (onFinish) onFinish();
}, 250);
}
// --- Main Game Loop: Cascade, Pop, Repeat ---
function startCascadeLoop() {
if (cascadeTimeout) {
LK.clearTimeout(cascadeTimeout);
cascadeTimeout = null;
}
var groups = findPopGroups();
if (groups.length === 0) {
isCascading = false;
return;
}
popGroups(groups, function () {
cascadeBoard(function () {
// Chain reaction!
cascadeTimeout = LK.setTimeout(function () {
startCascadeLoop();
}, 120);
});
});
}
// --- Start New Game ---
function startGame() {
LK.setScore(2000);
scoreTxt.setText('2000');
// Reset combo multiplier state for new game
comboMultiplier = 1;
cascadeMoveActive = false;
// Reset bomb usage for new game
bombUsed = false;
// Reset swap usage for new game
swapUsedCount = 0;
if (swapBtnText) swapBtnText.setText('Swap (' + (swapMaxCount - swapUsedCount) + ')');
if (swapBtnBg) swapBtnBg.alpha = 1;
if (swapBtnContainer) {
swapBtnContainer.interactive = true;
swapBtnContainer.buttonMode = true;
}
// Reset freeze usage for new game
freezeUsedCount = 0;
freezeBtnText.setText('Freeze (' + (freezeMaxCount - freezeUsedCount) + ')');
freezeBtnBg.alpha = 1;
freezeBtnContainer.interactive = true;
freezeBtnContainer.buttonMode = true;
// Reset level and jokerProbability
level = 1;
jokerProbability = 0.10;
// Remove all crystals
for (var row = 0; row < BOARD_ROWS; row++) {
for (var col = 0; col < BOARD_COLS; col++) {
if (board[row] && board[row][col]) {
board[row][col].frozen = false;
board[row][col].alpha = 1;
// Remove freeze frame if present
if (typeof board[row][col].setFreezeFrame === "function") {
board[row][col].setFreezeFrame(false);
}
board[row][col].destroy();
}
}
}
createEmptyBoard();
fillBoardRandom();
refreshCrystalInteractivity();
LK.setTimeout(function () {
startCascadeLoop();
}, 400);
}
// --- Karıştır Button ---
var shuffleBtn = new Text2('Karıştır', {
size: 110,
fill: 0x00E6B8,
font: "Arial Black"
});
shuffleBtn.anchor.set(0.5, 1);
shuffleBtn.x = LK.gui.width / 2;
shuffleBtn.y = LK.gui.height - 60 - 140; // Move up to make space for spin button
shuffleBtn.interactive = true;
shuffleBtn.buttonMode = true;
LK.gui.bottom.addChild(shuffleBtn);
function reshuffleBoardWithFall() {
if (isCascading) return;
// Reset combo multiplier state for new move
comboMultiplier = 1;
cascadeMoveActive = false;
// Remove all crystals
for (var row = 0; row < BOARD_ROWS; row++) {
for (var col = 0; col < BOARD_COLS; col++) {
if (board[row][col]) {
// Only destroy if not a frozen crystal
if (!board[row][col].frozen) {
board[row][col].frozen = false;
board[row][col].alpha = 1;
// Remove freeze frame if present
if (typeof board[row][col].setFreezeFrame === "function") {
board[row][col].setFreezeFrame(false);
}
board[row][col].destroy();
board[row][col] = null;
}
}
}
}
// Do not recreate the board, just refill non-frozen spots
// Fill board with new crystals, but spawn them above and animate falling
for (var row = 0; row < BOARD_ROWS; row++) {
for (var col = 0; col < BOARD_COLS; col++) {
// Eğer kristal donmuş ve justFrozen ise, bu turda silme, sadece işaretle (removeNextTurn)
if (board[row][col] && board[row][col].frozen && board[row][col].justFrozen) {
// Bir tur bekle, bu turda sadece işaretle
board[row][col].justFrozen = false;
board[row][col].removeNextTurn = true;
if (typeof board[row][col].setFreezeFrame === "function") {
board[row][col].setFreezeFrame(false);
}
tween(board[row][col], {
alpha: 1
}, {
duration: 300,
easing: tween.easeOut
});
}
// Eğer kristal donmuş ve removeNextTurn işaretli ise, bu turda sil
else if (board[row][col] && board[row][col].frozen && board[row][col].removeNextTurn) {
board[row][col].frozen = false;
board[row][col].removeNextTurn = false;
if (typeof board[row][col].setFreezeFrame === "function") {
board[row][col].setFreezeFrame(false);
}
// Kristali sil
board[row][col].destroy();
board[row][col] = null;
}
// Eğer kristal donmuş ve justFrozen değilse, donmayı kaldır (yani ikinci turda donma kalksın)
else if (board[row][col] && board[row][col].frozen && !board[row][col].justFrozen) {
board[row][col].frozen = false;
if (typeof board[row][col].setFreezeFrame === "function") {
board[row][col].setFreezeFrame(false);
}
tween(board[row][col], {
alpha: 1
}, {
duration: 300,
easing: tween.easeOut
});
}
// If the cell is empty, fill with a new crystal
if (!board[row][col]) {
var type = randomCrystalType();
var crystal = spawnCrystal(row, col, type);
board[row][col] = crystal;
// Start above the board
var targetY = crystal.y;
crystal.y = BOARD_Y - CRYSTAL_SIZE * 2;
tween(crystal, {
y: targetY
}, {
duration: 3000,
easing: tween.cubicOut
});
} else if (board[row][col].frozen) {
// Animate frozen crystal to its own position for effect
var targetY = BOARD_Y + row * (CRYSTAL_SIZE + CRYSTAL_SPACING) + CRYSTAL_SIZE / 2;
tween(board[row][col], {
y: targetY
}, {
duration: 3000,
easing: tween.cubicOut
});
// Ensure freeze frame is visible
if (typeof board[row][col].setFreezeFrame === "function") {
board[row][col].setFreezeFrame(true);
}
board[row][col].alpha = 0.5;
} else if (!(board[row][col].type === "joker" && board[row][col].frozen)) {
// For all non-frozen crystals (including non-jokers and non-frozen jokers), replace with a new random crystal
var prev = board[row][col];
// If this is a frozen joker, skip replacing it
if (prev.type === "joker" && prev.frozen) {
// Animate frozen joker to its own position for effect
var targetY = BOARD_Y + row * (CRYSTAL_SIZE + CRYSTAL_SPACING) + CRYSTAL_SIZE / 2;
tween(prev, {
y: targetY
}, {
duration: 3000,
easing: tween.cubicOut
});
// Ensure freeze frame is visible
if (typeof prev.setFreezeFrame === "function") {
prev.setFreezeFrame(true);
}
prev.alpha = 0.5;
continue;
}
var type = randomCrystalType();
// Remove old crystal
prev.destroy();
// Spawn new crystal
var crystal = spawnCrystal(row, col, type);
board[row][col] = crystal;
// Start above the board
var targetY = crystal.y;
crystal.y = BOARD_Y - CRYSTAL_SIZE * 2;
tween(crystal, {
y: targetY
}, {
duration: 3000,
easing: tween.cubicOut
});
}
}
}
refreshCrystalInteractivity();
LK.setTimeout(function () {
startCascadeLoop();
}, 3100);
}
// Button event
shuffleBtn.down = function (x, y, obj) {
reshuffleBoardWithFall();
};
// --- Spin Button (visible, below crystals) ---
var spinBtnContainer = new Container();
var spinBtnBg = LK.getAsset('spin_btn_shape', {
anchorX: 0.5,
anchorY: 0.5
});
spinBtnContainer.addChild(spinBtnBg);
var spinBtnText = new Text2('Spin', {
size: 90,
fill: 0xffffff,
font: "Arial Black"
});
spinBtnText.anchor.set(0.5, 0.5);
spinBtnText.x = 0;
spinBtnText.y = 0;
spinBtnContainer.addChild(spinBtnText);
// Position: just below the board, centered horizontally
spinBtnContainer.x = 2048 / 2;
spinBtnContainer.y = BOARD_Y + BOARD_HEIGHT + 100 + spinBtnBg.height / 2;
// Make interactive
spinBtnContainer.interactive = true;
spinBtnContainer.buttonMode = true;
spinBtnContainer.down = function (x, y, obj) {
if (isCascading) return;
var currentCoins = LK.getScore();
if (currentCoins < 100) {
// Not enough coins, flash red for feedback
LK.effects.flashObject(spinBtnBg, 0xff0000, 300);
return;
}
// Reset combo multiplier state for new move
comboMultiplier = 1;
cascadeMoveActive = false;
LK.setScore(currentCoins - 100);
scoreTxt.setText(LK.getScore());
reshuffleBoardWithFall();
// Optional: flash the button for feedback
LK.effects.flashObject(spinBtnBg, 0xffffff, 200);
};
// Add to game scene (not GUI, so it stays relative to board)
game.addChild(spinBtnContainer);
// --- Special Power Buttons ---
// State for power selection
var powerMode = null; // "bomb", "freeze", "swap" or null
// --- Swap Button State ---
var swapBtnContainer;
var swapBtnBg;
var swapBtnText;
var swapUsedCount = 0;
var swapMaxCount = 5;
// Helper to clear power mode UI state
function clearPowerMode() {
powerMode = null;
swapFirstCrystal = null;
bombBtnBg.alpha = 1;
freezeBtnBg.alpha = 1;
if (swapBtnBg) swapBtnBg.alpha = 1;
}
// --- Bomb Button ---
var bombBtnContainer = new Container();
var bombBtnBg = LK.getAsset('spin_btn_shape', {
anchorX: 0.5,
anchorY: 0.5
});
bombBtnBg.tint = 0xff4444;
bombBtnContainer.addChild(bombBtnBg);
var bombBtnText = new Text2('Bomb (1)', {
size: 70,
fill: 0xffffff,
font: "Arial Black"
});
bombBtnText.anchor.set(0.5, 0.5);
bombBtnText.x = 0;
bombBtnText.y = 0;
bombBtnContainer.addChild(bombBtnText);
bombBtnContainer.x = spinBtnContainer.x - 500;
bombBtnContainer.y = spinBtnContainer.y;
bombBtnContainer.interactive = true;
bombBtnContainer.buttonMode = true;
bombBtnContainer.down = function () {
clearPowerMode();
powerMode = "bomb";
bombBtnBg.alpha = 0.7;
};
// --- Freeze Button ---
var freezeBtnContainer = new Container();
var freezeBtnBg = LK.getAsset('spin_btn_shape', {
anchorX: 0.5,
anchorY: 0.5
});
freezeBtnBg.tint = 0x44ffd0;
freezeBtnContainer.addChild(freezeBtnBg);
var freezeBtnText = new Text2('Freeze (' + (freezeMaxCount - freezeUsedCount) + ')', {
size: 70,
fill: 0xffffff,
font: "Arial Black"
});
freezeBtnText.anchor.set(0.5, 0.5);
freezeBtnText.x = 0;
freezeBtnText.y = 0;
freezeBtnContainer.addChild(freezeBtnText);
freezeBtnContainer.x = spinBtnContainer.x + 500;
freezeBtnContainer.y = spinBtnContainer.y;
freezeBtnContainer.interactive = true;
freezeBtnContainer.buttonMode = true;
// Track freeze usage
if (typeof freezeUsedCount === "undefined") {
var freezeUsedCount = 0;
}
var freezeMaxCount = 3;
freezeBtnContainer.down = function () {
clearPowerMode();
if (freezeUsedCount >= freezeMaxCount) {
// Visually disable freeze button if limit reached
freezeBtnBg.alpha = 0.3;
freezeBtnContainer.interactive = false;
freezeBtnContainer.buttonMode = false;
return;
}
powerMode = "freeze";
freezeBtnBg.alpha = 0.7;
};
// Add to game
game.addChild(bombBtnContainer);
game.addChild(freezeBtnContainer);
// --- Swap Button ---
swapBtnContainer = new Container();
swapBtnBg = LK.getAsset('spin_btn_shape', {
anchorX: 0.5,
anchorY: 0.5
});
swapBtnBg.tint = 0x4488ff;
swapBtnContainer.addChild(swapBtnBg);
swapBtnText = new Text2('Swap (' + (swapMaxCount - swapUsedCount) + ')', {
size: 70,
fill: 0xffffff,
font: "Arial Black"
});
swapBtnText.anchor.set(0.5, 0.5);
swapBtnText.x = 0;
swapBtnText.y = 0;
swapBtnContainer.addChild(swapBtnText);
// Position: just below the spin button, centered horizontally
swapBtnContainer.x = spinBtnContainer.x;
swapBtnContainer.y = spinBtnContainer.y + spinBtnBg.height + 40;
swapBtnContainer.interactive = true;
swapBtnContainer.buttonMode = true;
swapBtnContainer.down = function () {
clearPowerMode();
if (swapUsedCount >= swapMaxCount) {
swapBtnBg.alpha = 0.3;
swapBtnContainer.interactive = false;
swapBtnContainer.buttonMode = false;
return;
}
powerMode = "swap";
swapBtnBg.alpha = 0.7;
};
// Add to game
game.addChild(swapBtnContainer);
// --- Power Button Crystal Selection Logic ---
// Track bomb usage
if (typeof bombUsed === "undefined") {
var bombUsed = false;
}
// Helper to re-apply crystal interactivity for powers
function applyPowerCrystalInteractivity() {
for (var row = 0; row < BOARD_ROWS; row++) {
if (!board[row]) continue;
for (var col = 0; col < BOARD_COLS; col++) {
(function (r, c) {
if (!board[r]) return;
var crystal = board[r][c];
if (!crystal) return;
crystal.interactive = true;
crystal.buttonMode = true;
crystal.down = function () {
if (isCascading) return;
// Bomb: destroy selected and neighbors, only if not used yet
if (powerMode === "bomb" && !bombUsed) {
var finishBombPop = function finishBombPop() {
clearPowerMode();
LK.setTimeout(function () {
startCascadeLoop();
}, 400);
};
bombUsed = true;
bombBtnText.setText('Bomb (0)');
var targets = [[r, c]];
var neighbors = getNeighbors(r, c);
for (var i = 0; i < neighbors.length; i++) {
var nr = neighbors[i][0],
nc = neighbors[i][1];
if (board[nr][nc]) targets.push([nr, nc]);
}
var poppedCount = 0;
var toPop = [];
for (var i = 0; i < targets.length; i++) {
var tr = targets[i][0],
tc = targets[i][1];
if (board[tr][tc]) {
toPop.push([tr, tc]);
}
}
if (toPop.length === 0) {
finishBombPop();
return;
}
var finished = 0;
for (var i = 0; i < toPop.length; i++) {
var tr = toPop[i][0],
tc = toPop[i][1];
if (board[tr][tc]) {
board[tr][tc].pop(function () {
finished++;
if (finished === toPop.length) {
// Remove all popped
for (var j = 0; j < toPop.length; j++) {
var rr = toPop[j][0],
cc = toPop[j][1];
if (board[rr][cc]) {
board[rr][cc].destroy();
board[rr][cc] = null;
}
}
// After bomb pop, trigger cascadeBoard to fill empty spaces
cascadeBoard(function () {
finishBombPop();
});
}
});
}
}
// Visually disable bomb button
bombBtnBg.alpha = 0.3;
bombBtnContainer.interactive = false;
bombBtnContainer.buttonMode = false;
return;
}
// Freeze: after clicking Freeze, the clicked crystal stays fixed in place and is not replaced on spin/reshuffle
else if (powerMode === "freeze") {
var crystal = board[r][c];
if (crystal && !crystal.frozen && freezeUsedCount < freezeMaxCount) {
crystal.frozen = true;
crystal.justFrozen = true; // Sadece bir tur için işaretle
// Visually indicate frozen state
tween(crystal, {
alpha: 0.5
}, {
duration: 300,
easing: tween.easeOut
});
// Show freeze frame
if (typeof crystal.setFreezeFrame === "function") {
crystal.setFreezeFrame(true);
}
freezeUsedCount++;
freezeBtnText.setText('Freeze (' + (freezeMaxCount - freezeUsedCount) + ')');
// If reached max, disable freeze button
if (freezeUsedCount >= freezeMaxCount) {
freezeBtnBg.alpha = 0.3;
freezeBtnContainer.interactive = false;
freezeBtnContainer.buttonMode = false;
}
// Always clear selection and power mode after one freeze
clearPowerMode();
}
}
// --- Swap Power Logic ---
else if (powerMode === "swap") {
var crystal = board[r][c];
if (crystal && !crystal.frozen && swapUsedCount < swapMaxCount) {
// Only allow swap on non-frozen crystals
var oldType = crystal.type;
// Pick a new type different from current
var possibleTypes = [];
for (var i = 0; i < CRYSTAL_TYPES.length; i++) {
var t = CRYSTAL_TYPES[i];
if (t !== oldType) possibleTypes.push(t);
}
var newType = possibleTypes[Math.floor(Math.random() * possibleTypes.length)];
// Animate pop, then replace
crystal.pop(function () {
// Remove old crystal from board and scene
if (board[r][c] === crystal) {
crystal.destroy();
// Spawn new crystal of newType at same position
var newCrystal = spawnCrystal(r, c, newType);
board[r][c] = newCrystal;
newCrystal.x = crystal.x;
newCrystal.y = crystal.y;
newCrystal.resetVisual();
// Re-apply interactivity
refreshCrystalInteractivity();
}
});
swapUsedCount++;
swapBtnText.setText('Swap (' + (swapMaxCount - swapUsedCount) + ')');
// If reached max, disable swap button
if (swapUsedCount >= swapMaxCount) {
swapBtnBg.alpha = 0.3;
swapBtnContainer.interactive = false;
swapBtnContainer.buttonMode = false;
}
// Always clear selection and power mode after one swap
clearPowerMode();
}
}
};
})(row, col);
}
}
}
// Call this after board is filled or refreshed
applyPowerCrystalInteractivity();
// --- Patch: When filling board, re-apply crystal interactivity for powers ---
function refreshCrystalInteractivity() {
for (var row = 0; row < BOARD_ROWS; row++) {
if (!board[row]) continue;
for (var col = 0; col < BOARD_COLS; col++) {
var crystal = board[row][col];
if (!crystal) continue;
crystal.interactive = true;
crystal.buttonMode = true;
crystal.down = function () {};
// Restore freeze frame if frozen
if (crystal.type === "joker" && crystal.frozen && typeof crystal.setFreezeFrame === "function") {
crystal.setFreezeFrame(true);
crystal.alpha = 0.5;
} else if (typeof crystal.setFreezeFrame === "function") {
crystal.setFreezeFrame(false);
crystal.alpha = 1;
}
}
}
// Re-apply power crystal interactivity
applyPowerCrystalInteractivity();
}
// --- Touch to Spin/Reshuffle (tap anywhere else, fallback for desktop) ---
// Removed global tap-to-reshuffle so only Spin button restarts the game
// --- Game Over/Win (not used, slot is endless) ---
// --- Game Update (not needed, all logic is event/cascade driven) ---
game.update = function () {
// Update bugs (background animation)
for (var i = 0; i < bugs.length; i++) {
if (bugs[i] && typeof bugs[i].update === "function") {
bugs[i].update();
}
}
};
// --- Start the game ---
startGame();
;