/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
var Column = Container.expand(function (gemTypes) {
var self = Container.call(this);
self.gems = [];
self.isActive = false;
// Create the three gems that form the column
for (var i = 0; i < 3; i++) {
var type = gemTypes && gemTypes[i] ? gemTypes[i] : null;
var gem = new Gem(type);
gem.y = -i * CELL_SIZE;
self.gems.push(gem);
self.addChild(gem);
}
self.rotate = function () {
if (!self.isActive) {
return;
}
// Move the last gem to a temporary position for animation
var lastGem = self.gems.pop();
var firstGem = self.gems[0];
var middleGem = self.gems[1];
// Animate the rotation
LK.getSound('rotate').play();
// Update the gems array with the new order
self.gems = [lastGem, firstGem, middleGem];
// Update positions to match new order
tween(lastGem, {
y: firstGem.y
}, {
duration: 150
});
tween(firstGem, {
y: middleGem.y
}, {
duration: 150
});
tween(middleGem, {
y: lastGem.y + 2 * CELL_SIZE
}, {
duration: 150,
onFinish: function onFinish() {
// Make sure to only reposition if the column is still active
if (self.isActive) {
// Reset positions after animation
for (var i = 0; i < 3; i++) {
self.gems[i].y = -i * CELL_SIZE;
}
}
}
});
};
self.getTypes = function () {
return self.gems.map(function (gem) {
return gem.type;
});
};
return self;
});
var GameBoard = Container.expand(function () {
var self = Container.call(this);
self.grid = [];
self.cellContainer = self.addChild(new Container());
self.gemContainer = self.addChild(new Container());
// Create background
var boardBg = self.cellContainer.attachAsset('board_bg', {
anchorX: 0.5,
// Center horizontally
anchorY: 0.5 // Center vertically
});
boardBg.x = COLS * CELL_SIZE / 2; // Center the background on the board
boardBg.y = ROWS * CELL_SIZE / 2; // Center the background on the board
// Create grid cells
for (var row = 0; row < ROWS; row++) {
self.grid[row] = [];
for (var col = 0; col < COLS; col++) {
// Create cell background
var cell = LK.getAsset('board_cell', {
anchorX: 0.5,
anchorY: 0.5,
x: col * CELL_SIZE + CELL_SIZE / 2,
y: row * CELL_SIZE + CELL_SIZE / 2,
alpha: 0
});
self.cellContainer.addChild(cell);
self.grid[row][col] = null;
// Create margin cells for impassable columns (first 3 and last 3)
if (col < 3 || col >= COLS - 3) {
// Use 'margin_cell_top' for row 0, 'margin_cell' for others
var marginCellAssetId = row === 0 ? 'margin_cell_top' : row == ROWS - 1 ? 'margin_cell_bottom' : 'margin_cell';
var marginCell = LK.getAsset(marginCellAssetId, {
anchorX: 0.5,
anchorY: 0.5,
x: col * CELL_SIZE + CELL_SIZE / 2,
y: row * CELL_SIZE + CELL_SIZE / 2,
alpha: 1
});
self.cellContainer.addChild(marginCell); // Add visual to board container
// Use the GLOBAL arrays
marginCells.push(marginCell); // Add cell object to global list
// Mark column as impassable globally
if (!impassableCols.includes(col)) {
// Check global list
impassableCols.push(col); // Add to global list
}
// Simulate a frozen cell by adding an entry to frozenGems
frozenGems[row + ',' + col] = true; // frozenGems is already global
}
}
}
self.addGemToGrid = function (gem, row, col) {
if (row < 0 || row >= ROWS || col < 0 || col >= COLS) {
return false;
}
if (self.grid[row][col] !== null) {
return false;
}
gem.x = col * CELL_SIZE + CELL_SIZE / 2;
gem.y = row * CELL_SIZE + CELL_SIZE / 2;
self.gemContainer.addChild(gem);
self.grid[row][col] = gem;
return true;
};
self.removeGemFromGrid = function (row, col) {
if (row < 0 || row >= ROWS || col < 0 || col >= COLS) {
return null;
}
var gem = self.grid[row][col];
if (gem) {
self.grid[row][col] = null;
delete frozenGems[row + ',' + col]; // Remove gem from the dictionary
return gem;
}
return null;
};
self.isColumnFull = function (col) {
return self.grid[0][col] !== null;
};
self.canPlaceColumn = function (col) {
// Check if column is in the impassable zone
if (impassableCols.includes(col)) {
return false;
}
// Check if any of the top 3 rows in the column are occupied
for (var row = 0; row < 3; row++) {
if (col < 0 || col >= COLS) {
return false;
}
if (row < ROWS && self.grid[row][col] !== null) {
return false;
}
}
return true;
};
self.findFirstEmptyRow = function (col) {
for (var row = ROWS - 1; row >= 0; row--) {
if (self.grid[row][col] === null) {
return row;
}
}
return -1; // Column is full
};
self.placeColumn = function (column, col, callback) {
var gems = column.gems;
var placedGems = [];
var firstEmptyRow = self.findFirstEmptyRow(col);
if (firstEmptyRow < 0 || firstEmptyRow < gems.length - 1) {
if (callback) {
callback(false);
}
return;
}
// Calculate the current positions for each gem before removing the column
var currentPositions = [];
for (var i = 0; i < gems.length; i++) {
var gem = gems[i];
if (column.parent) {
var globalPos = column.parent.toGlobal({
x: gem.x + column.x,
y: gem.y + column.y
});
currentPositions.push(globalPos);
}
}
// Remove gems from their current parent before placing them
// This prevents them from being manipulated during the transition
if (column.parent) {
column.parent.removeChild(column);
}
// Place gems from bottom to top
for (var i = 0; i < gems.length; i++) {
var row = firstEmptyRow - i;
if (row >= 0) {
// Use the existing gem instead of creating a new one
var gem = gems[i];
placedGems.push({
gem: gem,
row: row,
col: col
});
// Remove gem from column container first to avoid parent hierarchy issues
if (gem.parent === column) {
column.removeChild(gem);
}
// Use the captured position if available to ensure smooth transition
if (currentPositions[i]) {
var localPos = self.gemContainer.toLocal(currentPositions[i]);
gem.x = localPos.x;
gem.y = localPos.y;
// Animate to final position
tween(gem, {
x: col * CELL_SIZE + CELL_SIZE / 2,
y: row * CELL_SIZE + CELL_SIZE / 2
}, {
duration: 100
});
} else {
// Fallback if position wasn't captured
gem.x = col * CELL_SIZE + CELL_SIZE / 2;
gem.y = row * CELL_SIZE + CELL_SIZE / 2;
}
// Add the gem to the grid
self.gemContainer.addChild(gem);
self.grid[row][col] = gem;
frozenGems[row + ',' + col] = gem; // Store gem in the dictionary
gem.isPlaced = true; // Mark gem as placed to prevent further interaction
}
}
// Make sure drop sound always plays
LK.getSound('drop').stop();
LK.getSound('drop').play();
if (callback) {
callback(true, placedGems);
}
};
self.checkMatches = function () {
var matchedCells = [];
// Check horizontal matches
for (var row = 0; row < ROWS; row++) {
var count = 1;
var type = null;
for (var col = 0; col < COLS; col++) {
var gem = self.grid[row][col];
if (gem && type === gem.type) {
count++;
} else {
if (count >= 3) {
for (var i = 0; i < count; i++) {
matchedCells.push({
row: row,
col: col - 1 - i
});
}
}
count = 1;
type = gem ? gem.type : null;
}
}
if (count >= 3) {
for (var i = 0; i < count; i++) {
matchedCells.push({
row: row,
col: COLS - 1 - i
});
}
}
}
// Check vertical matches
for (var col = 0; col < COLS; col++) {
var count = 1;
var type = null;
for (var row = 0; row < ROWS; row++) {
var gem = self.grid[row][col];
if (gem && type === gem.type) {
count++;
} else {
if (count >= 3) {
for (var i = 0; i < count; i++) {
matchedCells.push({
row: row - 1 - i,
col: col
});
}
}
count = 1;
type = gem ? gem.type : null;
}
}
if (count >= 3) {
for (var i = 0; i < count; i++) {
matchedCells.push({
row: ROWS - 1 - i,
col: col
});
}
}
}
// Check diagonal matches (top-left to bottom-right)
for (var startRow = 0; startRow <= ROWS - 3; startRow++) {
for (var startCol = 0; startCol <= COLS - 3; startCol++) {
var count = 1;
var type = null;
// Check diagonal starting at [startRow, startCol]
for (var i = 0; i < Math.min(ROWS - startRow, COLS - startCol); i++) {
var gem = self.grid[startRow + i][startCol + i];
if (gem && type === gem.type) {
count++;
} else {
if (count >= 3) {
// Add matching gems to matchedCells
for (var j = 0; j < count; j++) {
matchedCells.push({
row: startRow + i - 1 - j,
col: startCol + i - 1 - j
});
}
}
count = 1;
type = gem ? gem.type : null;
}
}
// Check if there's a match at the end of the diagonal
if (count >= 3) {
var endI = Math.min(ROWS - startRow, COLS - startCol);
for (var j = 0; j < count; j++) {
matchedCells.push({
row: startRow + endI - 1 - j,
col: startCol + endI - 1 - j
});
}
}
}
}
// Check diagonal matches (top-right to bottom-left)
for (var startRow = 0; startRow <= ROWS - 3; startRow++) {
for (var startCol = COLS - 1; startCol >= 2; startCol--) {
var count = 1;
var type = null;
// Check diagonal starting at [startRow, startCol]
for (var i = 0; i < Math.min(ROWS - startRow, startCol + 1); i++) {
var gem = self.grid[startRow + i][startCol - i];
if (gem && type === gem.type) {
count++;
} else {
if (count >= 3) {
// Add matching gems to matchedCells
for (var j = 0; j < count; j++) {
matchedCells.push({
row: startRow + i - 1 - j,
col: startCol - i + 1 + j
});
}
}
count = 1;
type = gem ? gem.type : null;
}
}
// Check if there's a match at the end of the diagonal
if (count >= 3) {
var endI = Math.min(ROWS - startRow, startCol + 1);
for (var j = 0; j < count; j++) {
matchedCells.push({
row: startRow + endI - 1 - j,
col: startCol - endI + 1 + j
});
}
}
}
}
// Remove duplicates from matchedCells
var uniqueMatches = [];
var matchMap = {};
for (var i = 0; i < matchedCells.length; i++) {
var cell = matchedCells[i];
var key = cell.row + "," + cell.col;
if (!matchMap[key]) {
matchMap[key] = true;
uniqueMatches.push(cell);
}
}
return uniqueMatches;
};
self.removeMatches = function (matches, callback) {
if (matches.length === 0) {
if (callback) {
callback(0);
}
return;
}
var remainingAnimations = matches.length;
var points = matches.length * 10;
// Make sure to stop any previously playing match sound to avoid conflicts
LK.getSound('match').stop();
// Play the match sound fresh
LK.getSound('match').play();
// Flash screen effect for matches - ensure it always triggers
LK.effects.flashScreen(0xFFFFFF, 300);
matches.forEach(function (match) {
var gem = self.grid[match.row][match.col];
if (gem) {
self.grid[match.row][match.col] = null;
delete frozenGems[match.row + ',' + match.col]; // Remove gem from the dictionary
gem.highlight();
LK.setTimeout(function () {
gem.animateMatch(function () {
self.gemContainer.removeChild(gem);
remainingAnimations--;
if (remainingAnimations === 0) {
if (callback) {
callback(points);
}
}
});
}, 100);
} else {
remainingAnimations--;
if (remainingAnimations === 0 && callback) {
callback(points);
}
}
});
};
self.applyGravity = function (callback) {
var movedGems = false;
var animationsRunning = 0;
// Iterate bottom to top, right to left
for (var col = 0; col < COLS; col++) {
for (var row = ROWS - 1; row > 0; row--) {
if (self.grid[row][col] === null) {
// Find the first non-null gem above this position
for (var checkRow = row - 1; checkRow >= 0; checkRow--) {
if (self.grid[checkRow][col] !== null) {
var gem = self.grid[checkRow][col];
self.grid[checkRow][col] = null;
self.grid[row][col] = gem;
// Update the frozenGems dictionary
delete frozenGems[checkRow + ',' + col];
frozenGems[row + ',' + col] = gem;
movedGems = true;
animationsRunning++;
gem.animateDrop(row * CELL_SIZE + CELL_SIZE / 2, 0, function () {
animationsRunning--;
if (animationsRunning === 0 && callback) {
callback(movedGems);
}
});
break;
}
}
}
}
}
if (animationsRunning === 0 && callback) {
callback(movedGems);
}
};
return self;
});
var Gem = Container.expand(function (type) {
var self = Container.call(this);
self.type = type || getRandomGemType();
self.isPlaced = false; // Track if gem is part of a placed column
var gemSprite = self.attachAsset('gem_' + self.type, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.95,
scaleY: 0.95
});
self.setType = function (newType) {
self.type = newType;
self.removeChildren();
gemSprite = self.attachAsset('gem_' + newType, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.95,
scaleY: 0.95
});
};
self.highlight = function () {
tween(gemSprite, {
scaleX: 1.1,
scaleY: 1.1
}, {
duration: 200,
easing: tween.easeOut
});
};
self.unhighlight = function () {
tween(gemSprite, {
scaleX: 0.95,
scaleY: 0.95
}, {
duration: 200,
easing: tween.easeOut
});
};
self.animateMatch = function (callback) {
tween(gemSprite, {
alpha: 0,
scaleX: 0.2,
scaleY: 0.2
}, {
duration: 300,
easing: tween.easeIn,
onFinish: function onFinish() {
if (callback) {
callback();
}
}
});
};
self.animateDrop = function (targetY, delay, callback) {
// Mark gem as being animated to prevent user interaction during animation
self.isAnimating = true;
tween(self, {
y: targetY
}, {
duration: 300 + delay,
easing: tween.bounceOut,
onFinish: function onFinish() {
self.isAnimating = false;
if (callback) {
callback();
}
}
});
};
// Add down handler to prevent interaction during transition
self.down = function (x, y, obj) {
// Prevent interaction if gem is being animated or is already placed
if (self.isAnimating || self.isPlaced) {
return;
}
// Default behavior for interaction with gems
// (This can be expanded if you want gems to be interactive in a certain way)
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x111122
});
/****
* Game Code
****/
// Constants
var CELL_SIZE = 120;
var COLS = 19;
var ROWS = Math.floor(2732 / CELL_SIZE);
//var ROWS = 25;
var GEM_TYPES = ['red', 'blue', 'green', 'yellow', 'purple'];
var DROP_INTERVAL_START = 60; // ms
var DROP_INTERVAL_MIN = 60; // ms
var DROP_DELTA = 20;
var LEVEL_THRESHOLD = 50; // Points needed to level up
// Game state
var board;
var activeColumn;
var nextColumn;
var dropInterval;
var dropTimer = 0;
var fallSpeed = DROP_INTERVAL_START;
var gameActive = false;
var score = 0;
var level = 1;
var nextColumnPreview;
var columnPositionX = 0;
// Dictionary to track frozen gems
var frozenGems = {};
// Arrays to track impassable columns and their visual representation
var impassableCols = []; // Stores column indices that are impassable
var marginCells = []; // Stores the Container objects for margin cell visuals
// UI elements
var scoreText;
var levelText;
var highScoreText;
function getRandomGemType() {
var index = Math.floor(Math.random() * GEM_TYPES.length);
return GEM_TYPES[index];
}
function setupUI() {
// Score text
scoreText = new Text2('SCORE: 0', {
size: 50,
fill: 0xFFFFFF,
// Changed to white
stroke: 0xFFFFFF,
// Add white stroke
strokeThickness: 5 // Thicker stroke for better visibility
});
scoreText.anchor.set(0.5, 0);
scoreText.y = 50;
LK.gui.top.addChild(scoreText);
// Level text
levelText = new Text2('LEVEL: 1', {
size: 50,
fill: 0xFFFFFF,
// Changed to white
stroke: 0xFFFFFF,
// Add white stroke
strokeThickness: 5 // Thicker stroke for better visibility
});
levelText.anchor.set(0.5, 0);
levelText.y = 100;
LK.gui.top.addChild(levelText);
// High score text
var highScore = storage.highScore || 0;
highScoreText = new Text2('BEST: ' + highScore, {
size: 40,
fill: 0x00FF00,
// Changed to white
stroke: 0xFFFFFF,
// Add white stroke
strokeThickness: 5 // Thicker stroke for better visibility
});
highScoreText.anchor.set(0.5, 0);
highScoreText.y = 150;
LK.gui.top.addChild(highScoreText);
}
function updateScore(points) {
score += points;
scoreText.setText('SCORE: ' + score);
// Check if player leveled up
var newLevel = Math.floor(score / LEVEL_THRESHOLD) + 1;
if (newLevel > level) {
level = newLevel;
levelText.setText('LEVEL: ' + level);
// Increase game speed
fallSpeed = Math.max(DROP_INTERVAL_MIN, DROP_INTERVAL_START - (level - 1) * DROP_DELTA); // Adjust fallSpeed based on the current level
// Play level up sound
LK.getSound('levelup').stop(); // Stop first to ensure it plays fresh
LK.getSound('levelup').play();
// Flash effect for level up
LK.effects.flashScreen(0x00FFFF, 500);
}
// Update high score if needed
var highScore = storage.highScore || 0;
if (score > highScore) {
storage.highScore = score;
highScoreText.setText('BEST: ' + score);
}
}
function createBoard() {
// Clear previous margin cells visually and from the array
marginCells.forEach(function (cell) {
if (cell && cell.parent) {
cell.parent.removeChild(cell);
}
// Optionally destroy: cell.destroy();
});
marginCells = []; // Reset the global array
// Reset impassable columns list
impassableCols = [];
// Remove old board instance if it exists
if (board && board.parent) {
board.parent.removeChild(board);
// Optionally destroy: board.destroy();
}
// Create and position the new board
board = new GameBoard();
board.x = (2048 - COLS * CELL_SIZE) / 2;
board.y = (2732 - ROWS * CELL_SIZE) / 2;
game.addChild(board);
}
function createNextColumnPreview() {
nextColumnPreview = new Container();
nextColumnPreview.x = 2048 - 120;
nextColumnPreview.y = 200;
// Create the "NEXT" label
var previewLabel = new Text2('NEXT', {
size: 80,
fill: 0xFFFF00,
stroke: 0xFFFFFF,
// White stroke
strokeThickness: 5 // Thicker stroke for better visibility
});
previewLabel.anchor.set(0.5, 0);
previewLabel.y = -80; // Position at the top of the container
nextColumnPreview.addChild(previewLabel);
game.addChild(nextColumnPreview);
}
function updateNextColumnPreview() {
// Clear previous preview
for (var i = nextColumnPreview.children.length - 1; i > 0; i--) {
nextColumnPreview.removeChild(nextColumnPreview.children[i]);
}
// Add new gems to preview in the correct order
if (nextColumn) {
// Loop through gems in reverse order to match the visual appearance in the game
for (var i = nextColumn.gems.length - 1; i >= 0; i--) {
var previewGem = new Gem(nextColumn.gems[i].type);
previewGem.x = 0;
// Position from top to bottom in the preview
previewGem.y = 80 + (nextColumn.gems.length - 1 - i) * CELL_SIZE;
previewGem.scale.set(0.8);
nextColumnPreview.addChild(previewGem);
}
}
}
function createNewColumn() {
var column = new Column();
column.isActive = true;
column.fastDrop = false; // Initialize fastDrop property for speed control
return column;
}
function startGame() {
// Reset game state
score = 0;
level = 1;
fallSpeed = DROP_INTERVAL_START; // Initialize fallSpeed with the starting drop interval
gameActive = true;
dropTimer = 0;
// Clear frozen gems dictionary
frozenGems = {};
// --- Start Cleanup ---
// Remove title screen elements safely
if (typeof titleImage !== 'undefined' && titleImage && titleImage.parent) {
titleImage.destroy(); // Use destroy to free resources
titleImage = null; // Nullify reference
}
if (typeof startGameText !== 'undefined' && startGameText && startGameText.parent) {
startGameText.destroy();
startGameText = null;
}
if (typeof blackBg !== 'undefined' && blackBg && blackBg.parent) {
blackBg.destroy();
blackBg = null;
}
// --- End Cleanup ---
// Create the game board (handles its own cleanup now)
createBoard();
// Create the preview for the next column
if (nextColumnPreview && nextColumnPreview.parent) {
// Check parent before removing
game.removeChild(nextColumnPreview);
// nextColumnPreview.destroy(); // Consider if needed
}
createNextColumnPreview();
// Set up UI (ensure setupUI handles potential existing UI elements if necessary)
setupUI();
// Create initial columns
activeColumn = createNewColumn();
nextColumn = createNewColumn();
updateNextColumnPreview();
// Position active column (avoid impassable columns)
columnPositionX = Math.floor(COLS / 2);
// Ensure starting position is not in an impassable column
while (impassableCols.includes(columnPositionX)) {
columnPositionX = Math.min(COLS - 4, Math.max(3, columnPositionX));
}
activeColumn.x = board.x + columnPositionX * CELL_SIZE + CELL_SIZE / 2;
activeColumn.y = 0;
game.addChild(activeColumn);
// Start music
LK.playMusic('bgmusic', {
fade: {
start: 0,
end: 0.4,
duration: 1000
}
});
}
function processMatches(callback) {
var matches = board.checkMatches();
if (matches.length > 0) {
// Ensure sound and visual feedback triggers for every match
board.removeMatches(matches, function (points) {
updateScore(points);
board.applyGravity(function (movedGems) {
// Check for chain reactions
if (movedGems) {
// For chain reactions, process additional matches
processMatches(callback);
} else {
if (callback) {
callback();
}
}
});
});
} else {
if (callback) {
callback();
}
}
}
function spawnNewColumn() {
activeColumn = nextColumn;
nextColumn = createNewColumn();
updateNextColumnPreview();
columnPositionX = Math.floor(COLS / 2);
// Ensure new column doesn't spawn in an impassable column
while (impassableCols.includes(columnPositionX)) {
columnPositionX = Math.min(COLS - 4, Math.max(3, columnPositionX));
}
activeColumn.x = board.x + columnPositionX * CELL_SIZE + CELL_SIZE / 2;
activeColumn.y = 0;
game.addChild(activeColumn);
// Check if game over
if (!board.canPlaceColumn(columnPositionX)) {
gameActive = false;
LK.showGameOver();
} else {
activeColumn.isActive = true; // Reactivate the new column
}
}
function moveColumnLeft() {
if (!gameActive || !activeColumn || !activeColumn.isActive) {
return;
}
if (columnPositionX > 0) {
// Check if there's a collision in the new position
var canMove = true;
var currentRow = Math.floor(activeColumn.y / CELL_SIZE);
for (var i = 0; i < activeColumn.gems.length; i++) {
var gemRow = currentRow - i;
if (gemRow >= 0 && gemRow < ROWS) {
var gemKey = gemRow + ',' + (columnPositionX - 1);
if (frozenGems[gemKey]) {
canMove = false;
break;
}
}
}
if (canMove) {
columnPositionX--;
activeColumn.x = board.x + columnPositionX * CELL_SIZE + CELL_SIZE / 2;
}
}
}
function moveColumnRight() {
if (!gameActive || !activeColumn || !activeColumn.isActive) {
return;
}
if (columnPositionX < COLS - 1) {
// Check if there's a collision in the new position
var canMove = true;
var currentRow = Math.floor(activeColumn.y / CELL_SIZE);
for (var i = 0; i < activeColumn.gems.length; i++) {
var gemRow = currentRow - i;
if (gemRow >= 0 && gemRow < ROWS) {
var gemKey = gemRow + ',' + (columnPositionX + 1);
if (frozenGems[gemKey]) {
canMove = false;
break;
}
}
}
if (canMove) {
columnPositionX++;
activeColumn.x = board.x + columnPositionX * CELL_SIZE + CELL_SIZE / 2;
}
}
}
function rotateColumn() {
if (!gameActive || !activeColumn) {
return;
}
activeColumn.rotate();
}
function dropColumn() {
if (!gameActive || !activeColumn || !activeColumn.isActive) {
return;
}
// First, make it inactive to prevent further user interaction
activeColumn.isActive = false;
// Update column position
activeColumn.x = board.x + columnPositionX * CELL_SIZE + CELL_SIZE / 2;
// Make sure the column is not interactive during the placement process
// by making all gems non-interactive
activeColumn.gems.forEach(function (gem) {
gem.interactive = false;
});
// Begin the placement process
board.placeColumn(activeColumn, columnPositionX, function (success, placedGems) {
if (success) {
// We don't need to remove activeColumn here as it's already handled in placeColumn
// But we do process matches
processMatches(function () {
spawnNewColumn(); // Spawn a new column after processing matches
});
} else {
// Failed to place column, game over
gameActive = false;
LK.showGameOver();
}
});
}
// Input handlers
game.down = function (x, y, obj) {
if (!gameActive) {
return;
}
// Check if touch is in the bottom 10% of the screen
if (y > 2732 * 0.9) {
// If active column exists and is active, apply fast drop (10x speed)
if (activeColumn && activeColumn.isActive) {
// Set a flag for fast dropping
activeColumn.fastDrop = true;
}
return;
}
// Determine which action to take based on touch position
var boardX = x - board.x;
// If touch is on the board
if (boardX >= 0 && boardX < COLS * CELL_SIZE) {
var touchCol = Math.floor(boardX / CELL_SIZE);
// If touch is on the left side of the active column, move left
if (touchCol < columnPositionX) {
moveColumnLeft();
}
// If touch is on the right side of the active column, move right
else if (touchCol > columnPositionX) {
moveColumnRight();
}
// If touch is on the active column, rotate it
else {
rotateColumn();
}
}
};
game.move = function (x, y, obj) {
// Optional: handle drag for more fluid control
};
game.up = function (x, y, obj) {
// Optional: implement swipe detection for quick drop
if (gameActive && y > game.down.y + 100) {
dropColumn();
}
// Reset fast drop when touch is released
if (activeColumn && activeColumn.isActive) {
activeColumn.fastDrop = false;
}
};
game.update = function () {
// Ensure margin cells are always rendered on top within their container (board.cellContainer)
// This needs to run even when the game isn't "active" if the board is visible before start
if (marginCells && marginCells.length > 0) {
for (var i = 0; i < marginCells.length; i++) {
var cell = marginCells[i];
// Check if cell and its parent container exist before manipulating
if (cell && cell.parent) {
var parentContainer = cell.parent; // Store parent reference
try {
// Bring cell to the top of its parent's display list
// This ensures it draws over other cells in the cellContainer
parentContainer.removeChild(cell);
parentContainer.addChild(cell);
} catch (e) {
// Log error if manipulation fails, but don't crash
console.log("Error updating margin cell rendering:", e);
}
}
}
}
// Only run game logic updates if the game is active
if (!gameActive || !activeColumn || !activeColumn.isActive) {
return;
}
// Update column position using rows
var currentRow = Math.floor(activeColumn.y / CELL_SIZE);
// Apply 10x speed multiplier if fast drop is active
var currentFallSpeed = activeColumn.fastDrop ? fallSpeed * 10 : fallSpeed;
// Calculate the next row position based on the current fall speed
dropTimer += currentFallSpeed; // Increment dropTimer by fallSpeed (or fast fallSpeed)
if (dropTimer >= 1000) {
// Check if dropTimer has reached the threshold for a drop
// 1000ms is the base interval for a row drop
dropTimer = 0;
// Check if the column can move down
var canMoveDown = true;
var nextRow = currentRow + 1;
// Check if any gem in the column would collide with a frozen gem
for (var i = 0; i < activeColumn.gems.length; i++) {
var gemRow = nextRow - i;
if (gemRow >= 0 && gemRow < ROWS) {
var gemKey = gemRow + ',' + columnPositionX;
if (frozenGems[gemKey]) {
canMoveDown = false;
break;
}
}
}
// Check if column reached the bottom
if (nextRow >= ROWS) {
canMoveDown = false;
}
if (canMoveDown) {
activeColumn.y = nextRow * CELL_SIZE; // Move down
} else {
// If the column can't move down anymore, trigger the placement immediately
// but make sure we prevent further direct manipulation
if (activeColumn && activeColumn.isActive) {
dropColumn();
}
return;
}
}
// Check for immediate collisions (for fast drops or manual movements)
// Only perform these checks if the column is still active
if (activeColumn && activeColumn.isActive) {
var checkRow = Math.floor(activeColumn.y / CELL_SIZE);
// Check if any gem in the column is colliding with a frozen gem
for (var i = 0; i < activeColumn.gems.length; i++) {
var gemRow = checkRow - i;
if (gemRow >= 0 && gemRow < ROWS) {
var gemKey = gemRow + ',' + columnPositionX;
if (frozenGems[gemKey]) {
// Move column back up to avoid overlap
activeColumn.y = (gemRow + i) * CELL_SIZE;
dropColumn();
return;
}
}
}
// Check if column reached the bottom
if (checkRow >= ROWS) {
activeColumn.y = (ROWS - 1) * CELL_SIZE;
dropColumn();
return;
}
}
};
// Initialize empty arrays before creating the board
impassableCols = [];
marginCells = [];
// Initialize the board
if (board) {
game.removeChild(board);
}
// Create and display the title image in the center of the screen
var titleImage = LK.getAsset('title', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2732 / 2 - 300
});
var blackBg = LK.getAsset('blackBg', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2732 / 2,
alpha: 0.25
});
game.addChild(blackBg);
game.addChild(titleImage);
startGameText = new Text2('COLUMNI', {
size: 150,
fill: 0xFFFFFF,
align: "center",
font: "Comic Sans MS",
fontWeight: "bold"
});
// Rainbow color array
var rainbowColors = [0xFF0000, 0xFF7F00, 0xFFFF00, 0x00FF00, 0x0000FF, 0x4B0082, 0x8B00FF];
var colorIndex = 0;
// Function to cycle through rainbow colors
function cycleRainbowColors() {
startGameText.tint = rainbowColors[colorIndex];
colorIndex = (colorIndex + 1) % rainbowColors.length;
}
// Set interval to change color every 500ms
LK.setInterval(cycleRainbowColors, 500);
startGameText2 = new Text2('š Tap on the column to š \nš Tap on the bottom of the screen to ā¬ļø\nš Collect gems of the same color in a row', {
size: 90,
align: 'left',
anchorX: 0.5,
anchorY: 0.5,
fill: 0xFFFFFF,
stroke: 0xFFFFFF,
strokeThickness: 5
});
startGameText.y = 2732 / 2 + 100;
startGameText.x = 2048 / 2 - 400;
startGameText.addChild(startGameText2);
startGameText2.y = 250;
startGameText2.x = -400;
game.addChild(startGameText);
// Function to start the game
// Start the game when the title image is clicked
titleImage.down = function (x, y, obj) {
titleImage.destroy();
startGameText.destroy();
blackBg.destroy();
startGame();
};
// createBoard(); // Removed: Board creation is now handled by startGame /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
var Column = Container.expand(function (gemTypes) {
var self = Container.call(this);
self.gems = [];
self.isActive = false;
// Create the three gems that form the column
for (var i = 0; i < 3; i++) {
var type = gemTypes && gemTypes[i] ? gemTypes[i] : null;
var gem = new Gem(type);
gem.y = -i * CELL_SIZE;
self.gems.push(gem);
self.addChild(gem);
}
self.rotate = function () {
if (!self.isActive) {
return;
}
// Move the last gem to a temporary position for animation
var lastGem = self.gems.pop();
var firstGem = self.gems[0];
var middleGem = self.gems[1];
// Animate the rotation
LK.getSound('rotate').play();
// Update the gems array with the new order
self.gems = [lastGem, firstGem, middleGem];
// Update positions to match new order
tween(lastGem, {
y: firstGem.y
}, {
duration: 150
});
tween(firstGem, {
y: middleGem.y
}, {
duration: 150
});
tween(middleGem, {
y: lastGem.y + 2 * CELL_SIZE
}, {
duration: 150,
onFinish: function onFinish() {
// Make sure to only reposition if the column is still active
if (self.isActive) {
// Reset positions after animation
for (var i = 0; i < 3; i++) {
self.gems[i].y = -i * CELL_SIZE;
}
}
}
});
};
self.getTypes = function () {
return self.gems.map(function (gem) {
return gem.type;
});
};
return self;
});
var GameBoard = Container.expand(function () {
var self = Container.call(this);
self.grid = [];
self.cellContainer = self.addChild(new Container());
self.gemContainer = self.addChild(new Container());
// Create background
var boardBg = self.cellContainer.attachAsset('board_bg', {
anchorX: 0.5,
// Center horizontally
anchorY: 0.5 // Center vertically
});
boardBg.x = COLS * CELL_SIZE / 2; // Center the background on the board
boardBg.y = ROWS * CELL_SIZE / 2; // Center the background on the board
// Create grid cells
for (var row = 0; row < ROWS; row++) {
self.grid[row] = [];
for (var col = 0; col < COLS; col++) {
// Create cell background
var cell = LK.getAsset('board_cell', {
anchorX: 0.5,
anchorY: 0.5,
x: col * CELL_SIZE + CELL_SIZE / 2,
y: row * CELL_SIZE + CELL_SIZE / 2,
alpha: 0
});
self.cellContainer.addChild(cell);
self.grid[row][col] = null;
// Create margin cells for impassable columns (first 3 and last 3)
if (col < 3 || col >= COLS - 3) {
// Use 'margin_cell_top' for row 0, 'margin_cell' for others
var marginCellAssetId = row === 0 ? 'margin_cell_top' : row == ROWS - 1 ? 'margin_cell_bottom' : 'margin_cell';
var marginCell = LK.getAsset(marginCellAssetId, {
anchorX: 0.5,
anchorY: 0.5,
x: col * CELL_SIZE + CELL_SIZE / 2,
y: row * CELL_SIZE + CELL_SIZE / 2,
alpha: 1
});
self.cellContainer.addChild(marginCell); // Add visual to board container
// Use the GLOBAL arrays
marginCells.push(marginCell); // Add cell object to global list
// Mark column as impassable globally
if (!impassableCols.includes(col)) {
// Check global list
impassableCols.push(col); // Add to global list
}
// Simulate a frozen cell by adding an entry to frozenGems
frozenGems[row + ',' + col] = true; // frozenGems is already global
}
}
}
self.addGemToGrid = function (gem, row, col) {
if (row < 0 || row >= ROWS || col < 0 || col >= COLS) {
return false;
}
if (self.grid[row][col] !== null) {
return false;
}
gem.x = col * CELL_SIZE + CELL_SIZE / 2;
gem.y = row * CELL_SIZE + CELL_SIZE / 2;
self.gemContainer.addChild(gem);
self.grid[row][col] = gem;
return true;
};
self.removeGemFromGrid = function (row, col) {
if (row < 0 || row >= ROWS || col < 0 || col >= COLS) {
return null;
}
var gem = self.grid[row][col];
if (gem) {
self.grid[row][col] = null;
delete frozenGems[row + ',' + col]; // Remove gem from the dictionary
return gem;
}
return null;
};
self.isColumnFull = function (col) {
return self.grid[0][col] !== null;
};
self.canPlaceColumn = function (col) {
// Check if column is in the impassable zone
if (impassableCols.includes(col)) {
return false;
}
// Check if any of the top 3 rows in the column are occupied
for (var row = 0; row < 3; row++) {
if (col < 0 || col >= COLS) {
return false;
}
if (row < ROWS && self.grid[row][col] !== null) {
return false;
}
}
return true;
};
self.findFirstEmptyRow = function (col) {
for (var row = ROWS - 1; row >= 0; row--) {
if (self.grid[row][col] === null) {
return row;
}
}
return -1; // Column is full
};
self.placeColumn = function (column, col, callback) {
var gems = column.gems;
var placedGems = [];
var firstEmptyRow = self.findFirstEmptyRow(col);
if (firstEmptyRow < 0 || firstEmptyRow < gems.length - 1) {
if (callback) {
callback(false);
}
return;
}
// Calculate the current positions for each gem before removing the column
var currentPositions = [];
for (var i = 0; i < gems.length; i++) {
var gem = gems[i];
if (column.parent) {
var globalPos = column.parent.toGlobal({
x: gem.x + column.x,
y: gem.y + column.y
});
currentPositions.push(globalPos);
}
}
// Remove gems from their current parent before placing them
// This prevents them from being manipulated during the transition
if (column.parent) {
column.parent.removeChild(column);
}
// Place gems from bottom to top
for (var i = 0; i < gems.length; i++) {
var row = firstEmptyRow - i;
if (row >= 0) {
// Use the existing gem instead of creating a new one
var gem = gems[i];
placedGems.push({
gem: gem,
row: row,
col: col
});
// Remove gem from column container first to avoid parent hierarchy issues
if (gem.parent === column) {
column.removeChild(gem);
}
// Use the captured position if available to ensure smooth transition
if (currentPositions[i]) {
var localPos = self.gemContainer.toLocal(currentPositions[i]);
gem.x = localPos.x;
gem.y = localPos.y;
// Animate to final position
tween(gem, {
x: col * CELL_SIZE + CELL_SIZE / 2,
y: row * CELL_SIZE + CELL_SIZE / 2
}, {
duration: 100
});
} else {
// Fallback if position wasn't captured
gem.x = col * CELL_SIZE + CELL_SIZE / 2;
gem.y = row * CELL_SIZE + CELL_SIZE / 2;
}
// Add the gem to the grid
self.gemContainer.addChild(gem);
self.grid[row][col] = gem;
frozenGems[row + ',' + col] = gem; // Store gem in the dictionary
gem.isPlaced = true; // Mark gem as placed to prevent further interaction
}
}
// Make sure drop sound always plays
LK.getSound('drop').stop();
LK.getSound('drop').play();
if (callback) {
callback(true, placedGems);
}
};
self.checkMatches = function () {
var matchedCells = [];
// Check horizontal matches
for (var row = 0; row < ROWS; row++) {
var count = 1;
var type = null;
for (var col = 0; col < COLS; col++) {
var gem = self.grid[row][col];
if (gem && type === gem.type) {
count++;
} else {
if (count >= 3) {
for (var i = 0; i < count; i++) {
matchedCells.push({
row: row,
col: col - 1 - i
});
}
}
count = 1;
type = gem ? gem.type : null;
}
}
if (count >= 3) {
for (var i = 0; i < count; i++) {
matchedCells.push({
row: row,
col: COLS - 1 - i
});
}
}
}
// Check vertical matches
for (var col = 0; col < COLS; col++) {
var count = 1;
var type = null;
for (var row = 0; row < ROWS; row++) {
var gem = self.grid[row][col];
if (gem && type === gem.type) {
count++;
} else {
if (count >= 3) {
for (var i = 0; i < count; i++) {
matchedCells.push({
row: row - 1 - i,
col: col
});
}
}
count = 1;
type = gem ? gem.type : null;
}
}
if (count >= 3) {
for (var i = 0; i < count; i++) {
matchedCells.push({
row: ROWS - 1 - i,
col: col
});
}
}
}
// Check diagonal matches (top-left to bottom-right)
for (var startRow = 0; startRow <= ROWS - 3; startRow++) {
for (var startCol = 0; startCol <= COLS - 3; startCol++) {
var count = 1;
var type = null;
// Check diagonal starting at [startRow, startCol]
for (var i = 0; i < Math.min(ROWS - startRow, COLS - startCol); i++) {
var gem = self.grid[startRow + i][startCol + i];
if (gem && type === gem.type) {
count++;
} else {
if (count >= 3) {
// Add matching gems to matchedCells
for (var j = 0; j < count; j++) {
matchedCells.push({
row: startRow + i - 1 - j,
col: startCol + i - 1 - j
});
}
}
count = 1;
type = gem ? gem.type : null;
}
}
// Check if there's a match at the end of the diagonal
if (count >= 3) {
var endI = Math.min(ROWS - startRow, COLS - startCol);
for (var j = 0; j < count; j++) {
matchedCells.push({
row: startRow + endI - 1 - j,
col: startCol + endI - 1 - j
});
}
}
}
}
// Check diagonal matches (top-right to bottom-left)
for (var startRow = 0; startRow <= ROWS - 3; startRow++) {
for (var startCol = COLS - 1; startCol >= 2; startCol--) {
var count = 1;
var type = null;
// Check diagonal starting at [startRow, startCol]
for (var i = 0; i < Math.min(ROWS - startRow, startCol + 1); i++) {
var gem = self.grid[startRow + i][startCol - i];
if (gem && type === gem.type) {
count++;
} else {
if (count >= 3) {
// Add matching gems to matchedCells
for (var j = 0; j < count; j++) {
matchedCells.push({
row: startRow + i - 1 - j,
col: startCol - i + 1 + j
});
}
}
count = 1;
type = gem ? gem.type : null;
}
}
// Check if there's a match at the end of the diagonal
if (count >= 3) {
var endI = Math.min(ROWS - startRow, startCol + 1);
for (var j = 0; j < count; j++) {
matchedCells.push({
row: startRow + endI - 1 - j,
col: startCol - endI + 1 + j
});
}
}
}
}
// Remove duplicates from matchedCells
var uniqueMatches = [];
var matchMap = {};
for (var i = 0; i < matchedCells.length; i++) {
var cell = matchedCells[i];
var key = cell.row + "," + cell.col;
if (!matchMap[key]) {
matchMap[key] = true;
uniqueMatches.push(cell);
}
}
return uniqueMatches;
};
self.removeMatches = function (matches, callback) {
if (matches.length === 0) {
if (callback) {
callback(0);
}
return;
}
var remainingAnimations = matches.length;
var points = matches.length * 10;
// Make sure to stop any previously playing match sound to avoid conflicts
LK.getSound('match').stop();
// Play the match sound fresh
LK.getSound('match').play();
// Flash screen effect for matches - ensure it always triggers
LK.effects.flashScreen(0xFFFFFF, 300);
matches.forEach(function (match) {
var gem = self.grid[match.row][match.col];
if (gem) {
self.grid[match.row][match.col] = null;
delete frozenGems[match.row + ',' + match.col]; // Remove gem from the dictionary
gem.highlight();
LK.setTimeout(function () {
gem.animateMatch(function () {
self.gemContainer.removeChild(gem);
remainingAnimations--;
if (remainingAnimations === 0) {
if (callback) {
callback(points);
}
}
});
}, 100);
} else {
remainingAnimations--;
if (remainingAnimations === 0 && callback) {
callback(points);
}
}
});
};
self.applyGravity = function (callback) {
var movedGems = false;
var animationsRunning = 0;
// Iterate bottom to top, right to left
for (var col = 0; col < COLS; col++) {
for (var row = ROWS - 1; row > 0; row--) {
if (self.grid[row][col] === null) {
// Find the first non-null gem above this position
for (var checkRow = row - 1; checkRow >= 0; checkRow--) {
if (self.grid[checkRow][col] !== null) {
var gem = self.grid[checkRow][col];
self.grid[checkRow][col] = null;
self.grid[row][col] = gem;
// Update the frozenGems dictionary
delete frozenGems[checkRow + ',' + col];
frozenGems[row + ',' + col] = gem;
movedGems = true;
animationsRunning++;
gem.animateDrop(row * CELL_SIZE + CELL_SIZE / 2, 0, function () {
animationsRunning--;
if (animationsRunning === 0 && callback) {
callback(movedGems);
}
});
break;
}
}
}
}
}
if (animationsRunning === 0 && callback) {
callback(movedGems);
}
};
return self;
});
var Gem = Container.expand(function (type) {
var self = Container.call(this);
self.type = type || getRandomGemType();
self.isPlaced = false; // Track if gem is part of a placed column
var gemSprite = self.attachAsset('gem_' + self.type, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.95,
scaleY: 0.95
});
self.setType = function (newType) {
self.type = newType;
self.removeChildren();
gemSprite = self.attachAsset('gem_' + newType, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.95,
scaleY: 0.95
});
};
self.highlight = function () {
tween(gemSprite, {
scaleX: 1.1,
scaleY: 1.1
}, {
duration: 200,
easing: tween.easeOut
});
};
self.unhighlight = function () {
tween(gemSprite, {
scaleX: 0.95,
scaleY: 0.95
}, {
duration: 200,
easing: tween.easeOut
});
};
self.animateMatch = function (callback) {
tween(gemSprite, {
alpha: 0,
scaleX: 0.2,
scaleY: 0.2
}, {
duration: 300,
easing: tween.easeIn,
onFinish: function onFinish() {
if (callback) {
callback();
}
}
});
};
self.animateDrop = function (targetY, delay, callback) {
// Mark gem as being animated to prevent user interaction during animation
self.isAnimating = true;
tween(self, {
y: targetY
}, {
duration: 300 + delay,
easing: tween.bounceOut,
onFinish: function onFinish() {
self.isAnimating = false;
if (callback) {
callback();
}
}
});
};
// Add down handler to prevent interaction during transition
self.down = function (x, y, obj) {
// Prevent interaction if gem is being animated or is already placed
if (self.isAnimating || self.isPlaced) {
return;
}
// Default behavior for interaction with gems
// (This can be expanded if you want gems to be interactive in a certain way)
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x111122
});
/****
* Game Code
****/
// Constants
var CELL_SIZE = 120;
var COLS = 19;
var ROWS = Math.floor(2732 / CELL_SIZE);
//var ROWS = 25;
var GEM_TYPES = ['red', 'blue', 'green', 'yellow', 'purple'];
var DROP_INTERVAL_START = 60; // ms
var DROP_INTERVAL_MIN = 60; // ms
var DROP_DELTA = 20;
var LEVEL_THRESHOLD = 50; // Points needed to level up
// Game state
var board;
var activeColumn;
var nextColumn;
var dropInterval;
var dropTimer = 0;
var fallSpeed = DROP_INTERVAL_START;
var gameActive = false;
var score = 0;
var level = 1;
var nextColumnPreview;
var columnPositionX = 0;
// Dictionary to track frozen gems
var frozenGems = {};
// Arrays to track impassable columns and their visual representation
var impassableCols = []; // Stores column indices that are impassable
var marginCells = []; // Stores the Container objects for margin cell visuals
// UI elements
var scoreText;
var levelText;
var highScoreText;
function getRandomGemType() {
var index = Math.floor(Math.random() * GEM_TYPES.length);
return GEM_TYPES[index];
}
function setupUI() {
// Score text
scoreText = new Text2('SCORE: 0', {
size: 50,
fill: 0xFFFFFF,
// Changed to white
stroke: 0xFFFFFF,
// Add white stroke
strokeThickness: 5 // Thicker stroke for better visibility
});
scoreText.anchor.set(0.5, 0);
scoreText.y = 50;
LK.gui.top.addChild(scoreText);
// Level text
levelText = new Text2('LEVEL: 1', {
size: 50,
fill: 0xFFFFFF,
// Changed to white
stroke: 0xFFFFFF,
// Add white stroke
strokeThickness: 5 // Thicker stroke for better visibility
});
levelText.anchor.set(0.5, 0);
levelText.y = 100;
LK.gui.top.addChild(levelText);
// High score text
var highScore = storage.highScore || 0;
highScoreText = new Text2('BEST: ' + highScore, {
size: 40,
fill: 0x00FF00,
// Changed to white
stroke: 0xFFFFFF,
// Add white stroke
strokeThickness: 5 // Thicker stroke for better visibility
});
highScoreText.anchor.set(0.5, 0);
highScoreText.y = 150;
LK.gui.top.addChild(highScoreText);
}
function updateScore(points) {
score += points;
scoreText.setText('SCORE: ' + score);
// Check if player leveled up
var newLevel = Math.floor(score / LEVEL_THRESHOLD) + 1;
if (newLevel > level) {
level = newLevel;
levelText.setText('LEVEL: ' + level);
// Increase game speed
fallSpeed = Math.max(DROP_INTERVAL_MIN, DROP_INTERVAL_START - (level - 1) * DROP_DELTA); // Adjust fallSpeed based on the current level
// Play level up sound
LK.getSound('levelup').stop(); // Stop first to ensure it plays fresh
LK.getSound('levelup').play();
// Flash effect for level up
LK.effects.flashScreen(0x00FFFF, 500);
}
// Update high score if needed
var highScore = storage.highScore || 0;
if (score > highScore) {
storage.highScore = score;
highScoreText.setText('BEST: ' + score);
}
}
function createBoard() {
// Clear previous margin cells visually and from the array
marginCells.forEach(function (cell) {
if (cell && cell.parent) {
cell.parent.removeChild(cell);
}
// Optionally destroy: cell.destroy();
});
marginCells = []; // Reset the global array
// Reset impassable columns list
impassableCols = [];
// Remove old board instance if it exists
if (board && board.parent) {
board.parent.removeChild(board);
// Optionally destroy: board.destroy();
}
// Create and position the new board
board = new GameBoard();
board.x = (2048 - COLS * CELL_SIZE) / 2;
board.y = (2732 - ROWS * CELL_SIZE) / 2;
game.addChild(board);
}
function createNextColumnPreview() {
nextColumnPreview = new Container();
nextColumnPreview.x = 2048 - 120;
nextColumnPreview.y = 200;
// Create the "NEXT" label
var previewLabel = new Text2('NEXT', {
size: 80,
fill: 0xFFFF00,
stroke: 0xFFFFFF,
// White stroke
strokeThickness: 5 // Thicker stroke for better visibility
});
previewLabel.anchor.set(0.5, 0);
previewLabel.y = -80; // Position at the top of the container
nextColumnPreview.addChild(previewLabel);
game.addChild(nextColumnPreview);
}
function updateNextColumnPreview() {
// Clear previous preview
for (var i = nextColumnPreview.children.length - 1; i > 0; i--) {
nextColumnPreview.removeChild(nextColumnPreview.children[i]);
}
// Add new gems to preview in the correct order
if (nextColumn) {
// Loop through gems in reverse order to match the visual appearance in the game
for (var i = nextColumn.gems.length - 1; i >= 0; i--) {
var previewGem = new Gem(nextColumn.gems[i].type);
previewGem.x = 0;
// Position from top to bottom in the preview
previewGem.y = 80 + (nextColumn.gems.length - 1 - i) * CELL_SIZE;
previewGem.scale.set(0.8);
nextColumnPreview.addChild(previewGem);
}
}
}
function createNewColumn() {
var column = new Column();
column.isActive = true;
column.fastDrop = false; // Initialize fastDrop property for speed control
return column;
}
function startGame() {
// Reset game state
score = 0;
level = 1;
fallSpeed = DROP_INTERVAL_START; // Initialize fallSpeed with the starting drop interval
gameActive = true;
dropTimer = 0;
// Clear frozen gems dictionary
frozenGems = {};
// --- Start Cleanup ---
// Remove title screen elements safely
if (typeof titleImage !== 'undefined' && titleImage && titleImage.parent) {
titleImage.destroy(); // Use destroy to free resources
titleImage = null; // Nullify reference
}
if (typeof startGameText !== 'undefined' && startGameText && startGameText.parent) {
startGameText.destroy();
startGameText = null;
}
if (typeof blackBg !== 'undefined' && blackBg && blackBg.parent) {
blackBg.destroy();
blackBg = null;
}
// --- End Cleanup ---
// Create the game board (handles its own cleanup now)
createBoard();
// Create the preview for the next column
if (nextColumnPreview && nextColumnPreview.parent) {
// Check parent before removing
game.removeChild(nextColumnPreview);
// nextColumnPreview.destroy(); // Consider if needed
}
createNextColumnPreview();
// Set up UI (ensure setupUI handles potential existing UI elements if necessary)
setupUI();
// Create initial columns
activeColumn = createNewColumn();
nextColumn = createNewColumn();
updateNextColumnPreview();
// Position active column (avoid impassable columns)
columnPositionX = Math.floor(COLS / 2);
// Ensure starting position is not in an impassable column
while (impassableCols.includes(columnPositionX)) {
columnPositionX = Math.min(COLS - 4, Math.max(3, columnPositionX));
}
activeColumn.x = board.x + columnPositionX * CELL_SIZE + CELL_SIZE / 2;
activeColumn.y = 0;
game.addChild(activeColumn);
// Start music
LK.playMusic('bgmusic', {
fade: {
start: 0,
end: 0.4,
duration: 1000
}
});
}
function processMatches(callback) {
var matches = board.checkMatches();
if (matches.length > 0) {
// Ensure sound and visual feedback triggers for every match
board.removeMatches(matches, function (points) {
updateScore(points);
board.applyGravity(function (movedGems) {
// Check for chain reactions
if (movedGems) {
// For chain reactions, process additional matches
processMatches(callback);
} else {
if (callback) {
callback();
}
}
});
});
} else {
if (callback) {
callback();
}
}
}
function spawnNewColumn() {
activeColumn = nextColumn;
nextColumn = createNewColumn();
updateNextColumnPreview();
columnPositionX = Math.floor(COLS / 2);
// Ensure new column doesn't spawn in an impassable column
while (impassableCols.includes(columnPositionX)) {
columnPositionX = Math.min(COLS - 4, Math.max(3, columnPositionX));
}
activeColumn.x = board.x + columnPositionX * CELL_SIZE + CELL_SIZE / 2;
activeColumn.y = 0;
game.addChild(activeColumn);
// Check if game over
if (!board.canPlaceColumn(columnPositionX)) {
gameActive = false;
LK.showGameOver();
} else {
activeColumn.isActive = true; // Reactivate the new column
}
}
function moveColumnLeft() {
if (!gameActive || !activeColumn || !activeColumn.isActive) {
return;
}
if (columnPositionX > 0) {
// Check if there's a collision in the new position
var canMove = true;
var currentRow = Math.floor(activeColumn.y / CELL_SIZE);
for (var i = 0; i < activeColumn.gems.length; i++) {
var gemRow = currentRow - i;
if (gemRow >= 0 && gemRow < ROWS) {
var gemKey = gemRow + ',' + (columnPositionX - 1);
if (frozenGems[gemKey]) {
canMove = false;
break;
}
}
}
if (canMove) {
columnPositionX--;
activeColumn.x = board.x + columnPositionX * CELL_SIZE + CELL_SIZE / 2;
}
}
}
function moveColumnRight() {
if (!gameActive || !activeColumn || !activeColumn.isActive) {
return;
}
if (columnPositionX < COLS - 1) {
// Check if there's a collision in the new position
var canMove = true;
var currentRow = Math.floor(activeColumn.y / CELL_SIZE);
for (var i = 0; i < activeColumn.gems.length; i++) {
var gemRow = currentRow - i;
if (gemRow >= 0 && gemRow < ROWS) {
var gemKey = gemRow + ',' + (columnPositionX + 1);
if (frozenGems[gemKey]) {
canMove = false;
break;
}
}
}
if (canMove) {
columnPositionX++;
activeColumn.x = board.x + columnPositionX * CELL_SIZE + CELL_SIZE / 2;
}
}
}
function rotateColumn() {
if (!gameActive || !activeColumn) {
return;
}
activeColumn.rotate();
}
function dropColumn() {
if (!gameActive || !activeColumn || !activeColumn.isActive) {
return;
}
// First, make it inactive to prevent further user interaction
activeColumn.isActive = false;
// Update column position
activeColumn.x = board.x + columnPositionX * CELL_SIZE + CELL_SIZE / 2;
// Make sure the column is not interactive during the placement process
// by making all gems non-interactive
activeColumn.gems.forEach(function (gem) {
gem.interactive = false;
});
// Begin the placement process
board.placeColumn(activeColumn, columnPositionX, function (success, placedGems) {
if (success) {
// We don't need to remove activeColumn here as it's already handled in placeColumn
// But we do process matches
processMatches(function () {
spawnNewColumn(); // Spawn a new column after processing matches
});
} else {
// Failed to place column, game over
gameActive = false;
LK.showGameOver();
}
});
}
// Input handlers
game.down = function (x, y, obj) {
if (!gameActive) {
return;
}
// Check if touch is in the bottom 10% of the screen
if (y > 2732 * 0.9) {
// If active column exists and is active, apply fast drop (10x speed)
if (activeColumn && activeColumn.isActive) {
// Set a flag for fast dropping
activeColumn.fastDrop = true;
}
return;
}
// Determine which action to take based on touch position
var boardX = x - board.x;
// If touch is on the board
if (boardX >= 0 && boardX < COLS * CELL_SIZE) {
var touchCol = Math.floor(boardX / CELL_SIZE);
// If touch is on the left side of the active column, move left
if (touchCol < columnPositionX) {
moveColumnLeft();
}
// If touch is on the right side of the active column, move right
else if (touchCol > columnPositionX) {
moveColumnRight();
}
// If touch is on the active column, rotate it
else {
rotateColumn();
}
}
};
game.move = function (x, y, obj) {
// Optional: handle drag for more fluid control
};
game.up = function (x, y, obj) {
// Optional: implement swipe detection for quick drop
if (gameActive && y > game.down.y + 100) {
dropColumn();
}
// Reset fast drop when touch is released
if (activeColumn && activeColumn.isActive) {
activeColumn.fastDrop = false;
}
};
game.update = function () {
// Ensure margin cells are always rendered on top within their container (board.cellContainer)
// This needs to run even when the game isn't "active" if the board is visible before start
if (marginCells && marginCells.length > 0) {
for (var i = 0; i < marginCells.length; i++) {
var cell = marginCells[i];
// Check if cell and its parent container exist before manipulating
if (cell && cell.parent) {
var parentContainer = cell.parent; // Store parent reference
try {
// Bring cell to the top of its parent's display list
// This ensures it draws over other cells in the cellContainer
parentContainer.removeChild(cell);
parentContainer.addChild(cell);
} catch (e) {
// Log error if manipulation fails, but don't crash
console.log("Error updating margin cell rendering:", e);
}
}
}
}
// Only run game logic updates if the game is active
if (!gameActive || !activeColumn || !activeColumn.isActive) {
return;
}
// Update column position using rows
var currentRow = Math.floor(activeColumn.y / CELL_SIZE);
// Apply 10x speed multiplier if fast drop is active
var currentFallSpeed = activeColumn.fastDrop ? fallSpeed * 10 : fallSpeed;
// Calculate the next row position based on the current fall speed
dropTimer += currentFallSpeed; // Increment dropTimer by fallSpeed (or fast fallSpeed)
if (dropTimer >= 1000) {
// Check if dropTimer has reached the threshold for a drop
// 1000ms is the base interval for a row drop
dropTimer = 0;
// Check if the column can move down
var canMoveDown = true;
var nextRow = currentRow + 1;
// Check if any gem in the column would collide with a frozen gem
for (var i = 0; i < activeColumn.gems.length; i++) {
var gemRow = nextRow - i;
if (gemRow >= 0 && gemRow < ROWS) {
var gemKey = gemRow + ',' + columnPositionX;
if (frozenGems[gemKey]) {
canMoveDown = false;
break;
}
}
}
// Check if column reached the bottom
if (nextRow >= ROWS) {
canMoveDown = false;
}
if (canMoveDown) {
activeColumn.y = nextRow * CELL_SIZE; // Move down
} else {
// If the column can't move down anymore, trigger the placement immediately
// but make sure we prevent further direct manipulation
if (activeColumn && activeColumn.isActive) {
dropColumn();
}
return;
}
}
// Check for immediate collisions (for fast drops or manual movements)
// Only perform these checks if the column is still active
if (activeColumn && activeColumn.isActive) {
var checkRow = Math.floor(activeColumn.y / CELL_SIZE);
// Check if any gem in the column is colliding with a frozen gem
for (var i = 0; i < activeColumn.gems.length; i++) {
var gemRow = checkRow - i;
if (gemRow >= 0 && gemRow < ROWS) {
var gemKey = gemRow + ',' + columnPositionX;
if (frozenGems[gemKey]) {
// Move column back up to avoid overlap
activeColumn.y = (gemRow + i) * CELL_SIZE;
dropColumn();
return;
}
}
}
// Check if column reached the bottom
if (checkRow >= ROWS) {
activeColumn.y = (ROWS - 1) * CELL_SIZE;
dropColumn();
return;
}
}
};
// Initialize empty arrays before creating the board
impassableCols = [];
marginCells = [];
// Initialize the board
if (board) {
game.removeChild(board);
}
// Create and display the title image in the center of the screen
var titleImage = LK.getAsset('title', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2732 / 2 - 300
});
var blackBg = LK.getAsset('blackBg', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2732 / 2,
alpha: 0.25
});
game.addChild(blackBg);
game.addChild(titleImage);
startGameText = new Text2('COLUMNI', {
size: 150,
fill: 0xFFFFFF,
align: "center",
font: "Comic Sans MS",
fontWeight: "bold"
});
// Rainbow color array
var rainbowColors = [0xFF0000, 0xFF7F00, 0xFFFF00, 0x00FF00, 0x0000FF, 0x4B0082, 0x8B00FF];
var colorIndex = 0;
// Function to cycle through rainbow colors
function cycleRainbowColors() {
startGameText.tint = rainbowColors[colorIndex];
colorIndex = (colorIndex + 1) % rainbowColors.length;
}
// Set interval to change color every 500ms
LK.setInterval(cycleRainbowColors, 500);
startGameText2 = new Text2('š Tap on the column to š \nš Tap on the bottom of the screen to ā¬ļø\nš Collect gems of the same color in a row', {
size: 90,
align: 'left',
anchorX: 0.5,
anchorY: 0.5,
fill: 0xFFFFFF,
stroke: 0xFFFFFF,
strokeThickness: 5
});
startGameText.y = 2732 / 2 + 100;
startGameText.x = 2048 / 2 - 400;
startGameText.addChild(startGameText2);
startGameText2.y = 250;
startGameText2.x = -400;
game.addChild(startGameText);
// Function to start the game
// Start the game when the title image is clicked
titleImage.down = function (x, y, obj) {
titleImage.destroy();
startGameText.destroy();
blackBg.destroy();
startGame();
};
// createBoard(); // Removed: Board creation is now handled by startGame