/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ // Monster class for AI movement and attack var Monster = Container.expand(function () { var self = Container.call(this); self.type = null; self.col = 0; self.row = 0; self.targetCol = 0; self.targetRow = 0; self.lastCol = 0; self.lastRow = 0; self.moveSpeed = 1.2; // px per frame, much slower monsters self.path = null; self.pathStepIndex = 0; self.isMoving = false; self.attackCooldown = 0; self.attackDelay = 60; // frames between attacks self.asset = null; // Set up monster asset self.init = function (type, col, row) { self.type = type; self.col = col; self.row = row; self.targetCol = col; self.targetRow = row; var assetId = type; self.asset = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5, width: MAZE_CELL_SIZE * 0.8, height: MAZE_CELL_SIZE * 0.8 }); self.x = col * MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2; self.y = row * MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2; }; // Find path to player using BFS self.findPathToPlayer = function (playerCol, playerRow) { // Defensive: check bounds if (playerCol < 0 || playerCol >= MAZE_COLS || playerRow < 0 || playerRow >= MAZE_ROWS) return null; if (maze[playerRow][playerCol] === 1) return null; var queue = []; var visited = []; for (var y = 0; y < MAZE_ROWS; y++) { visited[y] = []; for (var x = 0; x < MAZE_COLS; x++) { visited[y][x] = false; } } var prev = []; for (var y = 0; y < MAZE_ROWS; y++) { prev[y] = []; for (var x = 0; x < MAZE_COLS; x++) { prev[y][x] = null; } } queue.push({ col: self.col, row: self.row }); visited[self.row][self.col] = true; var found = false; while (queue.length > 0) { var curr = queue.shift(); if (curr.col === playerCol && curr.row === playerRow) { found = true; break; } var dirs = [{ dc: 0, dr: -1 }, // up { dc: 0, dr: 1 }, // down { dc: -1, dr: 0 }, // left { dc: 1, dr: 0 } // right ]; for (var d = 0; d < dirs.length; d++) { var nc = curr.col + dirs[d].dc; var nr = curr.row + dirs[d].dr; if (nc >= 0 && nc < MAZE_COLS && nr >= 0 && nr < MAZE_ROWS && !visited[nr][nc] && maze[nr][nc] !== 1 && maze[nr][nc] !== 4) { queue.push({ col: nc, row: nr }); visited[nr][nc] = true; prev[nr][nc] = { col: curr.col, row: curr.row }; } } } if (!found) return null; // Reconstruct path var path = []; var currCol = playerCol, currRow = playerRow; while (!(currCol === self.col && currRow === self.row)) { path.unshift({ col: currCol, row: currRow }); var p = prev[currRow][currCol]; currCol = p.col; currRow = p.row; } return path; }; // Monster update: move toward player, attack if adjacent self.update = function () { // Track last position for event logic self.lastCol = self.col; self.lastRow = self.row; // Only recalc path every 15 frames for performance if (LK.ticks % 15 === 0 || !self.path || self.path.length === 0) { self.path = self.findPathToPlayer(charCol, charRow); self.pathStepIndex = 0; } // If adjacent to player, attack var dx = Math.abs(self.col - charCol); var dy = Math.abs(self.row - charRow); if (dx + dy === 1) { // Attack cooldown if (self.attackCooldown <= 0) { // Show attack effect var effect = LK.getAsset('attackEffect', { anchorX: 0.5, anchorY: 0.5, x: charNode.x, y: charNode.y, width: MAZE_CELL_SIZE * 0.7, height: MAZE_CELL_SIZE * 0.7 }); effect.alpha = 1; game.addChild(effect); // Defensive: ensure effect is defined and tween is a function if (effect && typeof tween === "function") { var tweenObj = tween(effect); if (tweenObj && typeof tweenObj.to === "function") { tweenObj.to({ alpha: 0 }, 400).onComplete(function () { effect.destroy(); }); } } else if (effect) { // fallback: just destroy after a timeout if tween is not available LK.setTimeout(function () { effect.destroy(); }, 400); } // Flash screen red and handle lives/game over LK.effects.flashScreen(0xff0000, 800); if (typeof lives === "undefined") lives = 3; if (typeof updateLivesDisplay === "function") updateLivesDisplay(); if (lives > 1) { lives--; if (typeof updateLivesDisplay === "function") updateLivesDisplay(); // Respawn player at start after short delay var oldCol = charCol, oldRow = charRow; var startCol = 1, startRow = 1; // Find start cell for (var y = 0; y < MAZE_ROWS; y++) { for (var x = 0; x < MAZE_COLS; x++) { if (maze[y][x] === 2) { startCol = x; startRow = y; } } } // Move player to start after 800ms LK.setTimeout(function () { charCol = startCol; charRow = startRow; charNode.x = charCol * MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2; charNode.y = charRow * MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2; moveTargetCol = charCol; moveTargetRow = charRow; isMoving = false; if (typeof updateLivesDisplay === "function") updateLivesDisplay(); }, 800); } else { // Out of lives: show game over lives = 0; if (typeof updateLivesDisplay === "function") updateLivesDisplay(); LK.showGameOver(); } self.attackCooldown = self.attackDelay; } } else if (self.path && self.path.length > 0 && self.pathStepIndex < self.path.length) { // Move toward next cell in path var next = self.path[self.pathStepIndex]; var targetX = next.col * MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2; var targetY = next.row * MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2; var ddx = targetX - self.x; var ddy = targetY - self.y; var dist = Math.sqrt(ddx * ddx + ddy * ddy); if (dist > 2) { var step = Math.min(self.moveSpeed, dist); self.x += ddx / dist * step; self.y += ddy / dist * step; } else { // Snap to cell self.x = targetX; self.y = targetY; self.col = next.col; self.row = next.row; self.pathStepIndex++; } } if (self.attackCooldown > 0) self.attackCooldown--; }; return self; }); /**** * Initialize Game ****/ // No classes needed for maze logic var game = new LK.Game({ backgroundColor: 0x181818 }); /**** * Game Code ****/ // Game constants var GAME_WIDTH = 2048; var GAME_HEIGHT = 2732; // --- Random speed variables for player and monsters, set per level --- var playerSpeed = 20; // px per frame (default, will be randomized) var monsterSpeed = 1.2; // px per frame (default, will be randomized) // Maze configuration // 0 = empty, 1 = wall, 2 = start, 3 = goal, 4 = door (closed) var MAZE_CELL_SIZE = 128; var MAZE_COLS = 16; var MAZE_ROWS = 21; var maze = []; // Generate a random maze for the first level (and every level) function generateRandomMaze() { // Always outer walls for (var y = 0; y < MAZE_ROWS; y++) { maze[y] = []; for (var x = 0; x < MAZE_COLS; x++) { if (y === 0 || y === MAZE_ROWS - 1 || x === 0 || x === MAZE_COLS - 1) { maze[y][x] = 1; } else { // Randomly place walls, doors, or empty var r = Math.random(); if (r < 0.13) { maze[y][x] = 1; // wall } else if (r < 0.16) { maze[y][x] = 4; // door } else { maze[y][x] = 0; // empty } } } } // Pick random start and goal positions (not on wall/door) function randomEmptyCell() { var tries = 0; while (tries < 1000) { var rx = 1 + Math.floor(Math.random() * (MAZE_COLS - 2)); var ry = 1 + Math.floor(Math.random() * (MAZE_ROWS - 2)); if (maze[ry][rx] === 0) return { col: rx, row: ry }; tries++; } // fallback return { col: 1, row: 1 }; } var start = randomEmptyCell(); maze[start.row][start.col] = 2; var goal = randomEmptyCell(); while (goal.col === start.col && goal.row === start.row) { goal = randomEmptyCell(); } maze[goal.row][goal.col] = 3; return { start: start, goal: goal }; } // Generate the first maze at game start var mazePositions = generateRandomMaze(); // Play Bonelab theme music and repeat it at the start of level 1 LK.playMusic('bonelab_theme', { loop: true }); // England time display (London, UK) var englandTimeTxt = new Text2('', { size: 90, fill: 0xffffff }); englandTimeTxt.anchor.set(0.5, 0); // Place below the top edge, but not in the top left 100x100px (menu area) englandTimeTxt.x = GAME_WIDTH / 2; // Place at the top of the abyss (top of maze area, not screen), just below the first maze row englandTimeTxt.y = MAZE_CELL_SIZE + 10; // 10px below the top maze wall game.addChild(englandTimeTxt); // Update England time every second LK.setInterval(function () { // Get current time in UTC, then add 0 for GMT or 1 for BST (British Summer Time) var now = new Date(); // Calculate if BST is in effect (last Sunday in March to last Sunday in October) var year = now.getUTCFullYear(); // Last Sunday in March var startBST = new Date(Date.UTC(year, 2, 31)); startBST.setUTCDate(31 - startBST.getUTCDay()); // Last Sunday in October var endBST = new Date(Date.UTC(year, 9, 31)); endBST.setUTCDate(31 - endBST.getUTCDay()); var isBST = now >= startBST && now < endBST; var offset = isBST ? 1 : 0; var hours = now.getUTCHours() + offset; if (hours < 0) hours += 24; if (hours > 23) hours -= 24; var mins = now.getUTCMinutes(); var secs = now.getUTCSeconds(); var pad = function pad(n) { return n < 10 ? '0' + n : n; }; var label = isBST ? "BST" : "GMT"; englandTimeTxt.setText("England Time: " + pad(hours) + ":" + pad(mins) + ":" + pad(secs) + " " + label); }, 1000); // Draw maze walls and monsters var wallNodes = []; var monsterNodes = []; // --- Mom with the bag message logic --- var momMsgNode = null; function showMomWithBagMessage() { // If already showing, don't show again if (momMsgNode && momMsgNode.parent) return; momMsgNode = new Text2("The man with the bag is coming.", { size: 110, fill: 0xFF66CC }); momMsgNode.anchor.set(0.5, 0.5); // Place at a random position in the visible game area, not in the top left 100x100px var margin = 120; var minX = margin, maxX = GAME_WIDTH - margin; var minY = margin + MAZE_CELL_SIZE, maxY = GAME_HEIGHT - margin; momMsgNode.x = minX + Math.random() * (maxX - minX); momMsgNode.y = minY + Math.random() * (maxY - minY); game.addChild(momMsgNode); // Hide after 2.5 seconds LK.setTimeout(function () { if (momMsgNode && momMsgNode.parent) momMsgNode.destroy(); }, 2500); } // Randomly trigger the message every 8-18 seconds function scheduleMomMsg() { var nextDelay = 8000 + Math.floor(Math.random() * 10000); // 8-18s LK.setTimeout(function () { showMomWithBagMessage(); scheduleMomMsg(); }, nextDelay); } scheduleMomMsg(); // Place monsters at specific hard maze locations for challenge var monsterSpawns = [{ type: 'skeleton', col: 5, row: 5 }, { type: 'goldenSkeleton', col: 7, row: 15 }, { type: 'zombie', col: 10, row: 10 }, { type: 'zombieSkeleton', col: 13, row: 8 }, { type: 'Ginger', col: 3, row: 13 }]; // Add asset for zombieSkeleton if not present for (var y = 0; y < MAZE_ROWS; y++) { // Defensive: check maze[y] is defined before accessing maze[y][x] if (!maze[y]) continue; for (var x = 0; x < MAZE_COLS; x++) { if (maze[y][x] === 1) { var wall = LK.getAsset('base', { anchorX: 0.5, anchorY: 0.5, x: x * MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2, y: y * MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2, width: MAZE_CELL_SIZE, height: MAZE_CELL_SIZE }); wallNodes.push(wall); game.addChild(wall); } // Render doors as special clickable assets if (maze[y][x] === 4) { // Use a different color for doors (yellowish) var door = LK.getAsset('base', { anchorX: 0.5, anchorY: 0.5, x: x * MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2, y: y * MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2, width: MAZE_CELL_SIZE, height: MAZE_CELL_SIZE, tint: 0xFFD700 // gold/yellow }); door._mazeCol = x; door._mazeRow = y; // Make door clickable: open on tap door.down = function (lx, ly, obj) { // Defensive: check if still a door var col = this._mazeCol; var row = this._mazeRow; if (maze[row][col] === 4) { // Open the door: set to empty maze[row][col] = 0; // Remove the door node this.destroy(); } }; wallNodes.push(door); game.addChild(door); } if (maze[y][x] === 3) { // Goal marker var goalNode = LK.getAsset('skeleton', { anchorX: 0.5, anchorY: 0.5, x: x * MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2, y: y * MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2, width: MAZE_CELL_SIZE, height: MAZE_CELL_SIZE }); game.addChild(goalNode); } // Place monsters at their spawn locations for (var mi = 0; mi < monsterSpawns.length; mi++) { var m = monsterSpawns[mi]; if (m.col === x && m.row === y) { var monster = new Monster(); monster.init(m.type, x, y); // Assign default speed to this monster monster.moveSpeed = monsterSpeed; monsterNodes.push(monster); game.addChild(monster); } } } } // Find start position var charCol = 1, charRow = 1; for (var y = 0; y < MAZE_ROWS; y++) { for (var x = 0; x < MAZE_COLS; x++) { if (maze[y][x] === 2) { charCol = x; charRow = y; } } } // Create player character node and add to game var charNode = LK.getAsset('Player', { anchorX: 0.5, anchorY: 0.5, x: charCol * MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2, y: charRow * MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2, width: MAZE_CELL_SIZE * 0.8, height: MAZE_CELL_SIZE * 0.8 }); game.addChild(charNode); // Movement state var moveTargetCol = charCol; var moveTargetRow = charRow; var isMoving = false; var moveSpeed = 20; // px per frame (increased for faster player movement) // Touch/drag movement: tap or drag to adjacent cell function getCellFromPos(x, y) { var col = Math.floor(x / MAZE_CELL_SIZE); var row = Math.floor(y / MAZE_CELL_SIZE); return { col: col, row: row }; } // Simple BFS pathfinding to allow tap-to-move to any reachable cell function findPath(startCol, startRow, endCol, endRow) { // Defensive: check bounds if (endCol < 0 || endCol >= MAZE_COLS || endRow < 0 || endRow >= MAZE_ROWS) return null; if (maze[endRow][endCol] === 1) return null; var queue = []; var visited = []; for (var y = 0; y < MAZE_ROWS; y++) { visited[y] = []; for (var x = 0; x < MAZE_COLS; x++) { visited[y][x] = false; } } var prev = []; for (var y = 0; y < MAZE_ROWS; y++) { prev[y] = []; for (var x = 0; x < MAZE_COLS; x++) { prev[y][x] = null; } } queue.push({ col: startCol, row: startRow }); visited[startRow][startCol] = true; var found = false; while (queue.length > 0) { var curr = queue.shift(); if (curr.col === endCol && curr.row === endRow) { found = true; break; } var dirs = [{ dc: 0, dr: -1 }, // up { dc: 0, dr: 1 }, // down { dc: -1, dr: 0 }, // left { dc: 1, dr: 0 } // right ]; for (var d = 0; d < dirs.length; d++) { var nc = curr.col + dirs[d].dc; var nr = curr.row + dirs[d].dr; if (nc >= 0 && nc < MAZE_COLS && nr >= 0 && nr < MAZE_ROWS && !visited[nr][nc] && maze[nr][nc] !== 1 && maze[nr][nc] !== 4) { queue.push({ col: nc, row: nr }); visited[nr][nc] = true; prev[nr][nc] = { col: curr.col, row: curr.row }; } } } if (!found) return null; // Reconstruct path var path = []; var currCol = endCol, currRow = endRow; while (!(currCol === startCol && currRow === startRow)) { path.unshift({ col: currCol, row: currRow }); var p = prev[currRow][currCol]; currCol = p.col; currRow = p.row; } return path; } var pathToFollow = null; var pathStepIndex = 0; game.down = function (x, y, obj) { // Prevent new move if already moving if (isMoving) return; // Compute direction vector from player to tap/click var px = charNode.x; var py = charNode.y; var dx = x - px; var dy = y - py; var dist = Math.sqrt(dx * dx + dy * dy); // Defensive: don't move if tap is too close to player if (dist < 10) return; // Normalize direction var dirX = dx / dist; var dirY = dy / dist; // Compute the furthest cell in that direction that is not a wall/door var currCol = charCol; var currRow = charRow; var lastFreeCol = charCol; var lastFreeRow = charRow; var maxSteps = 50; // Prevent infinite loop for (var step = 0; step < maxSteps; step++) { // Move a small step in direction var nextX = px + dirX * (step + 1) * MAZE_CELL_SIZE * 0.5; var nextY = py + dirY * (step + 1) * MAZE_CELL_SIZE * 0.5; var cell = getCellFromPos(nextX, nextY); // Defensive: check bounds if (cell.col < 0 || cell.col >= MAZE_COLS || cell.row < 0 || cell.row >= MAZE_ROWS) break; // Stop at wall or door if (maze[cell.row][cell.col] === 1 || maze[cell.row][cell.col] === 4) break; lastFreeCol = cell.col; lastFreeRow = cell.row; } // Don't move if already at that cell if (lastFreeCol === charCol && lastFreeRow === charRow) return; moveTargetCol = lastFreeCol; moveTargetRow = lastFreeRow; isMoving = true; pathToFollow = null; pathStepIndex = 0; }; game.move = function (x, y, obj) { // Only allow drag-move if not already moving if (!isMoving) { game.down(x, y, obj); } }; game.up = function (x, y, obj) { // No-op }; game.update = function () { // Track lastCol and lastRow for player if (typeof game.lastCol === "undefined") game.lastCol = charCol; if (typeof game.lastRow === "undefined") game.lastRow = charRow; // Move character toward target cell var targetX = moveTargetCol * MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2; var targetY = moveTargetRow * MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2; var dx = targetX - charNode.x; var dy = targetY - charNode.y; var dist = Math.sqrt(dx * dx + dy * dy); if (isMoving && dist > 2) { var step = Math.min(playerSpeed, dist); charNode.x += dx / dist * step; charNode.y += dy / dist * step; } else if (isMoving) { // Snap to cell charNode.x = targetX; charNode.y = targetY; // Update lastCol/lastRow before changing charCol/charRow game.lastCol = charCol; game.lastRow = charRow; charCol = moveTargetCol; charRow = moveTargetRow; // If following a path, advance to next step if (typeof pathToFollow !== "undefined" && pathToFollow && pathStepIndex < pathToFollow.length - 1) { pathStepIndex++; moveTargetCol = pathToFollow[pathStepIndex].col; moveTargetRow = pathToFollow[pathStepIndex].row; // Remain isMoving = true } else { isMoving = false; pathToFollow = null; pathStepIndex = 0; // Check for goal using cell coordinates and last cell coordinates var goalCell = false; for (var y = 0; y < MAZE_ROWS; y++) { for (var x = 0; x < MAZE_COLS; x++) { if (maze[y][x] === 3) { // Only trigger if player just entered the goal cell this frame var wasOutside = game.lastCol !== x || game.lastRow !== y; var isInside = charCol === x && charRow === y; if (wasOutside && isInside) { goalCell = true; break; } } } if (goalCell) break; } if (goalCell) { LK.effects.flashScreen(0x00ff00, 800); LK.setTimeout(function () { startNextLevel(); }, 1000); return; } } } // Update all monsters for (var i = 0; i < monsterNodes.length; i++) { if (monsterNodes[i] && typeof monsterNodes[i].update === "function") { monsterNodes[i].update(); } } }; // --- LIVES SYSTEM --- // Number of lives per full game (reset only on full restart) var MAX_LIVES = 3; var lives = MAX_LIVES; // Reset lives to 3 on full game restart if (typeof LK !== "undefined" && typeof LK.on === "function") { LK.on("gameStart", function () { lives = MAX_LIVES; if (typeof updateLivesDisplay === "function") updateLivesDisplay(); }); } // GUI: Show lives as hearts at top right var heartNodes = []; var livesTxt = null; function updateLivesDisplay() { // Remove old hearts if any for (var i = 0; i < heartNodes.length; i++) { if (heartNodes[i] && typeof heartNodes[i].destroy === "function") { heartNodes[i].destroy(); } } heartNodes.length = 0; // Draw hearts for each life var heartSize = 90; var margin = 18; var startX = GAME_WIDTH - 40 - (MAX_LIVES - 1) * (heartSize + margin); var y = MAZE_CELL_SIZE + 10; for (var i = 0; i < MAX_LIVES; i++) { var heartAsset = LK.getAsset('Exiy', { anchorX: 0.5, anchorY: 0, width: heartSize, height: heartSize, x: startX + i * (heartSize + margin), y: y }); if (i >= lives) { heartAsset.alpha = 0.25; // faded for lost life } else { heartAsset.alpha = 1; } heartNodes.push(heartAsset); game.addChild(heartAsset); } // Optionally, keep the text for accessibility if (!livesTxt) { livesTxt = new Text2("", { size: 60, fill: 0xFFD700 }); livesTxt.anchor.set(1, 0); // top right livesTxt.x = GAME_WIDTH - 40; livesTxt.y = y + heartSize + 2; game.addChild(livesTxt); } livesTxt.setText("x" + lives); } // Infinite levels: generate a new random maze and monsters each time function startNextLevel() { // Do not restore lives at start of each level; lives only reset on full game restart updateLivesDisplay(); // Remove all wall nodes and goal node from previous level for (var i = 0; i < wallNodes.length; i++) { if (wallNodes[i] && typeof wallNodes[i].destroy === "function") { wallNodes[i].destroy(); } } wallNodes.length = 0; // Remove all monsters for (var i = 0; i < monsterNodes.length; i++) { if (monsterNodes[i] && typeof monsterNodes[i].destroy === "function") { monsterNodes[i].destroy(); } } monsterNodes.length = 0; // Set default player and monster speed for this level playerSpeed = 20; // Player speed: 20 px/frame monsterSpeed = 1.2; // Monster speed: 1.2 px/frame // Generate a random maze // 0 = empty, 1 = wall, 2 = start, 3 = goal, 4 = door (closed) // Always outer walls for (var y = 0; y < MAZE_ROWS; y++) { maze[y] = []; for (var x = 0; x < MAZE_COLS; x++) { if (y === 0 || y === MAZE_ROWS - 1 || x === 0 || x === MAZE_COLS - 1) { maze[y][x] = 1; } else { // Randomly place walls, doors, or empty var r = Math.random(); if (r < 0.13) { maze[y][x] = 1; // wall } else if (r < 0.16) { maze[y][x] = 4; // door } else { maze[y][x] = 0; // empty } } } } // Pick random start and goal positions (not on wall/door) function randomEmptyCell() { var tries = 0; while (tries < 1000) { var rx = 1 + Math.floor(Math.random() * (MAZE_COLS - 2)); var ry = 1 + Math.floor(Math.random() * (MAZE_ROWS - 2)); if (maze[ry][rx] === 0) return { col: rx, row: ry }; tries++; } // fallback return { col: 1, row: 1 }; } var start = randomEmptyCell(); maze[start.row][start.col] = 2; var goal = randomEmptyCell(); while (goal.col === start.col && goal.row === start.row) { goal = randomEmptyCell(); } maze[goal.row][goal.col] = 3; // Redraw maze walls, doors, and goal for (var y = 0; y < MAZE_ROWS; y++) { for (var x = 0; x < MAZE_COLS; x++) { if (maze[y][x] === 1) { var wall = LK.getAsset('base', { anchorX: 0.5, anchorY: 0.5, x: x * MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2, y: y * MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2, width: MAZE_CELL_SIZE, height: MAZE_CELL_SIZE }); wallNodes.push(wall); game.addChild(wall); } if (maze[y][x] === 4) { var door = LK.getAsset('base', { anchorX: 0.5, anchorY: 0.5, x: x * MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2, y: y * MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2, width: MAZE_CELL_SIZE, height: MAZE_CELL_SIZE, tint: 0xFFD700 }); door._mazeCol = x; door._mazeRow = y; door.down = function (lx, ly, obj) { var col = this._mazeCol; var row = this._mazeRow; if (maze[row][col] === 4) { maze[row][col] = 0; this.destroy(); } }; wallNodes.push(door); game.addChild(door); } if (maze[y][x] === 3) { var goalNode = LK.getAsset('skeleton', { anchorX: 0.5, anchorY: 0.5, x: x * MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2, y: y * MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2, width: MAZE_CELL_SIZE, height: MAZE_CELL_SIZE }); game.addChild(goalNode); } } } // Place new monsters for this level // Place 3-5 monsters at random empty cells (not start/goal) var monsterTypes = ['skeleton', 'goldenSkeleton', 'zombie', 'zombieSkeleton', 'Ginger']; var monsterCount = 3 + Math.floor(Math.random() * 3); var monsterSpots = []; for (var i = 0; i < monsterCount; i++) { var spot = randomEmptyCell(); // Don't place on start or goal while (spot.col === start.col && spot.row === start.row || spot.col === goal.col && spot.row === goal.row || monsterSpots.some(function (ms) { return ms.col === spot.col && ms.row === spot.row; })) { spot = randomEmptyCell(); } monsterSpots.push(spot); var mtype = monsterTypes[Math.floor(Math.random() * monsterTypes.length)]; var monster = new Monster(); monster.init(mtype, spot.col, spot.row); // Assign default speed to this monster monster.moveSpeed = monsterSpeed; monsterNodes.push(monster); game.addChild(monster); } // Reset player to new start charCol = start.col; charRow = start.row; charNode.x = charCol * MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2; charNode.y = charRow * MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2; moveTargetCol = charCol; moveTargetRow = charRow; isMoving = false; // Show a message for new level start if (typeof startNextLevel.level === "undefined") startNextLevel.level = 2;else startNextLevel.level++; var msg = new Text2("Level " + startNextLevel.level + "!", { size: 100, fill: 0x00FFAA }); msg.anchor.set(0.5, 0.5); msg.x = GAME_WIDTH / 2; msg.y = GAME_HEIGHT / 2 + 200; game.addChild(msg); LK.setTimeout(function () { msg.destroy(); }, 1500); // Play Bonelab theme music and repeat it every level LK.playMusic('bonelab_theme', { loop: true }); }
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Monster class for AI movement and attack
var Monster = Container.expand(function () {
var self = Container.call(this);
self.type = null;
self.col = 0;
self.row = 0;
self.targetCol = 0;
self.targetRow = 0;
self.lastCol = 0;
self.lastRow = 0;
self.moveSpeed = 1.2; // px per frame, much slower monsters
self.path = null;
self.pathStepIndex = 0;
self.isMoving = false;
self.attackCooldown = 0;
self.attackDelay = 60; // frames between attacks
self.asset = null;
// Set up monster asset
self.init = function (type, col, row) {
self.type = type;
self.col = col;
self.row = row;
self.targetCol = col;
self.targetRow = row;
var assetId = type;
self.asset = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5,
width: MAZE_CELL_SIZE * 0.8,
height: MAZE_CELL_SIZE * 0.8
});
self.x = col * MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2;
self.y = row * MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2;
};
// Find path to player using BFS
self.findPathToPlayer = function (playerCol, playerRow) {
// Defensive: check bounds
if (playerCol < 0 || playerCol >= MAZE_COLS || playerRow < 0 || playerRow >= MAZE_ROWS) return null;
if (maze[playerRow][playerCol] === 1) return null;
var queue = [];
var visited = [];
for (var y = 0; y < MAZE_ROWS; y++) {
visited[y] = [];
for (var x = 0; x < MAZE_COLS; x++) {
visited[y][x] = false;
}
}
var prev = [];
for (var y = 0; y < MAZE_ROWS; y++) {
prev[y] = [];
for (var x = 0; x < MAZE_COLS; x++) {
prev[y][x] = null;
}
}
queue.push({
col: self.col,
row: self.row
});
visited[self.row][self.col] = true;
var found = false;
while (queue.length > 0) {
var curr = queue.shift();
if (curr.col === playerCol && curr.row === playerRow) {
found = true;
break;
}
var dirs = [{
dc: 0,
dr: -1
},
// up
{
dc: 0,
dr: 1
},
// down
{
dc: -1,
dr: 0
},
// left
{
dc: 1,
dr: 0
} // right
];
for (var d = 0; d < dirs.length; d++) {
var nc = curr.col + dirs[d].dc;
var nr = curr.row + dirs[d].dr;
if (nc >= 0 && nc < MAZE_COLS && nr >= 0 && nr < MAZE_ROWS && !visited[nr][nc] && maze[nr][nc] !== 1 && maze[nr][nc] !== 4) {
queue.push({
col: nc,
row: nr
});
visited[nr][nc] = true;
prev[nr][nc] = {
col: curr.col,
row: curr.row
};
}
}
}
if (!found) return null;
// Reconstruct path
var path = [];
var currCol = playerCol,
currRow = playerRow;
while (!(currCol === self.col && currRow === self.row)) {
path.unshift({
col: currCol,
row: currRow
});
var p = prev[currRow][currCol];
currCol = p.col;
currRow = p.row;
}
return path;
};
// Monster update: move toward player, attack if adjacent
self.update = function () {
// Track last position for event logic
self.lastCol = self.col;
self.lastRow = self.row;
// Only recalc path every 15 frames for performance
if (LK.ticks % 15 === 0 || !self.path || self.path.length === 0) {
self.path = self.findPathToPlayer(charCol, charRow);
self.pathStepIndex = 0;
}
// If adjacent to player, attack
var dx = Math.abs(self.col - charCol);
var dy = Math.abs(self.row - charRow);
if (dx + dy === 1) {
// Attack cooldown
if (self.attackCooldown <= 0) {
// Show attack effect
var effect = LK.getAsset('attackEffect', {
anchorX: 0.5,
anchorY: 0.5,
x: charNode.x,
y: charNode.y,
width: MAZE_CELL_SIZE * 0.7,
height: MAZE_CELL_SIZE * 0.7
});
effect.alpha = 1;
game.addChild(effect);
// Defensive: ensure effect is defined and tween is a function
if (effect && typeof tween === "function") {
var tweenObj = tween(effect);
if (tweenObj && typeof tweenObj.to === "function") {
tweenObj.to({
alpha: 0
}, 400).onComplete(function () {
effect.destroy();
});
}
} else if (effect) {
// fallback: just destroy after a timeout if tween is not available
LK.setTimeout(function () {
effect.destroy();
}, 400);
}
// Flash screen red and handle lives/game over
LK.effects.flashScreen(0xff0000, 800);
if (typeof lives === "undefined") lives = 3;
if (typeof updateLivesDisplay === "function") updateLivesDisplay();
if (lives > 1) {
lives--;
if (typeof updateLivesDisplay === "function") updateLivesDisplay();
// Respawn player at start after short delay
var oldCol = charCol,
oldRow = charRow;
var startCol = 1,
startRow = 1;
// Find start cell
for (var y = 0; y < MAZE_ROWS; y++) {
for (var x = 0; x < MAZE_COLS; x++) {
if (maze[y][x] === 2) {
startCol = x;
startRow = y;
}
}
}
// Move player to start after 800ms
LK.setTimeout(function () {
charCol = startCol;
charRow = startRow;
charNode.x = charCol * MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2;
charNode.y = charRow * MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2;
moveTargetCol = charCol;
moveTargetRow = charRow;
isMoving = false;
if (typeof updateLivesDisplay === "function") updateLivesDisplay();
}, 800);
} else {
// Out of lives: show game over
lives = 0;
if (typeof updateLivesDisplay === "function") updateLivesDisplay();
LK.showGameOver();
}
self.attackCooldown = self.attackDelay;
}
} else if (self.path && self.path.length > 0 && self.pathStepIndex < self.path.length) {
// Move toward next cell in path
var next = self.path[self.pathStepIndex];
var targetX = next.col * MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2;
var targetY = next.row * MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2;
var ddx = targetX - self.x;
var ddy = targetY - self.y;
var dist = Math.sqrt(ddx * ddx + ddy * ddy);
if (dist > 2) {
var step = Math.min(self.moveSpeed, dist);
self.x += ddx / dist * step;
self.y += ddy / dist * step;
} else {
// Snap to cell
self.x = targetX;
self.y = targetY;
self.col = next.col;
self.row = next.row;
self.pathStepIndex++;
}
}
if (self.attackCooldown > 0) self.attackCooldown--;
};
return self;
});
/****
* Initialize Game
****/
// No classes needed for maze logic
var game = new LK.Game({
backgroundColor: 0x181818
});
/****
* Game Code
****/
// Game constants
var GAME_WIDTH = 2048;
var GAME_HEIGHT = 2732;
// --- Random speed variables for player and monsters, set per level ---
var playerSpeed = 20; // px per frame (default, will be randomized)
var monsterSpeed = 1.2; // px per frame (default, will be randomized)
// Maze configuration
// 0 = empty, 1 = wall, 2 = start, 3 = goal, 4 = door (closed)
var MAZE_CELL_SIZE = 128;
var MAZE_COLS = 16;
var MAZE_ROWS = 21;
var maze = [];
// Generate a random maze for the first level (and every level)
function generateRandomMaze() {
// Always outer walls
for (var y = 0; y < MAZE_ROWS; y++) {
maze[y] = [];
for (var x = 0; x < MAZE_COLS; x++) {
if (y === 0 || y === MAZE_ROWS - 1 || x === 0 || x === MAZE_COLS - 1) {
maze[y][x] = 1;
} else {
// Randomly place walls, doors, or empty
var r = Math.random();
if (r < 0.13) {
maze[y][x] = 1; // wall
} else if (r < 0.16) {
maze[y][x] = 4; // door
} else {
maze[y][x] = 0; // empty
}
}
}
}
// Pick random start and goal positions (not on wall/door)
function randomEmptyCell() {
var tries = 0;
while (tries < 1000) {
var rx = 1 + Math.floor(Math.random() * (MAZE_COLS - 2));
var ry = 1 + Math.floor(Math.random() * (MAZE_ROWS - 2));
if (maze[ry][rx] === 0) return {
col: rx,
row: ry
};
tries++;
}
// fallback
return {
col: 1,
row: 1
};
}
var start = randomEmptyCell();
maze[start.row][start.col] = 2;
var goal = randomEmptyCell();
while (goal.col === start.col && goal.row === start.row) {
goal = randomEmptyCell();
}
maze[goal.row][goal.col] = 3;
return {
start: start,
goal: goal
};
}
// Generate the first maze at game start
var mazePositions = generateRandomMaze();
// Play Bonelab theme music and repeat it at the start of level 1
LK.playMusic('bonelab_theme', {
loop: true
});
// England time display (London, UK)
var englandTimeTxt = new Text2('', {
size: 90,
fill: 0xffffff
});
englandTimeTxt.anchor.set(0.5, 0);
// Place below the top edge, but not in the top left 100x100px (menu area)
englandTimeTxt.x = GAME_WIDTH / 2;
// Place at the top of the abyss (top of maze area, not screen), just below the first maze row
englandTimeTxt.y = MAZE_CELL_SIZE + 10; // 10px below the top maze wall
game.addChild(englandTimeTxt);
// Update England time every second
LK.setInterval(function () {
// Get current time in UTC, then add 0 for GMT or 1 for BST (British Summer Time)
var now = new Date();
// Calculate if BST is in effect (last Sunday in March to last Sunday in October)
var year = now.getUTCFullYear();
// Last Sunday in March
var startBST = new Date(Date.UTC(year, 2, 31));
startBST.setUTCDate(31 - startBST.getUTCDay());
// Last Sunday in October
var endBST = new Date(Date.UTC(year, 9, 31));
endBST.setUTCDate(31 - endBST.getUTCDay());
var isBST = now >= startBST && now < endBST;
var offset = isBST ? 1 : 0;
var hours = now.getUTCHours() + offset;
if (hours < 0) hours += 24;
if (hours > 23) hours -= 24;
var mins = now.getUTCMinutes();
var secs = now.getUTCSeconds();
var pad = function pad(n) {
return n < 10 ? '0' + n : n;
};
var label = isBST ? "BST" : "GMT";
englandTimeTxt.setText("England Time: " + pad(hours) + ":" + pad(mins) + ":" + pad(secs) + " " + label);
}, 1000);
// Draw maze walls and monsters
var wallNodes = [];
var monsterNodes = [];
// --- Mom with the bag message logic ---
var momMsgNode = null;
function showMomWithBagMessage() {
// If already showing, don't show again
if (momMsgNode && momMsgNode.parent) return;
momMsgNode = new Text2("The man with the bag is coming.", {
size: 110,
fill: 0xFF66CC
});
momMsgNode.anchor.set(0.5, 0.5);
// Place at a random position in the visible game area, not in the top left 100x100px
var margin = 120;
var minX = margin,
maxX = GAME_WIDTH - margin;
var minY = margin + MAZE_CELL_SIZE,
maxY = GAME_HEIGHT - margin;
momMsgNode.x = minX + Math.random() * (maxX - minX);
momMsgNode.y = minY + Math.random() * (maxY - minY);
game.addChild(momMsgNode);
// Hide after 2.5 seconds
LK.setTimeout(function () {
if (momMsgNode && momMsgNode.parent) momMsgNode.destroy();
}, 2500);
}
// Randomly trigger the message every 8-18 seconds
function scheduleMomMsg() {
var nextDelay = 8000 + Math.floor(Math.random() * 10000); // 8-18s
LK.setTimeout(function () {
showMomWithBagMessage();
scheduleMomMsg();
}, nextDelay);
}
scheduleMomMsg();
// Place monsters at specific hard maze locations for challenge
var monsterSpawns = [{
type: 'skeleton',
col: 5,
row: 5
}, {
type: 'goldenSkeleton',
col: 7,
row: 15
}, {
type: 'zombie',
col: 10,
row: 10
}, {
type: 'zombieSkeleton',
col: 13,
row: 8
}, {
type: 'Ginger',
col: 3,
row: 13
}];
// Add asset for zombieSkeleton if not present
for (var y = 0; y < MAZE_ROWS; y++) {
// Defensive: check maze[y] is defined before accessing maze[y][x]
if (!maze[y]) continue;
for (var x = 0; x < MAZE_COLS; x++) {
if (maze[y][x] === 1) {
var wall = LK.getAsset('base', {
anchorX: 0.5,
anchorY: 0.5,
x: x * MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2,
y: y * MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2,
width: MAZE_CELL_SIZE,
height: MAZE_CELL_SIZE
});
wallNodes.push(wall);
game.addChild(wall);
}
// Render doors as special clickable assets
if (maze[y][x] === 4) {
// Use a different color for doors (yellowish)
var door = LK.getAsset('base', {
anchorX: 0.5,
anchorY: 0.5,
x: x * MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2,
y: y * MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2,
width: MAZE_CELL_SIZE,
height: MAZE_CELL_SIZE,
tint: 0xFFD700 // gold/yellow
});
door._mazeCol = x;
door._mazeRow = y;
// Make door clickable: open on tap
door.down = function (lx, ly, obj) {
// Defensive: check if still a door
var col = this._mazeCol;
var row = this._mazeRow;
if (maze[row][col] === 4) {
// Open the door: set to empty
maze[row][col] = 0;
// Remove the door node
this.destroy();
}
};
wallNodes.push(door);
game.addChild(door);
}
if (maze[y][x] === 3) {
// Goal marker
var goalNode = LK.getAsset('skeleton', {
anchorX: 0.5,
anchorY: 0.5,
x: x * MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2,
y: y * MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2,
width: MAZE_CELL_SIZE,
height: MAZE_CELL_SIZE
});
game.addChild(goalNode);
}
// Place monsters at their spawn locations
for (var mi = 0; mi < monsterSpawns.length; mi++) {
var m = monsterSpawns[mi];
if (m.col === x && m.row === y) {
var monster = new Monster();
monster.init(m.type, x, y);
// Assign default speed to this monster
monster.moveSpeed = monsterSpeed;
monsterNodes.push(monster);
game.addChild(monster);
}
}
}
}
// Find start position
var charCol = 1,
charRow = 1;
for (var y = 0; y < MAZE_ROWS; y++) {
for (var x = 0; x < MAZE_COLS; x++) {
if (maze[y][x] === 2) {
charCol = x;
charRow = y;
}
}
}
// Create player character node and add to game
var charNode = LK.getAsset('Player', {
anchorX: 0.5,
anchorY: 0.5,
x: charCol * MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2,
y: charRow * MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2,
width: MAZE_CELL_SIZE * 0.8,
height: MAZE_CELL_SIZE * 0.8
});
game.addChild(charNode);
// Movement state
var moveTargetCol = charCol;
var moveTargetRow = charRow;
var isMoving = false;
var moveSpeed = 20; // px per frame (increased for faster player movement)
// Touch/drag movement: tap or drag to adjacent cell
function getCellFromPos(x, y) {
var col = Math.floor(x / MAZE_CELL_SIZE);
var row = Math.floor(y / MAZE_CELL_SIZE);
return {
col: col,
row: row
};
}
// Simple BFS pathfinding to allow tap-to-move to any reachable cell
function findPath(startCol, startRow, endCol, endRow) {
// Defensive: check bounds
if (endCol < 0 || endCol >= MAZE_COLS || endRow < 0 || endRow >= MAZE_ROWS) return null;
if (maze[endRow][endCol] === 1) return null;
var queue = [];
var visited = [];
for (var y = 0; y < MAZE_ROWS; y++) {
visited[y] = [];
for (var x = 0; x < MAZE_COLS; x++) {
visited[y][x] = false;
}
}
var prev = [];
for (var y = 0; y < MAZE_ROWS; y++) {
prev[y] = [];
for (var x = 0; x < MAZE_COLS; x++) {
prev[y][x] = null;
}
}
queue.push({
col: startCol,
row: startRow
});
visited[startRow][startCol] = true;
var found = false;
while (queue.length > 0) {
var curr = queue.shift();
if (curr.col === endCol && curr.row === endRow) {
found = true;
break;
}
var dirs = [{
dc: 0,
dr: -1
},
// up
{
dc: 0,
dr: 1
},
// down
{
dc: -1,
dr: 0
},
// left
{
dc: 1,
dr: 0
} // right
];
for (var d = 0; d < dirs.length; d++) {
var nc = curr.col + dirs[d].dc;
var nr = curr.row + dirs[d].dr;
if (nc >= 0 && nc < MAZE_COLS && nr >= 0 && nr < MAZE_ROWS && !visited[nr][nc] && maze[nr][nc] !== 1 && maze[nr][nc] !== 4) {
queue.push({
col: nc,
row: nr
});
visited[nr][nc] = true;
prev[nr][nc] = {
col: curr.col,
row: curr.row
};
}
}
}
if (!found) return null;
// Reconstruct path
var path = [];
var currCol = endCol,
currRow = endRow;
while (!(currCol === startCol && currRow === startRow)) {
path.unshift({
col: currCol,
row: currRow
});
var p = prev[currRow][currCol];
currCol = p.col;
currRow = p.row;
}
return path;
}
var pathToFollow = null;
var pathStepIndex = 0;
game.down = function (x, y, obj) {
// Prevent new move if already moving
if (isMoving) return;
// Compute direction vector from player to tap/click
var px = charNode.x;
var py = charNode.y;
var dx = x - px;
var dy = y - py;
var dist = Math.sqrt(dx * dx + dy * dy);
// Defensive: don't move if tap is too close to player
if (dist < 10) return;
// Normalize direction
var dirX = dx / dist;
var dirY = dy / dist;
// Compute the furthest cell in that direction that is not a wall/door
var currCol = charCol;
var currRow = charRow;
var lastFreeCol = charCol;
var lastFreeRow = charRow;
var maxSteps = 50; // Prevent infinite loop
for (var step = 0; step < maxSteps; step++) {
// Move a small step in direction
var nextX = px + dirX * (step + 1) * MAZE_CELL_SIZE * 0.5;
var nextY = py + dirY * (step + 1) * MAZE_CELL_SIZE * 0.5;
var cell = getCellFromPos(nextX, nextY);
// Defensive: check bounds
if (cell.col < 0 || cell.col >= MAZE_COLS || cell.row < 0 || cell.row >= MAZE_ROWS) break;
// Stop at wall or door
if (maze[cell.row][cell.col] === 1 || maze[cell.row][cell.col] === 4) break;
lastFreeCol = cell.col;
lastFreeRow = cell.row;
}
// Don't move if already at that cell
if (lastFreeCol === charCol && lastFreeRow === charRow) return;
moveTargetCol = lastFreeCol;
moveTargetRow = lastFreeRow;
isMoving = true;
pathToFollow = null;
pathStepIndex = 0;
};
game.move = function (x, y, obj) {
// Only allow drag-move if not already moving
if (!isMoving) {
game.down(x, y, obj);
}
};
game.up = function (x, y, obj) {
// No-op
};
game.update = function () {
// Track lastCol and lastRow for player
if (typeof game.lastCol === "undefined") game.lastCol = charCol;
if (typeof game.lastRow === "undefined") game.lastRow = charRow;
// Move character toward target cell
var targetX = moveTargetCol * MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2;
var targetY = moveTargetRow * MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2;
var dx = targetX - charNode.x;
var dy = targetY - charNode.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (isMoving && dist > 2) {
var step = Math.min(playerSpeed, dist);
charNode.x += dx / dist * step;
charNode.y += dy / dist * step;
} else if (isMoving) {
// Snap to cell
charNode.x = targetX;
charNode.y = targetY;
// Update lastCol/lastRow before changing charCol/charRow
game.lastCol = charCol;
game.lastRow = charRow;
charCol = moveTargetCol;
charRow = moveTargetRow;
// If following a path, advance to next step
if (typeof pathToFollow !== "undefined" && pathToFollow && pathStepIndex < pathToFollow.length - 1) {
pathStepIndex++;
moveTargetCol = pathToFollow[pathStepIndex].col;
moveTargetRow = pathToFollow[pathStepIndex].row;
// Remain isMoving = true
} else {
isMoving = false;
pathToFollow = null;
pathStepIndex = 0;
// Check for goal using cell coordinates and last cell coordinates
var goalCell = false;
for (var y = 0; y < MAZE_ROWS; y++) {
for (var x = 0; x < MAZE_COLS; x++) {
if (maze[y][x] === 3) {
// Only trigger if player just entered the goal cell this frame
var wasOutside = game.lastCol !== x || game.lastRow !== y;
var isInside = charCol === x && charRow === y;
if (wasOutside && isInside) {
goalCell = true;
break;
}
}
}
if (goalCell) break;
}
if (goalCell) {
LK.effects.flashScreen(0x00ff00, 800);
LK.setTimeout(function () {
startNextLevel();
}, 1000);
return;
}
}
}
// Update all monsters
for (var i = 0; i < monsterNodes.length; i++) {
if (monsterNodes[i] && typeof monsterNodes[i].update === "function") {
monsterNodes[i].update();
}
}
};
// --- LIVES SYSTEM ---
// Number of lives per full game (reset only on full restart)
var MAX_LIVES = 3;
var lives = MAX_LIVES;
// Reset lives to 3 on full game restart
if (typeof LK !== "undefined" && typeof LK.on === "function") {
LK.on("gameStart", function () {
lives = MAX_LIVES;
if (typeof updateLivesDisplay === "function") updateLivesDisplay();
});
}
// GUI: Show lives as hearts at top right
var heartNodes = [];
var livesTxt = null;
function updateLivesDisplay() {
// Remove old hearts if any
for (var i = 0; i < heartNodes.length; i++) {
if (heartNodes[i] && typeof heartNodes[i].destroy === "function") {
heartNodes[i].destroy();
}
}
heartNodes.length = 0;
// Draw hearts for each life
var heartSize = 90;
var margin = 18;
var startX = GAME_WIDTH - 40 - (MAX_LIVES - 1) * (heartSize + margin);
var y = MAZE_CELL_SIZE + 10;
for (var i = 0; i < MAX_LIVES; i++) {
var heartAsset = LK.getAsset('Exiy', {
anchorX: 0.5,
anchorY: 0,
width: heartSize,
height: heartSize,
x: startX + i * (heartSize + margin),
y: y
});
if (i >= lives) {
heartAsset.alpha = 0.25; // faded for lost life
} else {
heartAsset.alpha = 1;
}
heartNodes.push(heartAsset);
game.addChild(heartAsset);
}
// Optionally, keep the text for accessibility
if (!livesTxt) {
livesTxt = new Text2("", {
size: 60,
fill: 0xFFD700
});
livesTxt.anchor.set(1, 0); // top right
livesTxt.x = GAME_WIDTH - 40;
livesTxt.y = y + heartSize + 2;
game.addChild(livesTxt);
}
livesTxt.setText("x" + lives);
}
// Infinite levels: generate a new random maze and monsters each time
function startNextLevel() {
// Do not restore lives at start of each level; lives only reset on full game restart
updateLivesDisplay();
// Remove all wall nodes and goal node from previous level
for (var i = 0; i < wallNodes.length; i++) {
if (wallNodes[i] && typeof wallNodes[i].destroy === "function") {
wallNodes[i].destroy();
}
}
wallNodes.length = 0;
// Remove all monsters
for (var i = 0; i < monsterNodes.length; i++) {
if (monsterNodes[i] && typeof monsterNodes[i].destroy === "function") {
monsterNodes[i].destroy();
}
}
monsterNodes.length = 0;
// Set default player and monster speed for this level
playerSpeed = 20; // Player speed: 20 px/frame
monsterSpeed = 1.2; // Monster speed: 1.2 px/frame
// Generate a random maze
// 0 = empty, 1 = wall, 2 = start, 3 = goal, 4 = door (closed)
// Always outer walls
for (var y = 0; y < MAZE_ROWS; y++) {
maze[y] = [];
for (var x = 0; x < MAZE_COLS; x++) {
if (y === 0 || y === MAZE_ROWS - 1 || x === 0 || x === MAZE_COLS - 1) {
maze[y][x] = 1;
} else {
// Randomly place walls, doors, or empty
var r = Math.random();
if (r < 0.13) {
maze[y][x] = 1; // wall
} else if (r < 0.16) {
maze[y][x] = 4; // door
} else {
maze[y][x] = 0; // empty
}
}
}
}
// Pick random start and goal positions (not on wall/door)
function randomEmptyCell() {
var tries = 0;
while (tries < 1000) {
var rx = 1 + Math.floor(Math.random() * (MAZE_COLS - 2));
var ry = 1 + Math.floor(Math.random() * (MAZE_ROWS - 2));
if (maze[ry][rx] === 0) return {
col: rx,
row: ry
};
tries++;
}
// fallback
return {
col: 1,
row: 1
};
}
var start = randomEmptyCell();
maze[start.row][start.col] = 2;
var goal = randomEmptyCell();
while (goal.col === start.col && goal.row === start.row) {
goal = randomEmptyCell();
}
maze[goal.row][goal.col] = 3;
// Redraw maze walls, doors, and goal
for (var y = 0; y < MAZE_ROWS; y++) {
for (var x = 0; x < MAZE_COLS; x++) {
if (maze[y][x] === 1) {
var wall = LK.getAsset('base', {
anchorX: 0.5,
anchorY: 0.5,
x: x * MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2,
y: y * MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2,
width: MAZE_CELL_SIZE,
height: MAZE_CELL_SIZE
});
wallNodes.push(wall);
game.addChild(wall);
}
if (maze[y][x] === 4) {
var door = LK.getAsset('base', {
anchorX: 0.5,
anchorY: 0.5,
x: x * MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2,
y: y * MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2,
width: MAZE_CELL_SIZE,
height: MAZE_CELL_SIZE,
tint: 0xFFD700
});
door._mazeCol = x;
door._mazeRow = y;
door.down = function (lx, ly, obj) {
var col = this._mazeCol;
var row = this._mazeRow;
if (maze[row][col] === 4) {
maze[row][col] = 0;
this.destroy();
}
};
wallNodes.push(door);
game.addChild(door);
}
if (maze[y][x] === 3) {
var goalNode = LK.getAsset('skeleton', {
anchorX: 0.5,
anchorY: 0.5,
x: x * MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2,
y: y * MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2,
width: MAZE_CELL_SIZE,
height: MAZE_CELL_SIZE
});
game.addChild(goalNode);
}
}
}
// Place new monsters for this level
// Place 3-5 monsters at random empty cells (not start/goal)
var monsterTypes = ['skeleton', 'goldenSkeleton', 'zombie', 'zombieSkeleton', 'Ginger'];
var monsterCount = 3 + Math.floor(Math.random() * 3);
var monsterSpots = [];
for (var i = 0; i < monsterCount; i++) {
var spot = randomEmptyCell();
// Don't place on start or goal
while (spot.col === start.col && spot.row === start.row || spot.col === goal.col && spot.row === goal.row || monsterSpots.some(function (ms) {
return ms.col === spot.col && ms.row === spot.row;
})) {
spot = randomEmptyCell();
}
monsterSpots.push(spot);
var mtype = monsterTypes[Math.floor(Math.random() * monsterTypes.length)];
var monster = new Monster();
monster.init(mtype, spot.col, spot.row);
// Assign default speed to this monster
monster.moveSpeed = monsterSpeed;
monsterNodes.push(monster);
game.addChild(monster);
}
// Reset player to new start
charCol = start.col;
charRow = start.row;
charNode.x = charCol * MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2;
charNode.y = charRow * MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2;
moveTargetCol = charCol;
moveTargetRow = charRow;
isMoving = false;
// Show a message for new level start
if (typeof startNextLevel.level === "undefined") startNextLevel.level = 2;else startNextLevel.level++;
var msg = new Text2("Level " + startNextLevel.level + "!", {
size: 100,
fill: 0x00FFAA
});
msg.anchor.set(0.5, 0.5);
msg.x = GAME_WIDTH / 2;
msg.y = GAME_HEIGHT / 2 + 200;
game.addChild(msg);
LK.setTimeout(function () {
msg.destroy();
}, 1500);
// Play Bonelab theme music and repeat it every level
LK.playMusic('bonelab_theme', {
loop: true
});
}
Fullscreen modern App Store landscape banner, 16:9, high definition, for a game titled "Monster Mash: Undead Defense" and with the description "Defend your base from zombies, skeletons, and their golden counterparts by tapping or dragging to attack. Survive waves and score points!". No text on banner!
Zombie with long red eyes. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
A zombie with long eyes but everything is golden. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
A golden terrifying skeleton. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
A zombie fixed with a skeleton. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
Ghost. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
Heart. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
Monster with big hands and ginger hair. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat