/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1"); /**** * Classes ****/ // Candy class: represents a single candy on the board var Candy = Container.expand(function () { var self = Container.call(this); // Properties self.type = 0; // 0-6, for 7 types self.row = 0; self.col = 0; self.isMatched = false; // Attach asset for candy, anchor center self.candyAsset = null; // Set candy type and update asset self.setType = function (type) { self.type = type; // Remove previous asset if exists if (self.candyAsset) { self.removeChild(self.candyAsset); } // Assign each candy type to a unique asset for visual distinction var assetIds = ['Sg1', // type 0 'Sg2', // type 1 'Sg3', // type 2 'Sg4', // type 3 'Sg5', // type 4 'Sg6', // type 5 'Sg7' // type 6 ]; self.candyAsset = self.attachAsset(assetIds[type], { anchorX: 0.5, anchorY: 0.5 }); }; // Animate pop (match) self.pop = function (_onFinish) { tween(self, { scaleX: 1.3, scaleY: 1.3, alpha: 0 }, { duration: 250, easing: tween.easeIn, onFinish: function onFinish() { self.alpha = 1; self.scaleX = 1; self.scaleY = 1; if (_onFinish) _onFinish(); } }); }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x222244 }); /**** * Game Code ****/ // --- Game Constants --- var boardCols = 7; var boardRows = 9; var candyTypes = 7; var candySize = 200; // px, will be scaled to fit var boardPadding = 0; // No padding between candies var moveLimit = 20; var targetScore = 1800; // --- Game State --- var board = []; // 2D array [row][col] of Candy var candies = []; // Flat array of all Candy objects var selectedCandy = null; var swappingCandy = null; var isSwapping = false; var isAnimating = false; var movesLeft = moveLimit; var score = 0; var level = storage.level || 1; // --- UI Elements --- var scoreTxt = new Text2('0', { size: 90, fill: "#fff" }); scoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(scoreTxt); var movesTxt = new Text2('Moves: ' + movesLeft, { size: 70, fill: "#fff" }); movesTxt.anchor.set(0.5, 0); LK.gui.top.addChild(movesTxt); movesTxt.y = 110; // Best score UI var bestScore = storage.bestScore || 0; var bestScoreTxt = new Text2('Best: ' + bestScore, { size: 60, fill: "#fff" }); bestScoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(bestScoreTxt); bestScoreTxt.y = 190; // Current score UI (label) var currentScoreTxt = new Text2('Score', { size: 60, fill: "#fff" }); currentScoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(currentScoreTxt); currentScoreTxt.y = 40; // Add "env games" text in the bottom right corner var envGamesTxt = new Text2('env games', { size: 40, fill: "#fff" }); envGamesTxt.anchor.set(1, 1); LK.gui.bottomRight.addChild(envGamesTxt); // --- Board Positioning --- // Add Sg_bg asset as a background layer, scaled to cover the game area var sgBgAsset = LK.getAsset('Sg_bg', { anchorX: 0, anchorY: 0, x: 0, y: 0, width: 2048, height: 2732 }); game.addChild(sgBgAsset); // Add Bg asset as background, scaled to cover the game area var bgAsset = LK.getAsset('Bg', { anchorX: 0, anchorY: 0, x: 0, y: 0, width: 2048, height: 2732 }); game.addChild(bgAsset); var boardWidth = boardCols * candySize; var boardHeight = boardRows * candySize; var boardOffsetX = Math.floor((2048 - boardWidth) / 2); var boardOffsetY = Math.floor((2732 - boardHeight) / 2) + 60; // --- Helper Functions --- // Get board position (x, y) for a given row, col function getBoardPos(row, col) { return { x: boardOffsetX + col * candySize + candySize / 2, y: boardOffsetY + row * candySize + candySize / 2 }; } // Get candy at (row, col) function getCandy(row, col) { if (row < 0 || row >= boardRows || col < 0 || col >= boardCols) return null; return board[row][col]; } // Swap two candies in board and update their row/col function swapCandies(c1, c2) { if (!c1 || !c2) { // Defensive: do nothing if either candy is null or undefined return; } var r1 = c1.row, c1c = c1.col, r2 = c2.row, c2c = c2.col; board[r1][c1c] = c2; board[r2][c2c] = c1; c1.row = r2; c1.col = c2c; c2.row = r1; c2.col = c1c; } // Animate swap between two candies function animateSwap(c1, c2, _onFinish2) { if (!c1 || !c2) { // Defensive: do nothing if either candy is null or undefined isAnimating = false; if (_onFinish2) _onFinish2(); return; } isAnimating = true; var pos1 = getBoardPos(c1.row, c1.col); var pos2 = getBoardPos(c2.row, c2.col); tween(c1, { x: pos2.x, y: pos2.y }, { duration: 180, easing: tween.cubicInOut }); tween(c2, { x: pos1.x, y: pos1.y }, { duration: 180, easing: tween.cubicInOut, onFinish: function onFinish() { isAnimating = false; if (_onFinish2) _onFinish2(); } }); } // Animate candy falling to new position function animateMoveTo(candy, newRow, newCol, onFinish) { var pos = getBoardPos(newRow, newCol); tween(candy, { x: pos.x, y: pos.y }, { duration: 180, easing: tween.cubicInOut, onFinish: onFinish }); } // Deselect all candies function deselectAll() { if (selectedCandy) { tween(selectedCandy, { scaleX: 1, scaleY: 1 }, { duration: 100 }); selectedCandy = null; } } // Check if two candies are adjacent function areAdjacent(c1, c2) { return Math.abs(c1.row - c2.row) + Math.abs(c1.col - c2.col) === 1; } // Find all matches on the board, mark candies as isMatched function findMatches() { var found = false; // Clear previous matches for (var r = 0; r < boardRows; r++) { for (var c = 0; c < boardCols; c++) { if (board[r][c]) board[r][c].isMatched = false; } } // Horizontal matches for (var r = 0; r < boardRows; r++) { var matchLen = 1; for (var c = 1; c < boardCols; c++) { if (board[r][c].type === board[r][c - 1].type) { matchLen++; } else { if (matchLen >= 3) { for (var k = 0; k < matchLen; k++) { board[r][c - 1 - k].isMatched = true; } found = true; } matchLen = 1; } } if (matchLen >= 3) { for (var k = 0; k < matchLen; k++) { board[r][boardCols - 1 - k].isMatched = true; } found = true; } } // Vertical matches for (var c = 0; c < boardCols; c++) { var matchLen = 1; for (var r = 1; r < boardRows; r++) { if (board[r][c].type === board[r - 1][c].type) { matchLen++; } else { if (matchLen >= 3) { for (var k = 0; k < matchLen; k++) { board[r - 1 - k][c].isMatched = true; } found = true; } matchLen = 1; } } if (matchLen >= 3) { for (var k = 0; k < matchLen; k++) { board[boardRows - 1 - k][c].isMatched = true; } found = true; } } return found; } // Remove matched candies, return number removed function removeMatches(onFinish) { var removed = 0; var toPop = []; for (var r = 0; r < boardRows; r++) { for (var c = 0; c < boardCols; c++) { var candy = board[r][c]; if (candy.isMatched) { toPop.push(candy); removed++; } } } if (removed === 0) { if (onFinish) onFinish(); return; } var popped = 0; for (var i = 0; i < toPop.length; i++) { // Play sg_crash sound for each exploding candy LK.getSound('Sg_crash').play(); toPop[i].pop(function () { popped++; if (popped === toPop.length) { for (var j = 0; j < toPop.length; j++) { var c = toPop[j]; game.removeChild(c); candies.splice(candies.indexOf(c), 1); board[c.row][c.col] = null; } if (onFinish) onFinish(removed); } }); } } // Drop candies down to fill empty spaces, return true if any moved function dropCandies(onFinish) { var moved = false; var moves = []; for (var c = 0; c < boardCols; c++) { for (var r = boardRows - 1; r >= 0; r--) { if (board[r][c] === null) { // Find first non-null above for (var k = r - 1; k >= 0; k--) { if (board[k][c]) { var candy = board[k][c]; board[r][c] = candy; board[k][c] = null; var oldRow = candy.row; candy.row = r; moves.push({ candy: candy, from: oldRow, to: r, col: c }); break; } } } } } if (moves.length === 0) { if (onFinish) onFinish(false); return; } var finished = 0; for (var i = 0; i < moves.length; i++) { var move = moves[i]; animateMoveTo(move.candy, move.to, move.col, function () { finished++; if (finished === moves.length) { if (onFinish) onFinish(true); } }); } } // Fill empty spaces at the top with new candies function fillBoard(onFinish) { var filled = 0; var toFill = []; for (var c = 0; c < boardCols; c++) { for (var r = 0; r < boardRows; r++) { if (board[r][c] === null) { var candy = new Candy(); var type = Math.floor(Math.random() * candyTypes); candy.setType(type); candy.row = r; candy.col = c; var pos = getBoardPos(r, c); candy.x = pos.x; candy.y = pos.y - 400; // Drop from above game.addChild(candy); candies.push(candy); board[r][c] = candy; toFill.push(candy); } } } if (toFill.length === 0) { if (onFinish) onFinish(); return; } var finished = 0; for (var i = 0; i < toFill.length; i++) { var candy = toFill[i]; var pos = getBoardPos(candy.row, candy.col); animateMoveTo(candy, candy.row, candy.col, function () { finished++; if (finished === toFill.length) { if (onFinish) onFinish(); } }); } } // Check if the board has any possible moves (for future: shuffle if not) function hasPossibleMoves() { // For MVP, skip shuffle, always assume possible moves return true; } // Update UI function updateUI() { scoreTxt.setText(score); movesTxt.setText('Moves: ' + movesLeft); if (score > bestScore) { bestScore = score; storage.bestScore = bestScore; } bestScoreTxt.setText('Best: ' + bestScore); } // Handle end of turn: check for matches, drop, fill, repeat as needed function resolveBoard(afterResolve) { if (findMatches()) { removeMatches(function (removed) { // Award more points for more candies exploded at once: 3=100, 4=150, 5=200, 6+=4x points var points = 0; if (removed >= 3) { if (removed === 3) { points = 100; } else if (removed === 4) { points = Math.floor(100 * 1.5); // 150 } else if (removed === 5) { points = 200; } else if (removed >= 6) { // 6-pack or more: 4x the base points for that many candies // Base: 3=100, 4=150, 5=200, 6=250, 7=300, 8=350, etc. var base = 100 + Math.max(0, removed - 3) * 50; points = base * 4; } } score += points; updateUI(); dropCandies(function () { fillBoard(function () { resolveBoard(afterResolve); }); }); }); } else { if (afterResolve) afterResolve(); } } // Handle win/lose function checkEnd() { if (movesLeft <= 0) { // Only end the game if there are no matches left to resolve if (!findMatches()) { // Show game over when moves are finished and board is resolved LK.showGameOver(); return true; } // Otherwise, let the board resolve naturally before ending return false; } return false; } // --- Board Initialization --- function createBoard() { // Clear previous for (var i = 0; i < candies.length; i++) { game.removeChild(candies[i]); } candies = []; board = []; for (var r = 0; r < boardRows; r++) { board[r] = []; for (var c = 0; c < boardCols; c++) { var candy = new Candy(); var type = Math.floor(Math.random() * candyTypes); candy.setType(type); candy.row = r; candy.col = c; var pos = getBoardPos(r, c); candy.x = pos.x; candy.y = pos.y; game.addChild(candy); candies.push(candy); board[r][c] = candy; } } // Remove any initial matches while (findMatches()) { for (var r = 0; r < boardRows; r++) { for (var c = 0; c < boardCols; c++) { if (board[r][c] && board[r][c].isMatched) { var newType; do { newType = Math.floor(Math.random() * candyTypes); } while (r > 0 && board[r - 1][c] && board[r - 1][c].type === newType || c > 0 && board[r][c - 1] && board[r][c - 1].type === newType); board[r][c].setType(newType); board[r][c].isMatched = false; } } } } } // --- Input Handling --- // Find candy at (x, y) in game coordinates function findCandyAt(x, y) { for (var i = 0; i < candies.length; i++) { var c = candies[i]; var dx = x - c.x; var dy = y - c.y; if (Math.abs(dx) < candySize / 2 && Math.abs(dy) < candySize / 2) { return c; } } return null; } // Handle tap or drag game.down = function (x, y, obj) { if (isAnimating) return; var c = findCandyAt(x, y); if (!c) { deselectAll(); return; } if (!selectedCandy) { selectedCandy = c; tween(selectedCandy, { scaleX: 1.15, scaleY: 1.15 }, { duration: 100 }); } else if (selectedCandy === c) { deselectAll(); } else if (areAdjacent(selectedCandy, c)) { // Swap attempt isSwapping = true; swappingCandy = c; // Animate swap animateSwap(selectedCandy, swappingCandy, function () { swapCandies(selectedCandy, swappingCandy); // Check if swap creates a match if (findMatches()) { movesLeft--; updateUI(); resolveBoard(function () { deselectAll(); isSwapping = false; swappingCandy = null; checkEnd(); }); } else { // No match, swap back and decrement moves (wrong move) // Play Wrong sound LK.getSound('Wrong').play(); movesLeft--; updateUI(); animateSwap(selectedCandy, swappingCandy, function () { swapCandies(selectedCandy, swappingCandy); deselectAll(); isSwapping = false; swappingCandy = null; checkEnd(); }); } }); } else { // Select new candy tween(selectedCandy, { scaleX: 1, scaleY: 1 }, { duration: 100 }); selectedCandy = c; tween(selectedCandy, { scaleX: 1.15, scaleY: 1.15 }, { duration: 100 }); } }; // Drag to swap game.move = function (x, y, obj) { if (isAnimating || !selectedCandy) return; var c = findCandyAt(x, y); if (c && c !== selectedCandy && areAdjacent(selectedCandy, c)) { game.down(x, y, obj); } }; game.up = function (x, y, obj) { // No-op for now }; // --- Game Start --- function startGame() { level = storage.level || 1; score = 0; if (level === 2) { movesLeft = 10; targetScore = 2500; } else { movesLeft = moveLimit; targetScore = 1800; } updateUI(); createBoard(); deselectAll(); isAnimating = false; isSwapping = false; swappingCandy = null; selectedCandy = null; // Start Fnt music LK.playMusic('Fnt'); // Remove any matches at start resolveBoard(); } startGame(); // --- Game Tick --- game.update = function () { // No per-frame logic needed for MVP };
===================================================================
--- original.js
+++ change.js
@@ -122,8 +122,15 @@
});
currentScoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(currentScoreTxt);
currentScoreTxt.y = 40;
+// Add "env games" text in the bottom right corner
+var envGamesTxt = new Text2('env games', {
+ size: 40,
+ fill: "#fff"
+});
+envGamesTxt.anchor.set(1, 1);
+LK.gui.bottomRight.addChild(envGamesTxt);
// --- Board Positioning ---
// Add Sg_bg asset as a background layer, scaled to cover the game area
var sgBgAsset = LK.getAsset('Sg_bg', {
anchorX: 0,