/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ // Represents a placed bomb var Bomb = Container.expand(function (col, row) { var self = Container.call(this); var graphics = self.attachAsset('bomb', { anchorX: 0.5, anchorY: 0.5 }); self.col = col; self.row = row; self.x = getWorldCoords(col, row).x; self.y = getWorldCoords(col, row).y; self.isBomb = true; // Identifier self.pulsatingTween = null; // Initialize tween variable // Define the pulsation functions function scaleDown() { // Prevent tweening if the bomb is already destroyed if (self.destroyed) { return; } self.pulsatingTween = tween(self.scale, { x: 1.0, y: 1.0 }, { duration: 200, ease: tween.easeInOut, onFinish: scaleUp // When shrinking finishes, start growing }); } function scaleUp() { // Prevent tweening if the bomb is already destroyed if (self.destroyed) { return; } self.pulsatingTween = tween(self.scale, { x: 1.1, y: 1.1 }, { duration: 200, ease: tween.easeInOut, onFinish: scaleDown // When growing finishes, start shrinking }); } scaleUp(); // Start the explosion timer var explosionTimer = LK.setTimeout(function () { explodeBomb(self); }, BOMB_TIMER); // Public method to trigger explosion early (chain reaction) self.triggerExplosion = function () { LK.clearTimeout(explosionTimer); explodeBomb(self); }; // Override destroy to clear the timer var baseDestroy = self.destroy; self.destroy = function () { LK.clearTimeout(explosionTimer); // Stop the specific pulsating tween instance if (self.pulsatingTween) { self.pulsatingTween.kill(); // Use the kill() method on the stored tween instance self.pulsatingTween = null; // Clear the reference } // Make sure the grid cell is marked empty when bomb is destroyed (e.g., by explosion) if (grid[self.col] && grid[self.col][self.row] === CELL_TYPE.BOMB) { setCell(self.col, self.row, CELL_TYPE.EMPTY); } // Remove from active bombs list in game scope var index = activeBombs.indexOf(self); if (index !== -1) { activeBombs.splice(index, 1); } // Mark as destroyed so ongoing tween callbacks won't try to run self.destroyed = true; if (baseDestroy) { baseDestroy.call(self); } // Call original destroy if exists }; return self; }); // Represents an enemy that moves between grid cells var Enemy = Container.expand(function (startCol, startRow) { var self = Container.call(this); var graphics = self.attachAsset('enemy', { anchorX: 0.5, anchorY: 0.5 }); self.col = startCol; self.row = startRow; self.isEnemy = true; // Identifier self.isMoving = false; // Flag to track movement animation state self.moveDelay = 500 + Math.random() * 500; // Reduced delay between moves (0.5-1s) self.lastMoveTime = Date.now(); // Track when last moved // Snap to initial grid position var initialPos = getWorldCoords(self.col, self.row); self.x = initialPos.x; self.y = initialPos.y; // Try to move, prioritizing adjacent player cell self.tryRandomMove = function () { if (self.isMoving || !player || player.isMoving) { // Don't move if animating or player doesn't exist/is moving return false; } // Check if player is adjacent var playerCol = player.col; var playerRow = player.row; var dx = playerCol - self.col; var dy = playerRow - self.row; // Check for adjacency (Manhattan distance of 1) if (Math.abs(dx) + Math.abs(dy) === 1) { // Player is adjacent. Try to move to the player's cell. var targetCol = playerCol; var targetRow = playerRow; var moveDx = dx; // Direction towards player // Check if the player's cell is walkable (it might not be if another enemy is there temporarily, unlikely but defensive check) if (isWalkable(targetCol, targetRow)) { // Move towards the player self.moveTo(targetCol, targetRow, moveDx); return true; // Moved towards player } // If player's cell is not walkable, fall through to random move } // Player is not adjacent, or adjacent cell is blocked: Try to move in a random direction var directions = [{ dx: 0, dy: -1 }, // up { dx: 1, dy: 0 }, // right { dx: 0, dy: 1 }, // down { dx: -1, dy: 0 } // left ]; // Shuffle directions for randomness for (var i = directions.length - 1; i > 0; i--) { var j = Math.floor(Math.random() * (i + 1)); var temp = directions[i]; directions[i] = directions[j]; directions[j] = temp; } // Try each random direction until a valid move is found for (var i = 0; i < directions.length; i++) { var newCol = self.col + directions[i].dx; var newRow = self.row + directions[i].dy; if (isWalkable(newCol, newRow)) { // Found valid random move self.moveTo(newCol, newRow, directions[i].dx); return true; } } return false; // No valid moves found }; // Move to a new cell self.moveTo = function (newCol, newRow, dx) { self.isMoving = true; self.col = newCol; self.row = newRow; var targetPos = getWorldCoords(newCol, newRow); // Flip sprite based on movement direction if (dx < 0) { graphics.scale.x = -1; } else if (dx > 0) { graphics.scale.x = 1; } // Select a random easing function var randomEasing = enemyEasings[Math.floor(Math.random() * enemyEasings.length)]; // Animate movement tween(self, { x: targetPos.x, y: targetPos.y }, { duration: 500, // Make movement animation faster easing: randomEasing, // Use the randomly selected easing function onFinish: function onFinish() { self.isMoving = false; self.lastMoveTime = Date.now(); } }); return true; }; return self; }); // Spawn enemies at random empty cells // Represents a single explosion particle/cell var Explosion = Container.expand(function (col, row) { var self = Container.call(this); var graphics = self.attachAsset('explosion', { anchorX: 0.5, anchorY: 0.5 }); self.col = col; self.row = row; self.x = getWorldCoords(col, row).x; self.y = getWorldCoords(col, row).y; self.isExplosion = true; // Identifier for collision checks // Start scaled down for explosion effect self.scale.set(0); // Animate the explosion appearing tween(self.scale, { x: 1, y: 1 }, { duration: 150, ease: tween.easeInOut }); // Fade out and destroy after duration var timer = LK.setTimeout(function () { self.destroy(); // Destroy handled by Game update loop removal }, EXPLOSION_DURATION); // Override destroy to clear the timer var baseDestroy = self.destroy; self.destroy = function () { LK.clearTimeout(timer); if (baseDestroy) { baseDestroy.call(self); } // Call original destroy if exists }; return self; }); // Represents the player character var Player = Container.expand(function (startCol, startRow) { var self = Container.call(this); var graphics = self.attachAsset('player', { anchorX: 0.5, anchorY: 0.5 }); self.col = startCol; self.row = startRow; self.isPlayer = true; // Identifier // Snap to initial grid position var initialPos = getWorldCoords(self.col, self.row); self.x = initialPos.x; self.y = initialPos.y; self.isMoving = false; // Flag to track movement animation state // Empty placeholder for movement - functionality removed self.moveTo = function (newCol, newRow) { // Movement functionality removed return false; // Movement disabled }; // Empty placeholder for bomb placement - functionality removed self.placeBomb = function () { // Bomb placement functionality removed }; }); // Represents a power-up item dropped on the grid var PowerUp = Container.expand(function (col, row, type) { var self = Container.call(this); self.col = col; self.row = row; self.type = type; // 'extra_bomb' or 'bigger_explosion' self.isPowerUp = true; // Identifier var assetId = type === 'extra_bomb' ? 'powerup_bomb' : 'powerup_range'; var graphics = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); var coords = getWorldCoords(col, row); self.x = coords.x; self.y = coords.y; // Simple bobbing animation var startY = self.y; var bobTween; function bobUp() { if (self.destroyed) { return; } bobTween = tween(self, { y: startY - 10 }, { duration: 500, ease: tween.easeInOutSine, onFinish: bobDown }); } function bobDown() { if (self.destroyed) { return; } bobTween = tween(self, { y: startY }, { duration: 500, ease: tween.easeInOutSine, onFinish: bobUp }); } bobUp(); // Start animation // Override destroy to stop tween var baseDestroy = self.destroy; self.destroy = function () { self.destroyed = true; // Flag to stop tween callbacks if (bobTween) { bobTween.kill(); bobTween = null; } // Remove from active power-ups list var index = activePowerUps.indexOf(self); if (index !== -1) { activePowerUps.splice(index, 1); } if (baseDestroy) { baseDestroy.call(self); } }; return self; }); // Class to display a floating message when picking up a powerup var PowerUpMessage = Container.expand(function (message, x, y) { var self = Container.call(this); // Create text with the powerup message var messageTxt = new Text2(message, { size: 60, fill: 0xFFFF00 // Yellow color for visibility }); messageTxt.anchor.set(0.5, 0.5); self.addChild(messageTxt); // Position at the specified location self.x = x; self.y = y; // Animate the message: move up and fade out function animateAndDestroy() { tween(self, { y: self.y - 100, // Move up alpha: 0 // Fade out }, { duration: 1000, // Animation lasts 1 second ease: tween.easeOutQuad, onFinish: function onFinish() { // Remove from parent when animation completes self.destroy(); } }); } // Start the animation animateAndDestroy(); return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: Math.floor(Math.random() * 0xFFFFFF) // Random background color }); /**** * Game Code ****/ // Let's define the shapes we'll use: // Engine will implicitly create assets based on usage below. // --- Constants --- // Placeholder ID // Placeholder ID // Placeholder ID for the logo var GRID_COLS = 15; // Ensure odd number for wall placement var GRID_ROWS = 20; // Remove the last row var CELL_SIZE = 128; var GRID_WIDTH = GRID_COLS * CELL_SIZE; var GRID_HEIGHT = GRID_ROWS * CELL_SIZE; var GRID_OFFSET_X = (2048 - GRID_WIDTH) / 2; var GRID_OFFSET_Y = 200; // Position grid 200 pixels from the top var CELL_TYPE = { EMPTY: 0, DESTRUCTIBLE: 1, INDESTRUCTIBLE: 2, BOMB: 3, // Cell temporarily occupied by a bomb EXPLOSION: 4 // Cell temporarily occupied by explosion }; var BOMB_TIMER = 2500; // Milliseconds before bomb explodes var EXPLOSION_DURATION = 400; // Milliseconds explosion visuals last var BASE_BOMB_RANGE = 2; // Default explosion range var BASE_MAX_BOMBS = 1; // Default max bombs var BOMB_RANGE; // Current explosion range (can be modified by powerups) var MAX_BOMBS; // Current max bombs (can be modified by powerups) var POWERUP_CHANCE = 0.3; // 30% chance to drop a powerup from a brick var BRICK_DENSITY = 0.2; // Further reduced probability of bricks to make more room for enemies // Array of tween easing functions for enemy movement var enemyEasings = [ // Basic easing functions tween.linear, // Quad easing tween.easeIn, tween.easeOut, tween.easeInOut, // Cubic easing tween.cubicIn, tween.cubicOut, tween.cubicInOut, // Quartic easing tween.quarticIn, tween.quarticOut, tween.quarticInOut, // Quintic easing tween.quinticIn, tween.quinticOut, tween.quinticInOut, // Sine easing tween.sineIn, tween.sineOut, tween.sineInOut, // Exponential easing tween.expoIn, tween.expoOut, tween.expoInOut, // Circular easing tween.easeInCirc, tween.easeOutCirc, tween.easeInOutCirc, // Include previously commented out elastic easing which can create interesting movements tween.elasticIn, tween.elasticOut, tween.elasticInOut]; // --- Game State Variables --- var grid = []; // 2D array holding CELL_TYPE for each cell var gridSprites = []; // 2D array holding visual sprites for walls/bricks var player; var activeBombs = []; var activeExplosions = []; // Array to hold active Explosion objects var enemies = []; // Array to hold all enemy instances var score = 0; var scoreTxt; var isGameOver = false; var ENEMY_COUNT = 6; // Increased number of enemies to spawn var startScreenContainer = null; // Container for the start screen elements var gameReady = false; // Flag to track if the game has started // --- Helper Functions --- function getWorldCoords(col, row) { var x = GRID_OFFSET_X + col * CELL_SIZE + CELL_SIZE / 2; var y = GRID_OFFSET_Y + row * CELL_SIZE + CELL_SIZE / 2; return { x: x, y: y }; } // Get the type of a cell, handling out-of-bounds function getCellType(col, row) { if (col < 0 || col >= GRID_COLS || row < 0 || row >= GRID_ROWS) { return CELL_TYPE.INDESTRUCTIBLE; // Treat out of bounds as walls } return grid[col][row]; } // Set the type of a cell function setCell(col, row, type) { if (col >= 0 && col < GRID_COLS && row >= 0 && row < GRID_ROWS) { grid[col][row] = type; } } // Check if a cell is valid and empty for movement // Check if a cell is occupied by an enemy function isEnemyOccupied(col, row) { for (var i = 0; i < enemies.length; i++) { if (enemies[i].col === col && enemies[i].row === row) { return true; } } return false; } // Check if a cell is valid and empty for movement (for both player and enemies) function isWalkable(col, row) { var cellType = getCellType(col, row); // A cell is walkable only if it's currently empty AND not occupied by another enemy. return cellType === CELL_TYPE.EMPTY && !isEnemyOccupied(col, row); } // --- Grid Generation --- function generateGrid() { grid = []; gridSprites = []; // Clear previous sprites if any game.children.forEach(function (child) { if (child.isGridElement) { // Add a flag to identify grid sprites child.destroy(); } }); game.removeChildren(); // Clear all children before regenerating // Add floor tiles everywhere first for (var c = 0; c < GRID_COLS; c++) { for (var r = 0; r < GRID_ROWS; r++) { var floorTile = LK.getAsset('floor', { anchorX: 0.0, anchorY: 0.0, alpha: 0 }); floorTile.x = GRID_OFFSET_X + c * CELL_SIZE; floorTile.y = GRID_OFFSET_Y + r * CELL_SIZE; floorTile.isGridElement = true; game.addChild(floorTile); } } for (var c = 0; c < GRID_COLS; c++) { grid[c] = []; gridSprites[c] = []; for (var r = 0; r < GRID_ROWS; r++) { var cellType; var sprite = null; // Place indestructible walls around border and in a checkerboard pattern inside if (c === 0 || c === GRID_COLS - 1 || r === 0 || r === GRID_ROWS - 1 || c % 2 === 0 && r % 2 === 0) { cellType = CELL_TYPE.INDESTRUCTIBLE; sprite = LK.getAsset('wall', { anchorX: 0.0, anchorY: 0.0 }); } else { // Place destructible bricks randomly, ensuring player start area is clear if (c <= 2 && r <= 2 || c >= GRID_COLS - 3 && r >= GRID_ROWS - 3) { // Clear corners for potential spawns cellType = CELL_TYPE.EMPTY; } else if (Math.random() < BRICK_DENSITY) { cellType = CELL_TYPE.DESTRUCTIBLE; sprite = LK.getAsset('brick', { anchorX: 0.0, anchorY: 0.0 }); } else { cellType = CELL_TYPE.EMPTY; } } grid[c][r] = cellType; if (sprite) { sprite.x = GRID_OFFSET_X + c * CELL_SIZE; sprite.y = GRID_OFFSET_Y + r * CELL_SIZE; sprite.isGridElement = true; // Mark as part of the grid visuals game.addChild(sprite); gridSprites[c][r] = sprite; } else { gridSprites[c][r] = null; } } } // Ensure player start is clear setCell(1, 1, CELL_TYPE.EMPTY); setCell(1, 2, CELL_TYPE.EMPTY); setCell(2, 1, CELL_TYPE.EMPTY); if (gridSprites[1] && gridSprites[1][1]) { var _gridSprites$1$; (_gridSprites$1$ = gridSprites[1][1]) === null || _gridSprites$1$ === void 0 || _gridSprites$1$.destroy(); gridSprites[1][1] = null; } if (gridSprites[1] && gridSprites[1][2]) { var _gridSprites$1$2; (_gridSprites$1$2 = gridSprites[1][2]) === null || _gridSprites$1$2 === void 0 || _gridSprites$1$2.destroy(); gridSprites[1][2] = null; } if (gridSprites[2] && gridSprites[2][1]) { var _gridSprites$2$; (_gridSprites$2$ = gridSprites[2][1]) === null || _gridSprites$2$ === void 0 || _gridSprites$2$.destroy(); gridSprites[2][1] = null; } } // --- Explosion Handling --- function createExplosion(col, row) { if (col < 0 || col >= GRID_COLS || row < 0 || row >= GRID_ROWS) { return false; } // Out of bounds var cellType = getCellType(col, row); if (cellType === CELL_TYPE.INDESTRUCTIBLE) { return false; // Explosion stops at indestructible walls } // Create visual explosion part var explosionPart = new Explosion(col, row); game.addChild(explosionPart); activeExplosions.push(explosionPart); setCell(col, row, CELL_TYPE.EXPLOSION); // Mark cell as exploding // Check for chain reactions or destroying bricks if (cellType === CELL_TYPE.DESTRUCTIBLE) { var brickSprite = gridSprites[col][row]; if (brickSprite) { brickSprite.destroy(); gridSprites[col][row] = null; } // Chance to spawn a power-up if (Math.random() < POWERUP_CHANCE) { var powerUpType = Math.random() < 0.5 ? 'extra_bomb' : 'bigger_explosion'; var newPowerUp = new PowerUp(col, row, powerUpType); activePowerUps.push(newPowerUp); // Add powerup slightly above floor, but below other dynamic elements // Find the index of the player to insert before it, otherwise add to end (simplistic layering) var playerIndex = game.children.indexOf(player); if (playerIndex !== -1) { game.addChildAt(newPowerUp, playerIndex); } else { game.addChild(newPowerUp); } } else { // Only set to empty if no powerup spawned here setCell(col, row, CELL_TYPE.EMPTY); // Brick destroyed, becomes empty } setCell(col, row, CELL_TYPE.EXPLOSION); // Temporarily becomes explosion regardless of powerup spawn LK.getSound('destroy_brick').play(); LK.setScore(LK.getScore() + 10); // Award score for destroying brick scoreTxt.setText(LK.getScore()); return false; // Explosion stops after destroying a brick } else if (cellType === CELL_TYPE.BOMB) { // Find the bomb object at this location and trigger it var bombToTrigger = null; for (var i = 0; i < activeBombs.length; i++) { if (activeBombs[i].col === col && activeBombs[i].row === row) { bombToTrigger = activeBombs[i]; break; } } if (bombToTrigger && !bombToTrigger.isExploding) { // Prevent infinite loops if already triggered bombToTrigger.isExploding = true; // Mark as exploding to avoid re-triggering bombToTrigger.triggerExplosion(); } return false; // Chain reaction handles further explosion; stop this particular ray } return true; // Explosion continues } function explodeBomb(bomb) { if (!bomb || bomb.destroyed) { return; } // Bomb might already be destroyed by chain reaction LK.getSound('explosion_sound').play(); var centerCol = bomb.col; var centerRow = bomb.row; // Remove bomb object itself first to prevent chain reaction with self bomb.destroy(); // This also removes from activeBombs and sets cell to EMPTY // Create explosion at the center createExplosion(centerCol, centerRow); // Create explosions outwards in four directions var directions = [[0, 1], [0, -1], [1, 0], [-1, 0]]; // Down, Up, Right, Left for (var i = 0; i < directions.length; i++) { var dx = directions[i][0]; var dy = directions[i][1]; for (var r = 1; r <= BOMB_RANGE; r++) { var currentC = centerCol + dx * r; var currentR = centerRow + dy * r; if (!createExplosion(currentC, currentR)) { break; // Stop this direction if explosion is blocked } } } } // --- Game Initialization --- function initGame() { isGameOver = false; score = 0; LK.setScore(0); activeBombs = []; activeExplosions = []; activePowerUps = []; // Reset active powerups enemies = []; MAX_BOMBS = BASE_MAX_BOMBS; // Reset max bombs BOMB_RANGE = BASE_BOMB_RANGE; // Reset bomb range generateGrid(); // Create Player player = new Player(1, 1); // Start at top-left clear area game.addChild(player); // Spawn enemies on empty cells spawnEnemies(); // Score Display scoreTxt = new Text2('0', { size: 80, fill: 0xFFFFFF }); scoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(scoreTxt); // Add score to top center UI // Play background music LK.playMusic('backgroundMusic'); } // --- Event Handlers --- // Handle player movement and bomb placement based on click/tap position game.down = function (x, y, obj) { // Only handle input if the game has started if (!gameReady) { return; } // Prevent actions if game is over or player is already moving/animating if (isGameOver || !player || player.isMoving) { // Added !player check for safety return; } // Get click position relative to the game area var clickGameX = x; var clickGameY = y; // Determine the target grid cell based on click position var targetCol = Math.floor((clickGameX - GRID_OFFSET_X) / CELL_SIZE); var targetRow = Math.floor((clickGameY - GRID_OFFSET_Y) / CELL_SIZE); // Ensure target is within grid bounds if (targetCol < 0 || targetCol >= GRID_COLS || targetRow < 0 || targetRow >= GRID_ROWS) { return; // Click outside the grid } // Check if the clicked cell is the player's current cell if (targetCol === player.col && targetRow === player.row) { // Clicked on player's cell, place a bomb if possible if (activeBombs.length < MAX_BOMBS && getCellType(player.col, player.row) !== CELL_TYPE.BOMB) { var newBomb = new Bomb(player.col, player.row); game.addChild(newBomb); activeBombs.push(newBomb); setCell(player.col, player.row, CELL_TYPE.BOMB); LK.getSound('placeBomb').play(); // Play sound when placing a bomb } } else { // Clicked on a different cell, attempt to move var dx = targetCol - player.col; var dy = targetRow - player.row; // Determine direction of movement based on the larger difference if (Math.abs(dx) > Math.abs(dy)) { // Horizontal movement if (dx > 0) { // Move Right if (isWalkable(player.col + 1, player.row)) { player.col++; player.isMoving = true; } } else { // Move Left if (isWalkable(player.col - 1, player.row)) { player.col--; player.isMoving = true; } } } else { // Vertical movement if (dy > 0) { // Move Down if (isWalkable(player.col, player.row + 1)) { player.row++; player.isMoving = true; } } else { // Move Up if (isWalkable(player.col, player.row - 1)) { player.row--; player.isMoving = true; } } } // If player is moving, tween the position if (player.isMoving) { var targetPos = getWorldCoords(player.col, player.row); // Mirror the player graphic if moving left if (dx < 0) { player.children[0].scale.x = -1; } else if (dx > 0) { // Un-mirror if moving right player.children[0].scale.x = 1; } tween(player, { x: targetPos.x, y: targetPos.y }, { duration: 150, onFinish: function onFinish() { player.isMoving = false; } }); } } }; // --- Game Update Loop --- game.update = function () { // Only run game logic if the start screen has been dismissed if (!gameReady) { return; } if (isGameOver) { return; } // Check for player collision with explosions var playerCol = player.col; var playerRow = player.row; if (getCellType(playerCol, playerRow) === CELL_TYPE.EXPLOSION) { LK.getSound('player_die').play(); isGameOver = true; // Set game over flag immediately to prevent further actions // Stop any ongoing player movement tween tween.stop(player); // Death animation: Spin and fade out tween(player, { rotation: player.rotation + Math.PI * 2, // Spin 360 degrees alpha: 0 // Fade out }, { duration: 500, // Animation duration ease: tween.easeInQuad, onFinish: function onFinish() { LK.showGameOver(); // Show game over screen after animation } }); return; // Stop further updates } // Check for player collision with enemies for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; if (enemy.col === playerCol && enemy.row === playerRow) { LK.getSound('player_die').play(); isGameOver = true; // Set game over flag immediately to prevent further actions // Stop any ongoing player movement tween tween.stop(player); // Death animation: Spin and fade out tween(player, { rotation: player.rotation + Math.PI * 2, // Spin 360 degrees alpha: 0 // Fade out }, { duration: 500, // Animation duration ease: tween.easeInQuad, onFinish: function onFinish() { LK.showGameOver(); // Show game over screen after animation } }); return; // Stop further updates } // Check for player collision with powerups for (var p = activePowerUps.length - 1; p >= 0; p--) { // Ensure powerUp is valid before accessing properties if (!activePowerUps[p]) { continue; } var powerUp = activePowerUps[p]; if (powerUp.col === playerCol && powerUp.row === playerRow) { // Player picked up powerup var message = ""; if (powerUp.type === 'extra_bomb') { MAX_BOMBS++; message = "+1 bomb!"; } else if (powerUp.type === 'bigger_explosion') { BOMB_RANGE++; message = "Range increased!"; } LK.getSound('powerup_pickup').play(); // Create floating message at player position var messagePos = getWorldCoords(playerCol, playerRow); var powerUpMessage = new PowerUpMessage(message, messagePos.x, messagePos.y - 50); game.addChild(powerUpMessage); // Cell should already be EMPTY, but ensure consistency setCell(powerUp.col, powerUp.row, CELL_TYPE.EMPTY); powerUp.destroy(); // Handles removal from array and visual stage. This also splices activePowerUps. // No need to adjust loop index `p` because destroy() splices the array we are iterating backward. } } // Powerups are no longer destroyed by explosions upon spawning. // Check for enemy collision with explosions if (getCellType(enemy.col, enemy.row) === CELL_TYPE.EXPLOSION) { // Enemy hit by explosion LK.effects.flashObject(enemy, 0xffff00, 200); // Play tinyMonsterDie sound when enemy dies LK.getSound('tinyMonsterDie').play(); enemy.destroy(); enemies.splice(i, 1); // Add score for killing enemy LK.setScore(LK.getScore() + 50); scoreTxt.setText(LK.getScore()); i--; // Adjust index after removing element } } // Move enemies randomly with their individual timing var now = Date.now(); for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; if (!enemy.isMoving && now - enemy.lastMoveTime > enemy.moveDelay) { enemy.tryRandomMove(); } } // Clean up finished explosions and reset cell types for (var i = activeExplosions.length - 1; i >= 0; i--) { var explosion = activeExplosions[i]; // Check if the explosion sprite itself has been destroyed (by its internal timer) // Accessing internal properties like `_destroyed` is generally discouraged, // but necessary if the destroy mechanism is purely timer-based without explicit flags. // A better approach would be for Explosion class to set a 'finished' flag. // Let's assume Explosion.destroy correctly removes it from the stage. // We need a reliable way to know when to remove from activeExplosions and reset the grid cell. // We'll check if it's still parented to the game stage as a proxy. if (!explosion.parent) { // If it's been removed from the stage (destroyed by its timer) var exCol = explosion.col; var exRow = explosion.row; // Reset the grid cell to EMPTY once the explosion effect finishes. // Powerups visually occupy the cell but the underlying grid state should be walkable. if (getCellType(exCol, exRow) === CELL_TYPE.EXPLOSION) { setCell(exCol, exRow, CELL_TYPE.EMPTY); } activeExplosions.splice(i, 1); // Remove from active list } } // Win condition: all enemies destroyed if (enemies.length === 0) { LK.showYouWin(); } }; // --- Create Start Screen --- function createStartScreen() { startScreenContainer = new Container(); startScreenContainer.interactive = true; // Make it cover the whole screen for interaction startScreenContainer.hitArea = new Rectangle(0, 0, 2048, 2732); // Add logo background var logo = startScreenContainer.attachAsset('logo', { anchorX: 0.5, anchorY: 0.5, width: 2048, height: 2732 }); logo.x = 2048 / 2; logo.y = 2732 / 2; var logoBomber = logo.attachAsset('player', { anchorX: 0.5, anchorY: 0.5, width: 500, height: 600, y: -500 }); // Simple bobbing animation for the logoBomber var logoBomberStartY = logoBomber.y; var logoBobTween; function logoBobUp() { if (!logoBomber || !logoBomber.parent) { return; } // Stop if destroyed logoBobTween = tween(logoBomber, { y: logoBomberStartY - 20 }, { duration: 800, ease: tween.easeInOutSine, onFinish: logoBobDown }); } function logoBobDown() { if (!logoBomber || !logoBomber.parent) { return; } // Stop if destroyed logoBobTween = tween(logoBomber, { y: logoBomberStartY }, { duration: 800, ease: tween.easeInOutSine, onFinish: logoBobUp }); } logoBobUp(); // Start the bobbing animation var title = new Text2("BOMB 'N BLOOM", { size: 70, // Adjusted size for better readability on full screen fill: 0xffffff, align: 'left', wordWrap: true, wordWrapWidth: 1800 // Ensure text wraps within screen bounds }); title.anchor.set(0.5, 0.5); title.x = 2048 / 2; title.y = 2732 / 2; // Center the text var instructionsText = "🏁 Click on an adjacent empty cell to move.\n💣 Click on yourself to place a bomb.\n🆙 Collect power ups!\n💥 Run from explosions!"; var instructions = new Text2(instructionsText, { size: 70, // Adjusted size for better readability on full screen fill: 0xffffff, align: 'left', wordWrap: true, wordWrapWidth: 1800 }); instructions.anchor.set(0.5, 0.5); title.addChild(instructions); instructions.y = 300; startScreenContainer.addChild(title); // Rainbow color effect for instructions var rainbowColors = [0xFF0000, 0xFF7F00, 0xFFFF00, 0x00FF00, 0x0000FF, 0x4B0082, 0x9400D3]; var rainbowIndex = 0; var rainbowInterval = LK.setInterval(function () { if (instructions) { // Check if instructions still exist title.tint = rainbowColors[rainbowIndex]; rainbowIndex = (rainbowIndex + 1) % rainbowColors.length; } else { // Clean up if instructions are gone but timer persists (shouldn't happen with current flow) LK.clearInterval(rainbowInterval); rainbowInterval = null; } }, 300); // Change color every 300ms // Function to start the game when clicked startScreenContainer.down = function () { if (gameReady) { return; } // Prevent multiple starts // Stop start screen animations and timers if (logoBobTween) { logoBobTween.kill(); logoBobTween = null; } if (rainbowInterval) { LK.clearInterval(rainbowInterval); rainbowInterval = null; } gameReady = true; // Remove start screen startScreenContainer.destroy(); startScreenContainer = null; // Initialize the actual game initGame(); // Start game timers only after game begins // Set up timer to play random enemy voice sound every 5 seconds var voiceTimer = LK.setInterval(function () { // Array of available voice sounds var voiceSounds = ['voices', 'voices1', 'voices2', 'voices3']; // Select a random voice sound from the array var randomVoice = voiceSounds[Math.floor(Math.random() * voiceSounds.length)]; // Play the selected random voice sound LK.getSound(randomVoice).play(); }, 5000); // 5000 milliseconds = 5 seconds // Set up timer to shake a random brick every few seconds var shakeTimer = LK.setInterval(function () { // Collect all brick sprites var bricks = []; for (var c = 0; c < GRID_COLS; c++) { for (var r = 0; r < GRID_ROWS; r++) { if (grid[c] && grid[c][r] === CELL_TYPE.DESTRUCTIBLE && gridSprites[c] && gridSprites[c][r]) { bricks.push(gridSprites[c][r]); } } } // Only proceed if there are bricks to shake if (bricks.length > 0) { // Pick a random brick var randomBrick = bricks[Math.floor(Math.random() * bricks.length)]; // Store the original position var originalX = randomBrick.x; var originalY = randomBrick.y; // Shake animation sequence tween(randomBrick, { x: originalX - 5, y: originalY - 5 }, { duration: 50, ease: tween.easeInOut, onFinish: function onFinish() { tween(randomBrick, { x: originalX + 5, y: originalY + 5 }, { duration: 50, ease: tween.easeInOut, onFinish: function onFinish() { tween(randomBrick, { x: originalX, y: originalY }, { duration: 50, ease: tween.easeInOut }); } }); } }); } }, 3000); // Shake a brick every 3 seconds }; game.addChild(startScreenContainer); } // --- Initialize the Start Screen --- createStartScreen(); // Spawn enemies at random empty cells function spawnEnemies() { // Clear any existing enemies for (var i = enemies.length - 1; i >= 0; i--) { enemies[i].destroy(); } enemies = []; // Find all empty cells var emptyCells = []; for (var c = 0; c < GRID_COLS; c++) { for (var r = 0; r < GRID_ROWS; r++) { // Don't spawn near player start position if (getCellType(c, r) === CELL_TYPE.EMPTY && !(c <= 3 && r <= 3)) { // Avoid player's starting area emptyCells.push({ col: c, row: r }); } } } // Shuffle empty cells for (var i = emptyCells.length - 1; i > 0; i--) { var j = Math.floor(Math.random() * (i + 1)); var temp = emptyCells[i]; emptyCells[i] = emptyCells[j]; emptyCells[j] = temp; } // Create enemies var enemiesToSpawn = Math.min(ENEMY_COUNT, emptyCells.length); for (var i = 0; i < enemiesToSpawn; i++) { var enemyCell = emptyCells[i]; // Randomly select an enemy type from the available options var enemyTypes = ['enemy', 'enemy1', 'enemy2', 'enemy3', 'enemy4']; var randomEnemyType = enemyTypes[Math.floor(Math.random() * enemyTypes.length)]; var enemy = new Enemy(enemyCell.col, enemyCell.row); // Set the random enemy type asset enemy.children[0].destroy(); // Remove the default 'enemy' asset var graphics = enemy.attachAsset(randomEnemyType, { anchorX: 0.5, anchorY: 0.5 }); // Apply a random color tint to each enemy enemies.push(enemy); game.addChild(enemy); } }
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Represents a placed bomb
var Bomb = Container.expand(function (col, row) {
var self = Container.call(this);
var graphics = self.attachAsset('bomb', {
anchorX: 0.5,
anchorY: 0.5
});
self.col = col;
self.row = row;
self.x = getWorldCoords(col, row).x;
self.y = getWorldCoords(col, row).y;
self.isBomb = true; // Identifier
self.pulsatingTween = null; // Initialize tween variable
// Define the pulsation functions
function scaleDown() {
// Prevent tweening if the bomb is already destroyed
if (self.destroyed) {
return;
}
self.pulsatingTween = tween(self.scale, {
x: 1.0,
y: 1.0
}, {
duration: 200,
ease: tween.easeInOut,
onFinish: scaleUp // When shrinking finishes, start growing
});
}
function scaleUp() {
// Prevent tweening if the bomb is already destroyed
if (self.destroyed) {
return;
}
self.pulsatingTween = tween(self.scale, {
x: 1.1,
y: 1.1
}, {
duration: 200,
ease: tween.easeInOut,
onFinish: scaleDown // When growing finishes, start shrinking
});
}
scaleUp();
// Start the explosion timer
var explosionTimer = LK.setTimeout(function () {
explodeBomb(self);
}, BOMB_TIMER);
// Public method to trigger explosion early (chain reaction)
self.triggerExplosion = function () {
LK.clearTimeout(explosionTimer);
explodeBomb(self);
};
// Override destroy to clear the timer
var baseDestroy = self.destroy;
self.destroy = function () {
LK.clearTimeout(explosionTimer);
// Stop the specific pulsating tween instance
if (self.pulsatingTween) {
self.pulsatingTween.kill(); // Use the kill() method on the stored tween instance
self.pulsatingTween = null; // Clear the reference
}
// Make sure the grid cell is marked empty when bomb is destroyed (e.g., by explosion)
if (grid[self.col] && grid[self.col][self.row] === CELL_TYPE.BOMB) {
setCell(self.col, self.row, CELL_TYPE.EMPTY);
}
// Remove from active bombs list in game scope
var index = activeBombs.indexOf(self);
if (index !== -1) {
activeBombs.splice(index, 1);
}
// Mark as destroyed so ongoing tween callbacks won't try to run
self.destroyed = true;
if (baseDestroy) {
baseDestroy.call(self);
} // Call original destroy if exists
};
return self;
});
// Represents an enemy that moves between grid cells
var Enemy = Container.expand(function (startCol, startRow) {
var self = Container.call(this);
var graphics = self.attachAsset('enemy', {
anchorX: 0.5,
anchorY: 0.5
});
self.col = startCol;
self.row = startRow;
self.isEnemy = true; // Identifier
self.isMoving = false; // Flag to track movement animation state
self.moveDelay = 500 + Math.random() * 500; // Reduced delay between moves (0.5-1s)
self.lastMoveTime = Date.now(); // Track when last moved
// Snap to initial grid position
var initialPos = getWorldCoords(self.col, self.row);
self.x = initialPos.x;
self.y = initialPos.y;
// Try to move, prioritizing adjacent player cell
self.tryRandomMove = function () {
if (self.isMoving || !player || player.isMoving) {
// Don't move if animating or player doesn't exist/is moving
return false;
}
// Check if player is adjacent
var playerCol = player.col;
var playerRow = player.row;
var dx = playerCol - self.col;
var dy = playerRow - self.row;
// Check for adjacency (Manhattan distance of 1)
if (Math.abs(dx) + Math.abs(dy) === 1) {
// Player is adjacent. Try to move to the player's cell.
var targetCol = playerCol;
var targetRow = playerRow;
var moveDx = dx; // Direction towards player
// Check if the player's cell is walkable (it might not be if another enemy is there temporarily, unlikely but defensive check)
if (isWalkable(targetCol, targetRow)) {
// Move towards the player
self.moveTo(targetCol, targetRow, moveDx);
return true; // Moved towards player
}
// If player's cell is not walkable, fall through to random move
}
// Player is not adjacent, or adjacent cell is blocked: Try to move in a random direction
var directions = [{
dx: 0,
dy: -1
},
// up
{
dx: 1,
dy: 0
},
// right
{
dx: 0,
dy: 1
},
// down
{
dx: -1,
dy: 0
} // left
];
// Shuffle directions for randomness
for (var i = directions.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var temp = directions[i];
directions[i] = directions[j];
directions[j] = temp;
}
// Try each random direction until a valid move is found
for (var i = 0; i < directions.length; i++) {
var newCol = self.col + directions[i].dx;
var newRow = self.row + directions[i].dy;
if (isWalkable(newCol, newRow)) {
// Found valid random move
self.moveTo(newCol, newRow, directions[i].dx);
return true;
}
}
return false; // No valid moves found
};
// Move to a new cell
self.moveTo = function (newCol, newRow, dx) {
self.isMoving = true;
self.col = newCol;
self.row = newRow;
var targetPos = getWorldCoords(newCol, newRow);
// Flip sprite based on movement direction
if (dx < 0) {
graphics.scale.x = -1;
} else if (dx > 0) {
graphics.scale.x = 1;
}
// Select a random easing function
var randomEasing = enemyEasings[Math.floor(Math.random() * enemyEasings.length)];
// Animate movement
tween(self, {
x: targetPos.x,
y: targetPos.y
}, {
duration: 500,
// Make movement animation faster
easing: randomEasing,
// Use the randomly selected easing function
onFinish: function onFinish() {
self.isMoving = false;
self.lastMoveTime = Date.now();
}
});
return true;
};
return self;
});
// Spawn enemies at random empty cells
// Represents a single explosion particle/cell
var Explosion = Container.expand(function (col, row) {
var self = Container.call(this);
var graphics = self.attachAsset('explosion', {
anchorX: 0.5,
anchorY: 0.5
});
self.col = col;
self.row = row;
self.x = getWorldCoords(col, row).x;
self.y = getWorldCoords(col, row).y;
self.isExplosion = true; // Identifier for collision checks
// Start scaled down for explosion effect
self.scale.set(0);
// Animate the explosion appearing
tween(self.scale, {
x: 1,
y: 1
}, {
duration: 150,
ease: tween.easeInOut
});
// Fade out and destroy after duration
var timer = LK.setTimeout(function () {
self.destroy(); // Destroy handled by Game update loop removal
}, EXPLOSION_DURATION);
// Override destroy to clear the timer
var baseDestroy = self.destroy;
self.destroy = function () {
LK.clearTimeout(timer);
if (baseDestroy) {
baseDestroy.call(self);
} // Call original destroy if exists
};
return self;
});
// Represents the player character
var Player = Container.expand(function (startCol, startRow) {
var self = Container.call(this);
var graphics = self.attachAsset('player', {
anchorX: 0.5,
anchorY: 0.5
});
self.col = startCol;
self.row = startRow;
self.isPlayer = true; // Identifier
// Snap to initial grid position
var initialPos = getWorldCoords(self.col, self.row);
self.x = initialPos.x;
self.y = initialPos.y;
self.isMoving = false; // Flag to track movement animation state
// Empty placeholder for movement - functionality removed
self.moveTo = function (newCol, newRow) {
// Movement functionality removed
return false; // Movement disabled
};
// Empty placeholder for bomb placement - functionality removed
self.placeBomb = function () {
// Bomb placement functionality removed
};
});
// Represents a power-up item dropped on the grid
var PowerUp = Container.expand(function (col, row, type) {
var self = Container.call(this);
self.col = col;
self.row = row;
self.type = type; // 'extra_bomb' or 'bigger_explosion'
self.isPowerUp = true; // Identifier
var assetId = type === 'extra_bomb' ? 'powerup_bomb' : 'powerup_range';
var graphics = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
var coords = getWorldCoords(col, row);
self.x = coords.x;
self.y = coords.y;
// Simple bobbing animation
var startY = self.y;
var bobTween;
function bobUp() {
if (self.destroyed) {
return;
}
bobTween = tween(self, {
y: startY - 10
}, {
duration: 500,
ease: tween.easeInOutSine,
onFinish: bobDown
});
}
function bobDown() {
if (self.destroyed) {
return;
}
bobTween = tween(self, {
y: startY
}, {
duration: 500,
ease: tween.easeInOutSine,
onFinish: bobUp
});
}
bobUp(); // Start animation
// Override destroy to stop tween
var baseDestroy = self.destroy;
self.destroy = function () {
self.destroyed = true; // Flag to stop tween callbacks
if (bobTween) {
bobTween.kill();
bobTween = null;
}
// Remove from active power-ups list
var index = activePowerUps.indexOf(self);
if (index !== -1) {
activePowerUps.splice(index, 1);
}
if (baseDestroy) {
baseDestroy.call(self);
}
};
return self;
});
// Class to display a floating message when picking up a powerup
var PowerUpMessage = Container.expand(function (message, x, y) {
var self = Container.call(this);
// Create text with the powerup message
var messageTxt = new Text2(message, {
size: 60,
fill: 0xFFFF00 // Yellow color for visibility
});
messageTxt.anchor.set(0.5, 0.5);
self.addChild(messageTxt);
// Position at the specified location
self.x = x;
self.y = y;
// Animate the message: move up and fade out
function animateAndDestroy() {
tween(self, {
y: self.y - 100,
// Move up
alpha: 0 // Fade out
}, {
duration: 1000,
// Animation lasts 1 second
ease: tween.easeOutQuad,
onFinish: function onFinish() {
// Remove from parent when animation completes
self.destroy();
}
});
}
// Start the animation
animateAndDestroy();
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: Math.floor(Math.random() * 0xFFFFFF) // Random background color
});
/****
* Game Code
****/
// Let's define the shapes we'll use:
// Engine will implicitly create assets based on usage below.
// --- Constants ---
// Placeholder ID
// Placeholder ID
// Placeholder ID for the logo
var GRID_COLS = 15; // Ensure odd number for wall placement
var GRID_ROWS = 20; // Remove the last row
var CELL_SIZE = 128;
var GRID_WIDTH = GRID_COLS * CELL_SIZE;
var GRID_HEIGHT = GRID_ROWS * CELL_SIZE;
var GRID_OFFSET_X = (2048 - GRID_WIDTH) / 2;
var GRID_OFFSET_Y = 200; // Position grid 200 pixels from the top
var CELL_TYPE = {
EMPTY: 0,
DESTRUCTIBLE: 1,
INDESTRUCTIBLE: 2,
BOMB: 3,
// Cell temporarily occupied by a bomb
EXPLOSION: 4 // Cell temporarily occupied by explosion
};
var BOMB_TIMER = 2500; // Milliseconds before bomb explodes
var EXPLOSION_DURATION = 400; // Milliseconds explosion visuals last
var BASE_BOMB_RANGE = 2; // Default explosion range
var BASE_MAX_BOMBS = 1; // Default max bombs
var BOMB_RANGE; // Current explosion range (can be modified by powerups)
var MAX_BOMBS; // Current max bombs (can be modified by powerups)
var POWERUP_CHANCE = 0.3; // 30% chance to drop a powerup from a brick
var BRICK_DENSITY = 0.2; // Further reduced probability of bricks to make more room for enemies
// Array of tween easing functions for enemy movement
var enemyEasings = [
// Basic easing functions
tween.linear,
// Quad easing
tween.easeIn, tween.easeOut, tween.easeInOut,
// Cubic easing
tween.cubicIn, tween.cubicOut, tween.cubicInOut,
// Quartic easing
tween.quarticIn, tween.quarticOut, tween.quarticInOut,
// Quintic easing
tween.quinticIn, tween.quinticOut, tween.quinticInOut,
// Sine easing
tween.sineIn, tween.sineOut, tween.sineInOut,
// Exponential easing
tween.expoIn, tween.expoOut, tween.expoInOut,
// Circular easing
tween.easeInCirc, tween.easeOutCirc, tween.easeInOutCirc,
// Include previously commented out elastic easing which can create interesting movements
tween.elasticIn, tween.elasticOut, tween.elasticInOut];
// --- Game State Variables ---
var grid = []; // 2D array holding CELL_TYPE for each cell
var gridSprites = []; // 2D array holding visual sprites for walls/bricks
var player;
var activeBombs = [];
var activeExplosions = []; // Array to hold active Explosion objects
var enemies = []; // Array to hold all enemy instances
var score = 0;
var scoreTxt;
var isGameOver = false;
var ENEMY_COUNT = 6; // Increased number of enemies to spawn
var startScreenContainer = null; // Container for the start screen elements
var gameReady = false; // Flag to track if the game has started
// --- Helper Functions ---
function getWorldCoords(col, row) {
var x = GRID_OFFSET_X + col * CELL_SIZE + CELL_SIZE / 2;
var y = GRID_OFFSET_Y + row * CELL_SIZE + CELL_SIZE / 2;
return {
x: x,
y: y
};
}
// Get the type of a cell, handling out-of-bounds
function getCellType(col, row) {
if (col < 0 || col >= GRID_COLS || row < 0 || row >= GRID_ROWS) {
return CELL_TYPE.INDESTRUCTIBLE; // Treat out of bounds as walls
}
return grid[col][row];
}
// Set the type of a cell
function setCell(col, row, type) {
if (col >= 0 && col < GRID_COLS && row >= 0 && row < GRID_ROWS) {
grid[col][row] = type;
}
}
// Check if a cell is valid and empty for movement
// Check if a cell is occupied by an enemy
function isEnemyOccupied(col, row) {
for (var i = 0; i < enemies.length; i++) {
if (enemies[i].col === col && enemies[i].row === row) {
return true;
}
}
return false;
}
// Check if a cell is valid and empty for movement (for both player and enemies)
function isWalkable(col, row) {
var cellType = getCellType(col, row);
// A cell is walkable only if it's currently empty AND not occupied by another enemy.
return cellType === CELL_TYPE.EMPTY && !isEnemyOccupied(col, row);
}
// --- Grid Generation ---
function generateGrid() {
grid = [];
gridSprites = [];
// Clear previous sprites if any
game.children.forEach(function (child) {
if (child.isGridElement) {
// Add a flag to identify grid sprites
child.destroy();
}
});
game.removeChildren(); // Clear all children before regenerating
// Add floor tiles everywhere first
for (var c = 0; c < GRID_COLS; c++) {
for (var r = 0; r < GRID_ROWS; r++) {
var floorTile = LK.getAsset('floor', {
anchorX: 0.0,
anchorY: 0.0,
alpha: 0
});
floorTile.x = GRID_OFFSET_X + c * CELL_SIZE;
floorTile.y = GRID_OFFSET_Y + r * CELL_SIZE;
floorTile.isGridElement = true;
game.addChild(floorTile);
}
}
for (var c = 0; c < GRID_COLS; c++) {
grid[c] = [];
gridSprites[c] = [];
for (var r = 0; r < GRID_ROWS; r++) {
var cellType;
var sprite = null;
// Place indestructible walls around border and in a checkerboard pattern inside
if (c === 0 || c === GRID_COLS - 1 || r === 0 || r === GRID_ROWS - 1 || c % 2 === 0 && r % 2 === 0) {
cellType = CELL_TYPE.INDESTRUCTIBLE;
sprite = LK.getAsset('wall', {
anchorX: 0.0,
anchorY: 0.0
});
} else {
// Place destructible bricks randomly, ensuring player start area is clear
if (c <= 2 && r <= 2 || c >= GRID_COLS - 3 && r >= GRID_ROWS - 3) {
// Clear corners for potential spawns
cellType = CELL_TYPE.EMPTY;
} else if (Math.random() < BRICK_DENSITY) {
cellType = CELL_TYPE.DESTRUCTIBLE;
sprite = LK.getAsset('brick', {
anchorX: 0.0,
anchorY: 0.0
});
} else {
cellType = CELL_TYPE.EMPTY;
}
}
grid[c][r] = cellType;
if (sprite) {
sprite.x = GRID_OFFSET_X + c * CELL_SIZE;
sprite.y = GRID_OFFSET_Y + r * CELL_SIZE;
sprite.isGridElement = true; // Mark as part of the grid visuals
game.addChild(sprite);
gridSprites[c][r] = sprite;
} else {
gridSprites[c][r] = null;
}
}
}
// Ensure player start is clear
setCell(1, 1, CELL_TYPE.EMPTY);
setCell(1, 2, CELL_TYPE.EMPTY);
setCell(2, 1, CELL_TYPE.EMPTY);
if (gridSprites[1] && gridSprites[1][1]) {
var _gridSprites$1$;
(_gridSprites$1$ = gridSprites[1][1]) === null || _gridSprites$1$ === void 0 || _gridSprites$1$.destroy();
gridSprites[1][1] = null;
}
if (gridSprites[1] && gridSprites[1][2]) {
var _gridSprites$1$2;
(_gridSprites$1$2 = gridSprites[1][2]) === null || _gridSprites$1$2 === void 0 || _gridSprites$1$2.destroy();
gridSprites[1][2] = null;
}
if (gridSprites[2] && gridSprites[2][1]) {
var _gridSprites$2$;
(_gridSprites$2$ = gridSprites[2][1]) === null || _gridSprites$2$ === void 0 || _gridSprites$2$.destroy();
gridSprites[2][1] = null;
}
}
// --- Explosion Handling ---
function createExplosion(col, row) {
if (col < 0 || col >= GRID_COLS || row < 0 || row >= GRID_ROWS) {
return false;
} // Out of bounds
var cellType = getCellType(col, row);
if (cellType === CELL_TYPE.INDESTRUCTIBLE) {
return false; // Explosion stops at indestructible walls
}
// Create visual explosion part
var explosionPart = new Explosion(col, row);
game.addChild(explosionPart);
activeExplosions.push(explosionPart);
setCell(col, row, CELL_TYPE.EXPLOSION); // Mark cell as exploding
// Check for chain reactions or destroying bricks
if (cellType === CELL_TYPE.DESTRUCTIBLE) {
var brickSprite = gridSprites[col][row];
if (brickSprite) {
brickSprite.destroy();
gridSprites[col][row] = null;
}
// Chance to spawn a power-up
if (Math.random() < POWERUP_CHANCE) {
var powerUpType = Math.random() < 0.5 ? 'extra_bomb' : 'bigger_explosion';
var newPowerUp = new PowerUp(col, row, powerUpType);
activePowerUps.push(newPowerUp);
// Add powerup slightly above floor, but below other dynamic elements
// Find the index of the player to insert before it, otherwise add to end (simplistic layering)
var playerIndex = game.children.indexOf(player);
if (playerIndex !== -1) {
game.addChildAt(newPowerUp, playerIndex);
} else {
game.addChild(newPowerUp);
}
} else {
// Only set to empty if no powerup spawned here
setCell(col, row, CELL_TYPE.EMPTY); // Brick destroyed, becomes empty
}
setCell(col, row, CELL_TYPE.EXPLOSION); // Temporarily becomes explosion regardless of powerup spawn
LK.getSound('destroy_brick').play();
LK.setScore(LK.getScore() + 10); // Award score for destroying brick
scoreTxt.setText(LK.getScore());
return false; // Explosion stops after destroying a brick
} else if (cellType === CELL_TYPE.BOMB) {
// Find the bomb object at this location and trigger it
var bombToTrigger = null;
for (var i = 0; i < activeBombs.length; i++) {
if (activeBombs[i].col === col && activeBombs[i].row === row) {
bombToTrigger = activeBombs[i];
break;
}
}
if (bombToTrigger && !bombToTrigger.isExploding) {
// Prevent infinite loops if already triggered
bombToTrigger.isExploding = true; // Mark as exploding to avoid re-triggering
bombToTrigger.triggerExplosion();
}
return false; // Chain reaction handles further explosion; stop this particular ray
}
return true; // Explosion continues
}
function explodeBomb(bomb) {
if (!bomb || bomb.destroyed) {
return;
} // Bomb might already be destroyed by chain reaction
LK.getSound('explosion_sound').play();
var centerCol = bomb.col;
var centerRow = bomb.row;
// Remove bomb object itself first to prevent chain reaction with self
bomb.destroy(); // This also removes from activeBombs and sets cell to EMPTY
// Create explosion at the center
createExplosion(centerCol, centerRow);
// Create explosions outwards in four directions
var directions = [[0, 1], [0, -1], [1, 0], [-1, 0]]; // Down, Up, Right, Left
for (var i = 0; i < directions.length; i++) {
var dx = directions[i][0];
var dy = directions[i][1];
for (var r = 1; r <= BOMB_RANGE; r++) {
var currentC = centerCol + dx * r;
var currentR = centerRow + dy * r;
if (!createExplosion(currentC, currentR)) {
break; // Stop this direction if explosion is blocked
}
}
}
}
// --- Game Initialization ---
function initGame() {
isGameOver = false;
score = 0;
LK.setScore(0);
activeBombs = [];
activeExplosions = [];
activePowerUps = []; // Reset active powerups
enemies = [];
MAX_BOMBS = BASE_MAX_BOMBS; // Reset max bombs
BOMB_RANGE = BASE_BOMB_RANGE; // Reset bomb range
generateGrid();
// Create Player
player = new Player(1, 1); // Start at top-left clear area
game.addChild(player);
// Spawn enemies on empty cells
spawnEnemies();
// Score Display
scoreTxt = new Text2('0', {
size: 80,
fill: 0xFFFFFF
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt); // Add score to top center UI
// Play background music
LK.playMusic('backgroundMusic');
}
// --- Event Handlers ---
// Handle player movement and bomb placement based on click/tap position
game.down = function (x, y, obj) {
// Only handle input if the game has started
if (!gameReady) {
return;
}
// Prevent actions if game is over or player is already moving/animating
if (isGameOver || !player || player.isMoving) {
// Added !player check for safety
return;
}
// Get click position relative to the game area
var clickGameX = x;
var clickGameY = y;
// Determine the target grid cell based on click position
var targetCol = Math.floor((clickGameX - GRID_OFFSET_X) / CELL_SIZE);
var targetRow = Math.floor((clickGameY - GRID_OFFSET_Y) / CELL_SIZE);
// Ensure target is within grid bounds
if (targetCol < 0 || targetCol >= GRID_COLS || targetRow < 0 || targetRow >= GRID_ROWS) {
return; // Click outside the grid
}
// Check if the clicked cell is the player's current cell
if (targetCol === player.col && targetRow === player.row) {
// Clicked on player's cell, place a bomb if possible
if (activeBombs.length < MAX_BOMBS && getCellType(player.col, player.row) !== CELL_TYPE.BOMB) {
var newBomb = new Bomb(player.col, player.row);
game.addChild(newBomb);
activeBombs.push(newBomb);
setCell(player.col, player.row, CELL_TYPE.BOMB);
LK.getSound('placeBomb').play(); // Play sound when placing a bomb
}
} else {
// Clicked on a different cell, attempt to move
var dx = targetCol - player.col;
var dy = targetRow - player.row;
// Determine direction of movement based on the larger difference
if (Math.abs(dx) > Math.abs(dy)) {
// Horizontal movement
if (dx > 0) {
// Move Right
if (isWalkable(player.col + 1, player.row)) {
player.col++;
player.isMoving = true;
}
} else {
// Move Left
if (isWalkable(player.col - 1, player.row)) {
player.col--;
player.isMoving = true;
}
}
} else {
// Vertical movement
if (dy > 0) {
// Move Down
if (isWalkable(player.col, player.row + 1)) {
player.row++;
player.isMoving = true;
}
} else {
// Move Up
if (isWalkable(player.col, player.row - 1)) {
player.row--;
player.isMoving = true;
}
}
}
// If player is moving, tween the position
if (player.isMoving) {
var targetPos = getWorldCoords(player.col, player.row);
// Mirror the player graphic if moving left
if (dx < 0) {
player.children[0].scale.x = -1;
} else if (dx > 0) {
// Un-mirror if moving right
player.children[0].scale.x = 1;
}
tween(player, {
x: targetPos.x,
y: targetPos.y
}, {
duration: 150,
onFinish: function onFinish() {
player.isMoving = false;
}
});
}
}
};
// --- Game Update Loop ---
game.update = function () {
// Only run game logic if the start screen has been dismissed
if (!gameReady) {
return;
}
if (isGameOver) {
return;
}
// Check for player collision with explosions
var playerCol = player.col;
var playerRow = player.row;
if (getCellType(playerCol, playerRow) === CELL_TYPE.EXPLOSION) {
LK.getSound('player_die').play();
isGameOver = true; // Set game over flag immediately to prevent further actions
// Stop any ongoing player movement tween
tween.stop(player);
// Death animation: Spin and fade out
tween(player, {
rotation: player.rotation + Math.PI * 2,
// Spin 360 degrees
alpha: 0 // Fade out
}, {
duration: 500,
// Animation duration
ease: tween.easeInQuad,
onFinish: function onFinish() {
LK.showGameOver(); // Show game over screen after animation
}
});
return; // Stop further updates
}
// Check for player collision with enemies
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy.col === playerCol && enemy.row === playerRow) {
LK.getSound('player_die').play();
isGameOver = true; // Set game over flag immediately to prevent further actions
// Stop any ongoing player movement tween
tween.stop(player);
// Death animation: Spin and fade out
tween(player, {
rotation: player.rotation + Math.PI * 2,
// Spin 360 degrees
alpha: 0 // Fade out
}, {
duration: 500,
// Animation duration
ease: tween.easeInQuad,
onFinish: function onFinish() {
LK.showGameOver(); // Show game over screen after animation
}
});
return; // Stop further updates
}
// Check for player collision with powerups
for (var p = activePowerUps.length - 1; p >= 0; p--) {
// Ensure powerUp is valid before accessing properties
if (!activePowerUps[p]) {
continue;
}
var powerUp = activePowerUps[p];
if (powerUp.col === playerCol && powerUp.row === playerRow) {
// Player picked up powerup
var message = "";
if (powerUp.type === 'extra_bomb') {
MAX_BOMBS++;
message = "+1 bomb!";
} else if (powerUp.type === 'bigger_explosion') {
BOMB_RANGE++;
message = "Range increased!";
}
LK.getSound('powerup_pickup').play();
// Create floating message at player position
var messagePos = getWorldCoords(playerCol, playerRow);
var powerUpMessage = new PowerUpMessage(message, messagePos.x, messagePos.y - 50);
game.addChild(powerUpMessage);
// Cell should already be EMPTY, but ensure consistency
setCell(powerUp.col, powerUp.row, CELL_TYPE.EMPTY);
powerUp.destroy(); // Handles removal from array and visual stage. This also splices activePowerUps.
// No need to adjust loop index `p` because destroy() splices the array we are iterating backward.
}
}
// Powerups are no longer destroyed by explosions upon spawning.
// Check for enemy collision with explosions
if (getCellType(enemy.col, enemy.row) === CELL_TYPE.EXPLOSION) {
// Enemy hit by explosion
LK.effects.flashObject(enemy, 0xffff00, 200);
// Play tinyMonsterDie sound when enemy dies
LK.getSound('tinyMonsterDie').play();
enemy.destroy();
enemies.splice(i, 1);
// Add score for killing enemy
LK.setScore(LK.getScore() + 50);
scoreTxt.setText(LK.getScore());
i--; // Adjust index after removing element
}
}
// Move enemies randomly with their individual timing
var now = Date.now();
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (!enemy.isMoving && now - enemy.lastMoveTime > enemy.moveDelay) {
enemy.tryRandomMove();
}
}
// Clean up finished explosions and reset cell types
for (var i = activeExplosions.length - 1; i >= 0; i--) {
var explosion = activeExplosions[i];
// Check if the explosion sprite itself has been destroyed (by its internal timer)
// Accessing internal properties like `_destroyed` is generally discouraged,
// but necessary if the destroy mechanism is purely timer-based without explicit flags.
// A better approach would be for Explosion class to set a 'finished' flag.
// Let's assume Explosion.destroy correctly removes it from the stage.
// We need a reliable way to know when to remove from activeExplosions and reset the grid cell.
// We'll check if it's still parented to the game stage as a proxy.
if (!explosion.parent) {
// If it's been removed from the stage (destroyed by its timer)
var exCol = explosion.col;
var exRow = explosion.row;
// Reset the grid cell to EMPTY once the explosion effect finishes.
// Powerups visually occupy the cell but the underlying grid state should be walkable.
if (getCellType(exCol, exRow) === CELL_TYPE.EXPLOSION) {
setCell(exCol, exRow, CELL_TYPE.EMPTY);
}
activeExplosions.splice(i, 1); // Remove from active list
}
}
// Win condition: all enemies destroyed
if (enemies.length === 0) {
LK.showYouWin();
}
};
// --- Create Start Screen ---
function createStartScreen() {
startScreenContainer = new Container();
startScreenContainer.interactive = true;
// Make it cover the whole screen for interaction
startScreenContainer.hitArea = new Rectangle(0, 0, 2048, 2732);
// Add logo background
var logo = startScreenContainer.attachAsset('logo', {
anchorX: 0.5,
anchorY: 0.5,
width: 2048,
height: 2732
});
logo.x = 2048 / 2;
logo.y = 2732 / 2;
var logoBomber = logo.attachAsset('player', {
anchorX: 0.5,
anchorY: 0.5,
width: 500,
height: 600,
y: -500
});
// Simple bobbing animation for the logoBomber
var logoBomberStartY = logoBomber.y;
var logoBobTween;
function logoBobUp() {
if (!logoBomber || !logoBomber.parent) {
return;
} // Stop if destroyed
logoBobTween = tween(logoBomber, {
y: logoBomberStartY - 20
}, {
duration: 800,
ease: tween.easeInOutSine,
onFinish: logoBobDown
});
}
function logoBobDown() {
if (!logoBomber || !logoBomber.parent) {
return;
} // Stop if destroyed
logoBobTween = tween(logoBomber, {
y: logoBomberStartY
}, {
duration: 800,
ease: tween.easeInOutSine,
onFinish: logoBobUp
});
}
logoBobUp(); // Start the bobbing animation
var title = new Text2("BOMB 'N BLOOM", {
size: 70,
// Adjusted size for better readability on full screen
fill: 0xffffff,
align: 'left',
wordWrap: true,
wordWrapWidth: 1800 // Ensure text wraps within screen bounds
});
title.anchor.set(0.5, 0.5);
title.x = 2048 / 2;
title.y = 2732 / 2; // Center the text
var instructionsText = "🏁 Click on an adjacent empty cell to move.\n💣 Click on yourself to place a bomb.\n🆙 Collect power ups!\n💥 Run from explosions!";
var instructions = new Text2(instructionsText, {
size: 70,
// Adjusted size for better readability on full screen
fill: 0xffffff,
align: 'left',
wordWrap: true,
wordWrapWidth: 1800
});
instructions.anchor.set(0.5, 0.5);
title.addChild(instructions);
instructions.y = 300;
startScreenContainer.addChild(title);
// Rainbow color effect for instructions
var rainbowColors = [0xFF0000, 0xFF7F00, 0xFFFF00, 0x00FF00, 0x0000FF, 0x4B0082, 0x9400D3];
var rainbowIndex = 0;
var rainbowInterval = LK.setInterval(function () {
if (instructions) {
// Check if instructions still exist
title.tint = rainbowColors[rainbowIndex];
rainbowIndex = (rainbowIndex + 1) % rainbowColors.length;
} else {
// Clean up if instructions are gone but timer persists (shouldn't happen with current flow)
LK.clearInterval(rainbowInterval);
rainbowInterval = null;
}
}, 300); // Change color every 300ms
// Function to start the game when clicked
startScreenContainer.down = function () {
if (gameReady) {
return;
} // Prevent multiple starts
// Stop start screen animations and timers
if (logoBobTween) {
logoBobTween.kill();
logoBobTween = null;
}
if (rainbowInterval) {
LK.clearInterval(rainbowInterval);
rainbowInterval = null;
}
gameReady = true;
// Remove start screen
startScreenContainer.destroy();
startScreenContainer = null;
// Initialize the actual game
initGame();
// Start game timers only after game begins
// Set up timer to play random enemy voice sound every 5 seconds
var voiceTimer = LK.setInterval(function () {
// Array of available voice sounds
var voiceSounds = ['voices', 'voices1', 'voices2', 'voices3'];
// Select a random voice sound from the array
var randomVoice = voiceSounds[Math.floor(Math.random() * voiceSounds.length)];
// Play the selected random voice sound
LK.getSound(randomVoice).play();
}, 5000); // 5000 milliseconds = 5 seconds
// Set up timer to shake a random brick every few seconds
var shakeTimer = LK.setInterval(function () {
// Collect all brick sprites
var bricks = [];
for (var c = 0; c < GRID_COLS; c++) {
for (var r = 0; r < GRID_ROWS; r++) {
if (grid[c] && grid[c][r] === CELL_TYPE.DESTRUCTIBLE && gridSprites[c] && gridSprites[c][r]) {
bricks.push(gridSprites[c][r]);
}
}
}
// Only proceed if there are bricks to shake
if (bricks.length > 0) {
// Pick a random brick
var randomBrick = bricks[Math.floor(Math.random() * bricks.length)];
// Store the original position
var originalX = randomBrick.x;
var originalY = randomBrick.y;
// Shake animation sequence
tween(randomBrick, {
x: originalX - 5,
y: originalY - 5
}, {
duration: 50,
ease: tween.easeInOut,
onFinish: function onFinish() {
tween(randomBrick, {
x: originalX + 5,
y: originalY + 5
}, {
duration: 50,
ease: tween.easeInOut,
onFinish: function onFinish() {
tween(randomBrick, {
x: originalX,
y: originalY
}, {
duration: 50,
ease: tween.easeInOut
});
}
});
}
});
}
}, 3000); // Shake a brick every 3 seconds
};
game.addChild(startScreenContainer);
}
// --- Initialize the Start Screen ---
createStartScreen();
// Spawn enemies at random empty cells
function spawnEnemies() {
// Clear any existing enemies
for (var i = enemies.length - 1; i >= 0; i--) {
enemies[i].destroy();
}
enemies = [];
// Find all empty cells
var emptyCells = [];
for (var c = 0; c < GRID_COLS; c++) {
for (var r = 0; r < GRID_ROWS; r++) {
// Don't spawn near player start position
if (getCellType(c, r) === CELL_TYPE.EMPTY && !(c <= 3 && r <= 3)) {
// Avoid player's starting area
emptyCells.push({
col: c,
row: r
});
}
}
}
// Shuffle empty cells
for (var i = emptyCells.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var temp = emptyCells[i];
emptyCells[i] = emptyCells[j];
emptyCells[j] = temp;
}
// Create enemies
var enemiesToSpawn = Math.min(ENEMY_COUNT, emptyCells.length);
for (var i = 0; i < enemiesToSpawn; i++) {
var enemyCell = emptyCells[i];
// Randomly select an enemy type from the available options
var enemyTypes = ['enemy', 'enemy1', 'enemy2', 'enemy3', 'enemy4'];
var randomEnemyType = enemyTypes[Math.floor(Math.random() * enemyTypes.length)];
var enemy = new Enemy(enemyCell.col, enemyCell.row);
// Set the random enemy type asset
enemy.children[0].destroy(); // Remove the default 'enemy' asset
var graphics = enemy.attachAsset(randomEnemyType, {
anchorX: 0.5,
anchorY: 0.5
});
// Apply a random color tint to each enemy
enemies.push(enemy);
game.addChild(enemy);
}
}
concrete floor tile, retro, pixel style. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
bomb, pixel style, retro. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
a demonic strawberry, pixel style. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
a demonic cherry, pixel style. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
a demonic kiwi, pixel style. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
A human character, player from an arcade retro game, looking like in the 80s 90s, hair, male, looking right, pixel style. In-Game asset. 2d. High contrast. No shadows
fire texture pixel style retro square. In-Game asset. 2d. High contrast. No shadows
powerup icon for an additional charge of a bomb, retro arcade game. In-Game asset. 2d. High contrast. No shadows
powerup icon for an additional range of the bomb explosion you can throw, retro arcade game. In-Game asset. 2d. High contrast. No shadows
A pixelated gun. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
A gray square with rounded edges. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows