/****
* 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;
});
// Class for bullets fired from the gun
var Bullet = Container.expand(function (startX, startY, targetX, targetY) {
var self = Container.call(this);
// Create a simple bullet graphic
var bulletGraphics = self.attachAsset('powerup_bomb', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.5,
scaleY: 0.5
});
self.x = startX;
self.y = startY;
self.isBullet = true;
// Calculate direction vector
var dx = targetX - startX;
var dy = targetY - startY;
var magnitude = Math.sqrt(dx * dx + dy * dy);
self.velocityX = dx / magnitude * 15; // Speed factor
self.velocityY = dy / magnitude * 15;
// Store last position for collision detection
self.lastX = self.x;
self.lastY = self.y;
self.update = function () {
// Store last position
self.lastX = self.x;
self.lastY = self.y;
// Update position based on velocity
self.x += self.velocityX;
self.y += self.velocityY;
// Check if bullet is outside screen
if (self.x < 0 || self.x > 2048 || self.y < 0 || self.y > 2732) {
self.destroy();
}
};
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;
});
// Class for the weapon slot
var Slot = Container.expand(function (type, x, y) {
var self = Container.call(this);
var slotGraphics = self.attachAsset('Slot', {
anchorX: 0.5,
anchorY: 0.5
});
self.type = type; // 'bomb' or 'gun'
self.x = x;
self.y = y;
self.occupied = false;
self.item = null;
// Method to add an item to the slot
self.addItem = function (itemType) {
if (self.occupied) {
return false;
}
var itemAsset = itemType === 'bomb' ? 'bomb' : 'Gun';
var item = LK.getAsset(itemAsset, {
anchorX: 0.5,
anchorY: 0.5
});
self.addChild(item);
self.item = item;
self.occupied = true;
self.itemType = itemType;
return true;
};
// Method to remove item from slot
self.removeItem = function () {
if (!self.occupied) {
return null;
}
var itemType = self.itemType;
if (self.item) {
self.item.destroy();
self.item = null;
}
self.occupied = false;
self.itemType = null;
return itemType;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: Math.floor(Math.random() * 0xFFFFFF) // Random background color
});
/****
* Game Code
****/
// Placeholder ID for the logo
// Placeholder ID
// Placeholder ID
// --- Constants ---
// Engine will implicitly create assets based on usage below.
// Let's define the shapes we'll use:
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
var bombSlot;
var gunSlot;
var activeBullets = []; // Track bullets for updates
var lastShotTime = 0; // Used for rate limiting shots
var SHOT_COOLDOWN = 500; // Milliseconds between shots
// --- 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');
// Create slots for bomb and gun
bombSlot = new Slot('bomb', 200, 200);
gunSlot = new Slot('gun', 400, 200);
game.addChild(bombSlot);
game.addChild(gunSlot);
// Add bomb to bomb slot
bombSlot.addItem('bomb');
// Add gun to gun slot
gunSlot.addItem('gun');
}
// --- 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;
}
// Check if clicked on the gunSlot to shoot
if (gunSlot && gunSlot.occupied && Math.abs(x - gunSlot.x) < 50 && Math.abs(y - gunSlot.y) < 50) {
// Clicked on gun slot - enable shooting mode
var currentTime = Date.now();
if (currentTime - lastShotTime > SHOT_COOLDOWN) {
// Find a random enemy to target
if (enemies.length > 0) {
var randomEnemy = enemies[Math.floor(Math.random() * enemies.length)];
var playerPos = getWorldCoords(player.col, player.row);
// Create a bullet from the player position toward the enemy
var bullet = new Bullet(playerPos.x, playerPos.y, randomEnemy.x, randomEnemy.y);
game.addChild(bullet);
activeBullets.push(bullet);
// Play shoot sound
LK.getSound('placeBomb').play();
lastShotTime = currentTime;
}
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 && bombSlot && bombSlot.occupied) {
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;
}
// Update bullets
for (var b = activeBullets.length - 1; b >= 0; b--) {
var bullet = activeBullets[b];
// Update bullet position
bullet.update();
// Check for collision with enemies
for (var e = enemies.length - 1; e >= 0; e--) {
var enemy = enemies[e];
// Simple distance-based collision detection
var dx = bullet.x - enemy.x;
var dy = bullet.y - enemy.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 50) {
// Collision radius
// Enemy hit by bullet
LK.effects.flashObject(enemy, 0xffff00, 200);
LK.getSound('tinyMonsterDie').play();
// Destroy the enemy
enemy.destroy();
enemies.splice(e, 1);
// Add score
LK.setScore(LK.getScore() + 50);
scoreTxt.setText(LK.getScore());
// Destroy the bullet
bullet.destroy();
activeBullets.splice(b, 1);
break; // Bullet is used up
}
}
// Check if bullet was destroyed due to going off screen
if (bullet.parent && (bullet.x < 0 || bullet.x > 2048 || bullet.y < 0 || bullet.y > 2732)) {
bullet.destroy();
activeBullets.splice(b, 1);
}
}
// 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;
});
// Class for bullets fired from the gun
var Bullet = Container.expand(function (startX, startY, targetX, targetY) {
var self = Container.call(this);
// Create a simple bullet graphic
var bulletGraphics = self.attachAsset('powerup_bomb', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.5,
scaleY: 0.5
});
self.x = startX;
self.y = startY;
self.isBullet = true;
// Calculate direction vector
var dx = targetX - startX;
var dy = targetY - startY;
var magnitude = Math.sqrt(dx * dx + dy * dy);
self.velocityX = dx / magnitude * 15; // Speed factor
self.velocityY = dy / magnitude * 15;
// Store last position for collision detection
self.lastX = self.x;
self.lastY = self.y;
self.update = function () {
// Store last position
self.lastX = self.x;
self.lastY = self.y;
// Update position based on velocity
self.x += self.velocityX;
self.y += self.velocityY;
// Check if bullet is outside screen
if (self.x < 0 || self.x > 2048 || self.y < 0 || self.y > 2732) {
self.destroy();
}
};
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;
});
// Class for the weapon slot
var Slot = Container.expand(function (type, x, y) {
var self = Container.call(this);
var slotGraphics = self.attachAsset('Slot', {
anchorX: 0.5,
anchorY: 0.5
});
self.type = type; // 'bomb' or 'gun'
self.x = x;
self.y = y;
self.occupied = false;
self.item = null;
// Method to add an item to the slot
self.addItem = function (itemType) {
if (self.occupied) {
return false;
}
var itemAsset = itemType === 'bomb' ? 'bomb' : 'Gun';
var item = LK.getAsset(itemAsset, {
anchorX: 0.5,
anchorY: 0.5
});
self.addChild(item);
self.item = item;
self.occupied = true;
self.itemType = itemType;
return true;
};
// Method to remove item from slot
self.removeItem = function () {
if (!self.occupied) {
return null;
}
var itemType = self.itemType;
if (self.item) {
self.item.destroy();
self.item = null;
}
self.occupied = false;
self.itemType = null;
return itemType;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: Math.floor(Math.random() * 0xFFFFFF) // Random background color
});
/****
* Game Code
****/
// Placeholder ID for the logo
// Placeholder ID
// Placeholder ID
// --- Constants ---
// Engine will implicitly create assets based on usage below.
// Let's define the shapes we'll use:
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
var bombSlot;
var gunSlot;
var activeBullets = []; // Track bullets for updates
var lastShotTime = 0; // Used for rate limiting shots
var SHOT_COOLDOWN = 500; // Milliseconds between shots
// --- 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');
// Create slots for bomb and gun
bombSlot = new Slot('bomb', 200, 200);
gunSlot = new Slot('gun', 400, 200);
game.addChild(bombSlot);
game.addChild(gunSlot);
// Add bomb to bomb slot
bombSlot.addItem('bomb');
// Add gun to gun slot
gunSlot.addItem('gun');
}
// --- 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;
}
// Check if clicked on the gunSlot to shoot
if (gunSlot && gunSlot.occupied && Math.abs(x - gunSlot.x) < 50 && Math.abs(y - gunSlot.y) < 50) {
// Clicked on gun slot - enable shooting mode
var currentTime = Date.now();
if (currentTime - lastShotTime > SHOT_COOLDOWN) {
// Find a random enemy to target
if (enemies.length > 0) {
var randomEnemy = enemies[Math.floor(Math.random() * enemies.length)];
var playerPos = getWorldCoords(player.col, player.row);
// Create a bullet from the player position toward the enemy
var bullet = new Bullet(playerPos.x, playerPos.y, randomEnemy.x, randomEnemy.y);
game.addChild(bullet);
activeBullets.push(bullet);
// Play shoot sound
LK.getSound('placeBomb').play();
lastShotTime = currentTime;
}
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 && bombSlot && bombSlot.occupied) {
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;
}
// Update bullets
for (var b = activeBullets.length - 1; b >= 0; b--) {
var bullet = activeBullets[b];
// Update bullet position
bullet.update();
// Check for collision with enemies
for (var e = enemies.length - 1; e >= 0; e--) {
var enemy = enemies[e];
// Simple distance-based collision detection
var dx = bullet.x - enemy.x;
var dy = bullet.y - enemy.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 50) {
// Collision radius
// Enemy hit by bullet
LK.effects.flashObject(enemy, 0xffff00, 200);
LK.getSound('tinyMonsterDie').play();
// Destroy the enemy
enemy.destroy();
enemies.splice(e, 1);
// Add score
LK.setScore(LK.getScore() + 50);
scoreTxt.setText(LK.getScore());
// Destroy the bullet
bullet.destroy();
activeBullets.splice(b, 1);
break; // Bullet is used up
}
}
// Check if bullet was destroyed due to going off screen
if (bullet.parent && (bullet.x < 0 || bullet.x > 2048 || bullet.y < 0 || bullet.y > 2732)) {
bullet.destroy();
activeBullets.splice(b, 1);
}
}
// 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