/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ // BonusObject class: animates through 5 assets and moves around, gives 1000-2000 points var BonusObject = Container.expand(function () { var self = Container.call(this); self.assetNames = ['bonus1', 'bonus2', 'bonus3', 'bonus4', 'bonus5']; self.assetIndex = 0; self.bonusGfx = self.attachAsset(self.assetNames[0], { anchorX: 0.5, anchorY: 0.5 }); self.radius = self.bonusGfx.width / 2; self.moveDir = { x: 1, y: 1 }; self.speed = 10 + Math.floor(Math.random() * 6); // 10-15 px/tick self.lastX = self.x; self.lastY = self.y; self._animTimer = LK.setInterval(function () { self.assetIndex = (self.assetIndex + 1) % self.assetNames.length; self.removeChildren(); self.bonusGfx = self.attachAsset(self.assetNames[self.assetIndex], { anchorX: 0.5, anchorY: 0.5 }); self.radius = self.bonusGfx.width / 2; }, 250); self.destroy = function () { if (self._animTimer) { LK.clearInterval(self._animTimer); self._animTimer = null; } Container.prototype.destroy.call(self); }; self.update = function () { // Save last position for edge/collision logic self.lastX = self.x; self.lastY = self.y; // Move self.x += self.moveDir.x * self.speed; self.y += self.moveDir.y * self.speed; // Bounce off maze edges (keep inside play area) var minX = MAZE_OFFSET_X + self.radius; var maxX = MAZE_OFFSET_X + MAZE_COLS * CELL_SIZE - self.radius; var minY = MAZE_OFFSET_Y + self.radius; var maxY = MAZE_OFFSET_Y + MAZE_ROWS * CELL_SIZE - self.radius; if (self.x <= minX && self.moveDir.x < 0 || self.x >= maxX && self.moveDir.x > 0) { self.moveDir.x *= -1; self.x = Math.max(minX, Math.min(self.x, maxX)); } if (self.y <= minY && self.moveDir.y < 0 || self.y >= maxY && self.moveDir.y > 0) { self.moveDir.y *= -1; self.y = Math.max(minY, Math.min(self.y, maxY)); } }; return self; }); // Cherry class var Cherry = Container.expand(function () { var self = Container.call(this); self.attachAsset('cherry', { anchorX: 0.5, anchorY: 0.5 }); return self; }); // Ghost class var Ghost = Container.expand(function () { var self = Container.call(this); self.colorId = 'ghost_red'; // default, will be set on init self.frightened = false; self.dir = { x: 0, y: 0 }; self.speed = 7; self.gridX = 0; self.gridY = 0; self.scatterTarget = { x: 0, y: 0 }; // for future AI self.mode = 'chase'; // 'chase', 'scatter', 'frightened' self.frightTimer = 0; self.attach = function (colorId) { self.colorId = colorId; self.removeChildren(); var ghostGfx = self.attachAsset(colorId, { anchorX: 0.5, anchorY: 0.5 }); }; self.setFrightened = function (on) { self.frightened = on; self.removeChildren(); if (on) { self.attachAsset('ghost_fright', { anchorX: 0.5, anchorY: 0.5 }); } else { self.attachAsset(self.colorId, { anchorX: 0.5, anchorY: 0.5 }); } }; self.update = function () { // Save last position for collision checks var prevX = self.x; var prevY = self.y; self.x += self.dir.x * self.speed; self.y += self.dir.y * self.speed; // Wall collision: check if ghost is inside a wall after moving, and correct if so var grid = posToGrid(self.x, self.y); if (isWall(grid.col, grid.row)) { // Undo movement and stop at wall edge self.x = prevX; self.y = prevY; // Snap to center of previous cell to avoid jitter var center = gridToPos(posToGrid(self.x, self.y).col, posToGrid(self.x, self.y).row); self.x = center.x; self.y = center.y; // Optionally, force ghost to pick a new direction next update } }; return self; }); // Pacman class var Pacman = Container.expand(function () { var self = Container.call(this); self.spriteIndex = 0; self.pacmanGfx = self.attachAsset('pacman', { anchorX: 0.5, anchorY: 0.5 }); self.radius = self.pacmanGfx.width / 2; self.dir = { x: 1, y: 0 }; // Start moving right self.nextDir = { x: 1, y: 0 }; self.speed = 8; // pixels per tick self.gridX = 0; self.gridY = 0; self.moving = true; // Animation: swap sprite every 0.5s self._animTimer = LK.setInterval(function () { self.spriteIndex = 1 - self.spriteIndex; self.removeChildren(); if (self.spriteIndex === 0) { self.pacmanGfx = self.attachAsset('pacman', { anchorX: 0.5, anchorY: 0.5 }); } else { self.pacmanGfx = self.attachAsset('pacman2', { anchorX: 0.5, anchorY: 0.5 }); } self.radius = self.pacmanGfx.width / 2; }, 500); self.destroy = function () { // Clean up animation timer if (self._animTimer) { LK.clearInterval(self._animTimer); self._animTimer = null; } Container.prototype.destroy.call(self); }; self.update = function () { if (!self.moving) return; // Move Pacman self.x += self.dir.x * self.speed; self.y += self.dir.y * self.speed; // Flip Pacman when moving left, reset otherwise if (self.dir.x < 0) { self.pacmanGfx.scaleX = -1; } else { self.pacmanGfx.scaleX = 1; } }; return self; }); // Pellet class var Pellet = Container.expand(function () { var self = Container.call(this); self.isPower = false; self.attach = function (isPower) { self.isPower = isPower; self.removeChildren(); if (isPower) { self.attachAsset('powerpellet', { anchorX: 0.5, anchorY: 0.5 }); } else { self.attachAsset('pellet', { anchorX: 0.5, anchorY: 0.5 }); } }; return self; }); // PizzaDeliveryGhost class: stationary, never despawns, asks for tip in pellets 2000-3000 var PizzaDeliveryGhost = Container.expand(function () { var self = Container.call(this); // Use ghost_orange asset for pizza ghost (could be changed to a pizza asset if available) self.ghostGfx = self.attachAsset('ghost_orange', { anchorX: 0.5, anchorY: 0.5 }); self.radius = self.ghostGfx.width / 2; self.lastX = self.x; self.lastY = self.y; self.tipAsked = false; self.tipAmount = 2000 + Math.floor(Math.random() * 1001); // 2000-3000 // Text for tip request self.tipText = new Text2('Tip: ' + self.tipAmount + ' pellets?', { size: 60, fill: "#fff" }); self.tipText.anchor.set(0.5, 1.2); self.tipText.visible = false; self.addChild(self.tipText); self.update = function () { // Save last position for edge/collision logic self.lastX = self.x; self.lastY = self.y; // No movement! Stationary pizza ghost // Show tip text if Pacman is close and tip not yet asked if (!self.tipAsked && pacman) { var dx = pacman.x - self.x; var dy = pacman.y - self.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < 120) { self.tipText.visible = true; self.tipAsked = true; } } // Hide tip text if Pacman moves away if (self.tipAsked && pacman) { var dx = pacman.x - self.x; var dy = pacman.y - self.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist > 180) { self.tipText.visible = false; } } }; self.destroy = function () { Container.prototype.destroy.call(self); }; return self; }); // Wall class var Wall = Container.expand(function () { var self = Container.call(this); self.attachAsset('wall', { anchorX: 0.5, anchorY: 0.5 }); return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x000000 }); /**** * Game Code ****/ // Frightened ghost (white) // Ghosts (four colors) // Wall (blue box) // Power Pellet (bigger blue dot) // Pellet (small white dot) // Pacman (yellow circle) // --- Maze Layout --- // 0: empty, 1: wall, 2: pellet, 3: power pellet // 19x23 grid (classic Pacman aspect, fits 2048x2732 well) var MAZE_ROWS = 23; var MAZE_COLS = 19; var CELL_SIZE = 112; // 19*112=2128, 23*112=2576, fits in 2048x2732 with margin var MAZE_OFFSET_X = (2048 - MAZE_COLS * CELL_SIZE) / 2; var MAZE_OFFSET_Y = (2732 - MAZE_ROWS * CELL_SIZE) / 2 + 40; // --- Multiple Mazes for More Content --- var MAZES = []; // Maze 1 (original) MAZES[0] = [ // Open up top row for pellets: add pellets (2) and open up more wall spaces for access [1, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 1], [1, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 1], [1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 1], [1, 3, 1, 2, 1, 2, 2, 2, 2, 1, 2, 2, 2, 1, 2, 1, 3, 1, 1], [1, 2, 1, 2, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 2, 1, 2, 2, 1], [1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1], [1, 2, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 2, 1], [0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0], [1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1], // Open up middle (row 9, col 0, 9,10,11, 18) [0, 2, 2, 2, 2, 2, 2, 1, 0, 0, 0, 1, 2, 2, 2, 2, 2, 2, 0], [0, 2, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 2, 0], // Open up center of row 11 (row 11, col 9) [1, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 1], [1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1], [1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1], [1, 2, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 2, 1], // Open up middle (row 15, col 0, 9, 18) [0, 3, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 3, 0], // Open up bottom center (row 16, col 0, 9, 18) [0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0], [0, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 0], [0, 2, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 2, 0], [0, 2, 1, 2, 1, 2, 2, 2, 2, 1, 2, 2, 2, 1, 2, 1, 2, 2, 0], // Open up (column 10, row 17) [1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 0, 1, 2, 2, 2, 2, 2, 2, 1], // Open up bottom center (row 21, col 9) [1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1], // Open up bottom row (row 22, col 9) [1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1]]; // Remove wall below ghost spawn (row 12, col 9) to allow escape MAZES[0][12][9] = 0; // Widen the gap below ghost spawn (row 13, cols 8, 9, 10) MAZES[0][13][8] = 0; MAZES[0][13][9] = 0; MAZES[0][13][10] = 0; // Maze 2 (significantly different, spiral and open center, more power pellets, fewer walls) MAZES[1] = [[1, 3, 2, 1, 2, 1, 2, 1, 2, 0, 2, 1, 2, 1, 2, 1, 2, 3, 1], [1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1], [1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1], [1, 2, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 2, 1], [1, 2, 1, 3, 2, 2, 2, 1, 2, 1, 2, 1, 2, 2, 2, 3, 1, 2, 1], [1, 2, 1, 2, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 2, 1, 2, 1], [1, 2, 1, 2, 1, 2, 2, 2, 2, 1, 2, 2, 2, 2, 1, 2, 1, 2, 1], [1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1], [1, 2, 2, 2, 1, 2, 1, 2, 2, 0, 2, 2, 1, 2, 1, 2, 2, 2, 1], [1, 1, 1, 2, 1, 2, 1, 2, 1, 1, 1, 2, 1, 2, 1, 2, 1, 1, 1], [0, 2, 2, 2, 1, 2, 2, 2, 2, 0, 2, 2, 2, 2, 1, 2, 2, 2, 0], [0, 1, 1, 2, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 2, 1, 1, 0], [0, 2, 2, 2, 2, 2, 2, 1, 2, 0, 2, 1, 2, 2, 2, 2, 2, 2, 0], [1, 1, 1, 2, 1, 1, 2, 1, 1, 1, 1, 1, 2, 1, 1, 2, 1, 1, 1], [1, 2, 2, 2, 1, 2, 2, 2, 2, 1, 2, 2, 2, 2, 1, 2, 2, 2, 1], [1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1], [1, 2, 1, 2, 1, 2, 2, 2, 2, 1, 2, 2, 2, 2, 1, 2, 1, 2, 1], [1, 2, 1, 3, 2, 2, 2, 1, 2, 1, 2, 1, 2, 2, 2, 3, 1, 2, 1], [1, 2, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 2, 1], [1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1], [1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1], [1, 3, 2, 1, 2, 1, 2, 1, 2, 0, 2, 1, 2, 1, 2, 1, 2, 3, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1]]; // Remove wall below ghost spawn (row 12, col 9) to allow escape MAZES[1][12][9] = 0; // Widen the gap below ghost spawn (row 13, cols 8, 9, 10) MAZES[1][13][8] = 0; MAZES[1][13][9] = 0; MAZES[1][13][10] = 0; // Maze 3 (new, more open, with corridors and more power pellets) MAZES[2] = [[1, 3, 2, 2, 2, 1, 1, 1, 2, 0, 2, 1, 1, 1, 2, 2, 2, 3, 1], [1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1], [1, 2, 2, 2, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 2, 2, 2, 1], [1, 2, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 2, 1], [1, 2, 1, 3, 2, 2, 2, 1, 2, 1, 2, 1, 2, 2, 2, 3, 1, 2, 1], [1, 2, 1, 2, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 2, 1, 2, 1], [1, 2, 1, 2, 1, 2, 2, 2, 2, 1, 2, 2, 2, 2, 1, 2, 1, 2, 1], [1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1], [1, 2, 2, 2, 1, 2, 1, 2, 2, 0, 2, 2, 1, 2, 1, 2, 2, 2, 1], [1, 1, 1, 2, 1, 2, 1, 2, 1, 1, 1, 2, 1, 2, 1, 2, 1, 1, 1], [0, 2, 2, 2, 1, 2, 2, 2, 2, 0, 2, 2, 2, 2, 1, 2, 2, 2, 0], [0, 1, 1, 2, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 2, 1, 1, 0], [0, 2, 2, 2, 2, 2, 2, 1, 2, 0, 2, 1, 2, 2, 2, 2, 2, 2, 0], [1, 1, 1, 2, 1, 1, 2, 1, 1, 1, 1, 1, 2, 1, 1, 2, 1, 1, 1], [1, 2, 2, 2, 1, 2, 2, 2, 2, 1, 2, 2, 2, 2, 1, 2, 2, 2, 1], [1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1], [1, 2, 1, 2, 1, 2, 2, 2, 2, 1, 2, 2, 2, 2, 1, 2, 1, 2, 1], [1, 2, 1, 3, 2, 2, 2, 1, 2, 1, 2, 1, 2, 2, 2, 3, 1, 2, 1], [1, 2, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 2, 1], [1, 2, 2, 2, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 2, 2, 2, 1], [1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 1, 1], [1, 3, 2, 2, 2, 1, 1, 1, 2, 0, 2, 1, 1, 1, 2, 2, 2, 3, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1]]; // Remove wall below ghost spawn (row 12, col 9) to allow escape MAZES[2][12][9] = 0; // Widen the gap below ghost spawn (row 13, cols 8, 9, 10) MAZES[2][13][8] = 0; MAZES[2][13][9] = 0; MAZES[2][13][10] = 0; // Maze 4 (unique: zig-zag corridors, more open, more power pellets, new pattern) MAZES[3] = [[1, 3, 2, 2, 2, 1, 2, 2, 2, 0, 2, 2, 2, 1, 2, 2, 2, 3, 1], [1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 1, 1], [1, 2, 2, 2, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 2, 2, 2, 1], [1, 2, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 2, 1], [1, 2, 1, 3, 2, 2, 2, 1, 2, 1, 2, 1, 2, 2, 2, 3, 1, 2, 1], [1, 2, 1, 2, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 2, 1, 2, 1], [1, 2, 1, 2, 1, 2, 2, 2, 2, 1, 2, 2, 2, 2, 1, 2, 1, 2, 1], [1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1], [1, 2, 2, 2, 1, 2, 1, 2, 2, 0, 2, 2, 1, 2, 1, 2, 2, 2, 1], [1, 1, 1, 2, 1, 2, 1, 2, 1, 1, 1, 2, 1, 2, 1, 2, 1, 1, 1], [0, 2, 2, 2, 1, 2, 2, 2, 2, 0, 2, 2, 2, 2, 1, 2, 2, 2, 0], [0, 1, 1, 2, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 2, 1, 1, 0], [0, 2, 2, 2, 2, 2, 2, 1, 2, 0, 2, 1, 2, 2, 2, 2, 2, 2, 0], [1, 1, 1, 2, 1, 1, 2, 1, 1, 1, 1, 1, 2, 1, 1, 2, 1, 1, 1], [1, 2, 2, 2, 1, 2, 2, 2, 2, 1, 2, 2, 2, 2, 1, 2, 2, 2, 1], [1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1], [1, 2, 1, 2, 1, 2, 2, 2, 2, 1, 2, 2, 2, 2, 1, 2, 1, 2, 1], [1, 2, 1, 3, 2, 2, 2, 1, 2, 1, 2, 1, 2, 2, 2, 3, 1, 2, 1], [1, 2, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 2, 1], [1, 2, 2, 2, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 2, 2, 2, 1], [1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 1, 1], [1, 3, 2, 2, 2, 1, 2, 2, 2, 0, 2, 2, 2, 1, 2, 2, 2, 3, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1]]; // Remove wall below ghost spawn (row 12, col 9) to allow escape MAZES[3][12][9] = 0; // Widen the gap below ghost spawn (row 13, cols 8, 9, 10) MAZES[3][13][8] = 0; MAZES[3][13][9] = 0; MAZES[3][13][10] = 0; // Maze 5 (new: diamond pattern, more open, more power pellets, unique layout) MAZES[4] = [[1, 3, 2, 2, 2, 1, 1, 1, 2, 0, 2, 1, 1, 1, 2, 2, 2, 3, 1], [1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 1, 1], [1, 2, 2, 2, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 2, 2, 2, 1], [1, 2, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 2, 1], [1, 2, 1, 3, 2, 2, 2, 1, 2, 1, 2, 1, 2, 2, 2, 3, 1, 2, 1], [1, 2, 1, 2, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 2, 1, 2, 1], [1, 2, 1, 2, 1, 2, 2, 2, 2, 1, 2, 2, 2, 2, 1, 2, 1, 2, 1], [1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1], [1, 2, 2, 2, 1, 2, 1, 2, 2, 0, 2, 2, 1, 2, 1, 2, 2, 2, 1], [1, 1, 1, 2, 1, 2, 1, 2, 1, 1, 1, 2, 1, 2, 1, 2, 1, 1, 1], [0, 2, 2, 2, 1, 2, 2, 2, 2, 0, 2, 2, 2, 2, 1, 2, 2, 2, 0], [0, 1, 1, 2, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 2, 1, 1, 0], [0, 2, 2, 2, 2, 2, 2, 1, 2, 0, 2, 1, 2, 2, 2, 2, 2, 2, 0], [1, 1, 1, 2, 1, 1, 2, 1, 1, 1, 1, 1, 2, 1, 1, 2, 1, 1, 1], [1, 2, 2, 2, 1, 2, 2, 2, 2, 1, 2, 2, 2, 2, 1, 2, 2, 2, 1], [1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1], [1, 2, 1, 2, 1, 2, 2, 2, 2, 1, 2, 2, 2, 2, 1, 2, 1, 2, 1], [1, 2, 1, 3, 2, 2, 2, 1, 2, 1, 2, 1, 2, 2, 2, 3, 1, 2, 1], [1, 2, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 2, 1], [1, 2, 2, 2, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 2, 2, 2, 1], [1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 1, 1], [1, 3, 2, 2, 2, 1, 1, 1, 2, 0, 2, 1, 1, 1, 2, 2, 2, 3, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1]]; // Remove wall below ghost spawn (row 12, col 9) to allow escape MAZES[4][12][9] = 0; // Widen the gap below ghost spawn (row 13, cols 8, 9, 10) MAZES[4][13][8] = 0; MAZES[4][13][9] = 0; MAZES[4][13][10] = 0; // Current maze index var currentMazeIndex = 0; var MAZE = MAZES[currentMazeIndex]; // --- Game State --- var pellets = []; var walls = []; var ghosts = []; var cherries = []; var pacman = null; var score = 0; var pelletsLeft = 0; var frightTicks = 0; var frightActive = false; var frightDuration = 360; // 6 seconds at 60fps var swipeStart = null; var dragNode = null; var lastGameOver = false; // Cherry spawn state var cherryActive = false; var cherryTimer = 0; var cherryObj = null; // --- Score Display --- var scoreTxt = new Text2('0', { size: 120, fill: "#fff" }); scoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(scoreTxt); // --- Bonus Object State --- var bonusObject = null; var bonusObjectActive = false; var bonusObjectPoints = 0; // --- Pizza Delivery Ghost State --- var pizzaDeliveryGhost = null; var pizzaDeliveryGhostActive = false; // --- Maze Selection UI --- var mazeSelectBtn = new Container(); var mazeBtnGfx = LK.getAsset('wall', { anchorX: 0.5, anchorY: 0.5 }); mazeBtnGfx.width = 90; mazeBtnGfx.height = 90; mazeBtnGfx.tint = 0x00bfff; mazeSelectBtn.addChild(mazeBtnGfx); var mazeBtnTxt = new Text2('≡', { size: 70, fill: "#fff" }); mazeBtnTxt.anchor.set(0.5, 0.5); mazeBtnTxt.x = 0; mazeBtnTxt.y = 0; mazeSelectBtn.addChild(mazeBtnTxt); // Place in top right, with margin, moved down a bit mazeSelectBtn.x = -100; mazeSelectBtn.y = 120; LK.gui.topRight.addChild(mazeSelectBtn); // Maze selection popup var mazePopup = new Container(); mazePopup.visible = false; mazePopup.zIndex = 1000; // ensure on top var popupBg = LK.getAsset('wall', { anchorX: 0.5, anchorY: 0.5 }); popupBg.width = 900; popupBg.height = 1050; popupBg.tint = 0x222244; mazePopup.addChild(popupBg); // Add "Choose a maze" text at the top of the popup var chooseMazeTxt = new Text2('Choose a maze', { size: 70, fill: "#fff" }); chooseMazeTxt.anchor.set(0.5, 0.5); chooseMazeTxt.x = 0; chooseMazeTxt.y = -400; mazePopup.addChild(chooseMazeTxt); var maze1Btn = new Container(); var maze1Gfx = LK.getAsset('wall', { anchorX: 0.5, anchorY: 0.5 }); maze1Gfx.width = 220; maze1Gfx.height = 100; maze1Gfx.tint = 0x00bfff; maze1Btn.addChild(maze1Gfx); var maze1Txt = new Text2('Maze 1', { size: 60, fill: "#fff" }); maze1Txt.anchor.set(0.5, 0.5); maze1Btn.addChild(maze1Txt); maze1Btn.x = 0; maze1Btn.y = -180; mazePopup.addChild(maze1Btn); var maze2Btn = new Container(); var maze2Gfx = LK.getAsset('wall', { anchorX: 0.5, anchorY: 0.5 }); maze2Gfx.width = 220; maze2Gfx.height = 100; maze2Gfx.tint = 0x00bfff; maze2Btn.addChild(maze2Gfx); var maze2Txt = new Text2('Maze 2', { size: 60, fill: "#fff" }); maze2Txt.anchor.set(0.5, 0.5); maze2Btn.addChild(maze2Txt); maze2Btn.x = 0; maze2Btn.y = -60; mazePopup.addChild(maze2Btn); // Maze 3 button var maze3Btn = new Container(); var maze3Gfx = LK.getAsset('wall', { anchorX: 0.5, anchorY: 0.5 }); maze3Gfx.width = 220; maze3Gfx.height = 100; maze3Gfx.tint = 0x00bfff; maze3Btn.addChild(maze3Gfx); var maze3Txt = new Text2('Maze 3', { size: 60, fill: "#fff" }); maze3Txt.anchor.set(0.5, 0.5); maze3Btn.addChild(maze3Txt); maze3Btn.x = 0; maze3Btn.y = 60; mazePopup.addChild(maze3Btn); // Maze 4 button var maze4Btn = new Container(); var maze4Gfx = LK.getAsset('wall', { anchorX: 0.5, anchorY: 0.5 }); maze4Gfx.width = 220; maze4Gfx.height = 100; maze4Gfx.tint = 0x00bfff; maze4Btn.addChild(maze4Gfx); var maze4Txt = new Text2('Maze 4', { size: 60, fill: "#fff" }); maze4Txt.anchor.set(0.5, 0.5); maze4Btn.addChild(maze4Txt); maze4Btn.x = 0; maze4Btn.y = 180; mazePopup.addChild(maze4Btn); // Maze 5 button var maze5Btn = new Container(); var maze5Gfx = LK.getAsset('wall', { anchorX: 0.5, anchorY: 0.5 }); maze5Gfx.width = 220; maze5Gfx.height = 100; maze5Gfx.tint = 0x00bfff; maze5Btn.addChild(maze5Gfx); var maze5Txt = new Text2('Maze 5', { size: 60, fill: "#fff" }); maze5Txt.anchor.set(0.5, 0.5); maze5Btn.addChild(maze5Txt); maze5Btn.x = 0; maze5Btn.y = 300; mazePopup.addChild(maze5Btn); // Center popup in GUI mazePopup.x = 0; mazePopup.y = 0; LK.gui.center.addChild(mazePopup); // Button event handlers mazeSelectBtn.down = function (x, y, obj) { mazePopup.visible = true; }; maze1Btn.down = function (x, y, obj) { if (currentMazeIndex !== 0) { currentMazeIndex = 0; MAZE = MAZES[currentMazeIndex]; startGame(); } mazePopup.visible = false; }; maze2Btn.down = function (x, y, obj) { if (currentMazeIndex !== 1) { currentMazeIndex = 1; MAZE = MAZES[currentMazeIndex]; startGame(); } mazePopup.visible = false; }; // Maze 3 button event handler maze3Btn.down = function (x, y, obj) { if (currentMazeIndex !== 2) { currentMazeIndex = 2; MAZE = MAZES[currentMazeIndex]; startGame(); } mazePopup.visible = false; }; // Maze 4 button event handler maze4Btn.down = function (x, y, obj) { if (currentMazeIndex !== 3) { currentMazeIndex = 3; MAZE = MAZES[currentMazeIndex]; startGame(); } mazePopup.visible = false; }; // Maze 5 button event handler maze5Btn.down = function (x, y, obj) { if (currentMazeIndex !== 4) { currentMazeIndex = 4; MAZE = MAZES[currentMazeIndex]; startGame(); } mazePopup.visible = false; }; // Hide popup if user taps outside popup area mazePopup.down = function (x, y, obj) { // Only close if not on a button var localX = x - mazePopup.x; var localY = y - mazePopup.y; // If not inside any button, close var inBtn = function inBtn(btn) { return localX > btn.x - 110 && localX < btn.x + 110 && localY > btn.y - 50 && localY < btn.y + 50; }; if (!(inBtn(maze1Btn) || inBtn(maze2Btn) || inBtn(maze3Btn) || inBtn(maze4Btn) || inBtn(maze5Btn))) { mazePopup.visible = false; } }; // --- Maze Build --- function buildMaze() { // Clear previous for (var i = 0; i < pellets.length; ++i) pellets[i].destroy(); for (var i = 0; i < walls.length; ++i) walls[i].destroy(); for (var i = 0; i < cherries.length; ++i) cherries[i].destroy(); pellets = []; walls = []; cherries = []; cherryActive = false; cherryTimer = 0; cherryObj = null; pelletsLeft = 0; for (var row = 0; row < MAZE_ROWS; ++row) { // Defensive: skip if MAZE or row is not defined if (!MAZE || !MAZE[row]) continue; for (var col = 0; col < MAZE_COLS; ++col) { // Defensive: skip if column is not defined if (typeof MAZE[row][col] === "undefined") continue; var cell = MAZE[row][col]; var x = MAZE_OFFSET_X + col * CELL_SIZE + CELL_SIZE / 2; var y = MAZE_OFFSET_Y + row * CELL_SIZE + CELL_SIZE / 2; if (cell === 1) { var wall = new Wall(); wall.x = x; wall.y = y; game.addChild(wall); walls.push(wall); } else if (cell === 2 || cell === 3) { // Prevent pellets from spawning at ghost spawn locations (center of maze) // Ghosts spawn at (row 11, col 9), (row 11, col 8), (row 11, col 10), (row 12, col 9) var isGhostSpawn = row === 11 && (col === 8 || col === 9 || col === 10) || row === 12 && col === 9; if (!isGhostSpawn) { var pellet = new Pellet(); pellet.x = x; pellet.y = y; pellet.attach(cell === 3); game.addChild(pellet); pellets.push(pellet); pelletsLeft++; } } } } } // --- Pacman Init --- function spawnPacman() { if (pacman) pacman.destroy(); pacman = new Pacman(); // Start position: center of maze, row 15, col 9 for maze 1 // For maze 2, pick a valid open cell (not a wall) if (currentMazeIndex === 1 || currentMazeIndex === 2) { // Find a non-wall cell near the center, prefer row 15, col 9, else search outward var found = false; var tryOrder = [{ col: 9, row: 15 }, { col: 9, row: 14 }, { col: 9, row: 16 }, { col: 8, row: 15 }, { col: 10, row: 15 }, { col: 8, row: 14 }, { col: 10, row: 14 }, { col: 8, row: 16 }, { col: 10, row: 16 }]; for (var i = 0; i < tryOrder.length; ++i) { var c = tryOrder[i].col, r = tryOrder[i].row; if (MAZE[r] && typeof MAZE[r][c] !== "undefined" && MAZE[r][c] !== 1) { pacman.gridX = c; pacman.gridY = r; found = true; break; } } if (!found) { // fallback: scan for first open cell for (var r = 0; r < MAZE_ROWS; ++r) { for (var c = 0; c < MAZE_COLS; ++c) { if (MAZE[r] && typeof MAZE[r][c] !== "undefined" && MAZE[r][c] !== 1) { pacman.gridX = c; pacman.gridY = r; found = true; break; } } if (found) break; } } } else if (currentMazeIndex === 3 || currentMazeIndex === 4) { // For maze 4 and 5, pick a valid open cell (not a wall), like maze 1/2 var found = false; var tryOrder = [{ col: 9, row: 15 }, { col: 9, row: 14 }, { col: 9, row: 16 }, { col: 8, row: 15 }, { col: 10, row: 15 }, { col: 8, row: 14 }, { col: 10, row: 14 }, { col: 8, row: 16 }, { col: 10, row: 16 }]; for (var i = 0; i < tryOrder.length; ++i) { var c = tryOrder[i].col, r = tryOrder[i].row; if (MAZE[r] && typeof MAZE[r][c] !== "undefined" && MAZE[r][c] !== 1) { pacman.gridX = c; pacman.gridY = r; found = true; break; } } if (!found) { // fallback: scan for first open cell for (var r = 0; r < MAZE_ROWS; ++r) { for (var c = 0; c < MAZE_COLS; ++c) { if (MAZE[r] && typeof MAZE[r][c] !== "undefined" && MAZE[r][c] !== 1) { pacman.gridX = c; pacman.gridY = r; found = true; break; } } if (found) break; } } } else { pacman.gridX = 9; pacman.gridY = 15; } pacman.x = MAZE_OFFSET_X + pacman.gridX * CELL_SIZE + CELL_SIZE / 2; pacman.y = MAZE_OFFSET_Y + pacman.gridY * CELL_SIZE + CELL_SIZE / 2; pacman.dir = { x: 1, y: 0 }; pacman.nextDir = { x: 1, y: 0 }; pacman.moving = true; game.addChild(pacman); } // --- Ghosts Init --- function spawnGhosts() { // Remove old ghosts for (var i = 0; i < ghosts.length; ++i) ghosts[i].destroy(); ghosts = []; // Four ghosts, different colors, start in center var ghostColors = ['ghost_red', 'ghost_pink', 'ghost_blue', 'ghost_orange']; var ghostStarts = [{ col: 9, row: 11 }, { col: 8, row: 11 }, { col: 10, row: 11 }, { col: 9, row: 12 }]; // For maze 5 (index 4), ensure ghosts spawn in open cells near center if (currentMazeIndex === 4) { // Try to find open cells near the classic spawn points var preferred = [{ col: 9, row: 11 }, { col: 8, row: 11 }, { col: 10, row: 11 }, { col: 9, row: 12 }, { col: 8, row: 12 }, { col: 10, row: 12 }, { col: 9, row: 10 }, { col: 8, row: 10 }, { col: 10, row: 10 }]; var found = []; for (var i = 0; i < preferred.length && found.length < 4; ++i) { var c = preferred[i].col, r = preferred[i].row; if (MAZE[r] && typeof MAZE[r][c] !== "undefined" && MAZE[r][c] !== 1) { // Only add if not already used var already = false; for (var j = 0; j < found.length; ++j) { if (found[j].col === c && found[j].row === r) { already = true; break; } } if (!already) found.push({ col: c, row: r }); } } // If not enough, fill with any open cell if (found.length < 4) { for (var r = 0; r < MAZE_ROWS && found.length < 4; ++r) { for (var c = 0; c < MAZE_COLS && found.length < 4; ++c) { if (MAZE[r] && typeof MAZE[r][c] !== "undefined" && MAZE[r][c] !== 1) { // Only add if not already used var already = false; for (var j = 0; j < found.length; ++j) { if (found[j].col === c && found[j].row === r) { already = true; break; } } if (!already) found.push({ col: c, row: r }); } } } } // Use found as ghostStarts for (var i = 0; i < 4; ++i) { if (found[i]) ghostStarts[i] = found[i]; } } for (var i = 0; i < 4; ++i) { var ghost = new Ghost(); ghost.attach(ghostColors[i]); ghost.gridX = ghostStarts[i].col; ghost.gridY = ghostStarts[i].row; ghost.x = MAZE_OFFSET_X + ghost.gridX * CELL_SIZE + CELL_SIZE / 2; ghost.y = MAZE_OFFSET_Y + ghost.gridY * CELL_SIZE + CELL_SIZE / 2; // Initial direction: up or left/right if (i === 0) ghost.dir = { x: 0, y: -1 };else if (i === 1) ghost.dir = { x: -1, y: 0 };else if (i === 2) ghost.dir = { x: 1, y: 0 };else ghost.dir = { x: 0, y: 1 }; ghost.frightened = false; ghosts.push(ghost); game.addChild(ghost); } } // --- Utility: Grid <-> Pixel --- function posToGrid(x, y) { var col = Math.floor((x - MAZE_OFFSET_X) / CELL_SIZE); var row = Math.floor((y - MAZE_OFFSET_Y) / CELL_SIZE); return { col: col, row: row }; } function gridToPos(col, row) { return { x: MAZE_OFFSET_X + col * CELL_SIZE + CELL_SIZE / 2, y: MAZE_OFFSET_Y + row * CELL_SIZE + CELL_SIZE / 2 }; } function isWall(col, row) { if (col < 0 || col >= MAZE_COLS || row < 0 || row >= MAZE_ROWS) return true; // Defensive: check MAZE and MAZE[row] exist, and col is defined if (!MAZE || !MAZE[row] || typeof MAZE[row][col] === "undefined") return true; return MAZE[row][col] === 1; } // --- Pacman Movement & Input --- function canMove(col, row, dir) { var nextCol = col + dir.x; var nextRow = row + dir.y; // Prevent Pacman from moving through PizzaDeliveryGhost by treating him as a wall if (pizzaDeliveryGhostActive && pizzaDeliveryGhost) { var pizzaGrid = posToGrid(pizzaDeliveryGhost.x, pizzaDeliveryGhost.y); if (nextCol === pizzaGrid.col && nextRow === pizzaGrid.row) { return false; } } return !isWall(nextCol, nextRow); } function updatePacmanDirection() { var grid = posToGrid(pacman.x, pacman.y); // Snap to center of cell var center = gridToPos(grid.col, grid.row); var dx = Math.abs(pacman.x - center.x); var dy = Math.abs(pacman.y - center.y); if (dx < 4 && dy < 4) { // At center, can change direction if (canMove(grid.col, grid.row, pacman.nextDir)) { pacman.dir = { x: pacman.nextDir.x, y: pacman.nextDir.y }; } // If can't move in current dir, stop if (!canMove(grid.col, grid.row, pacman.dir)) { pacman.moving = false; } else { // Prevent Pacman from being stuck inside PizzaDeliveryGhost's tile if (pizzaDeliveryGhostActive && pizzaDeliveryGhost) { var pizzaGrid = posToGrid(pizzaDeliveryGhost.x, pizzaDeliveryGhost.y); if (grid.col === pizzaGrid.col && grid.row === pizzaGrid.row) { pacman.moving = false; // Optionally, snap Pacman out of the tile (push back) // Try to move Pacman to previous cell if possible var prevCol = grid.col - pacman.dir.x; var prevRow = grid.row - pacman.dir.y; if (!isWall(prevCol, prevRow)) { var prevPos = gridToPos(prevCol, prevRow); pacman.x = prevPos.x; pacman.y = prevPos.y; } } else { pacman.moving = true; } } else { pacman.moving = true; } } // Snap to center pacman.x = center.x; pacman.y = center.y; } } // --- Ghost Movement (Random for MVP, smarter later) --- function getValidGhostDirs(ghost) { var grid = posToGrid(ghost.x, ghost.y); var dirs = [{ x: 1, y: 0 }, { x: -1, y: 0 }, { x: 0, y: 1 }, { x: 0, y: -1 }]; var valid = []; for (var i = 0; i < dirs.length; ++i) { var d = dirs[i]; // Don't reverse direction if (ghost.dir.x === -d.x && ghost.dir.y === -d.y) continue; if (!isWall(grid.col + d.x, grid.row + d.y)) valid.push(d); } return valid; } function updateGhostDirection(ghost) { var grid = posToGrid(ghost.x, ghost.y); var center = gridToPos(grid.col, grid.row); var dx = Math.abs(ghost.x - center.x); var dy = Math.abs(ghost.y - center.y); if (dx < 4 && dy < 4) { // At center, pick new direction var valid = getValidGhostDirs(ghost); if (valid.length === 0) { ghost.dir = { x: -ghost.dir.x, y: -ghost.dir.y }; } else { // MVP: random direction var idx = Math.floor(Math.random() * valid.length); ghost.dir = { x: valid[idx].x, y: valid[idx].y }; } // Snap to center ghost.x = center.x; ghost.y = center.y; } } // --- Pellet Eating --- function checkPelletEat() { for (var i = pellets.length - 1; i >= 0; --i) { var pellet = pellets[i]; var dx = pacman.x - pellet.x; var dy = pacman.y - pellet.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < 48) { // Eat pellet if (pellet.isPower) { frightTicks = frightDuration; frightActive = true; for (var g = 0; g < ghosts.length; ++g) { ghosts[g].setFrightened(true); } } pellet.destroy(); pellets.splice(i, 1); pelletsLeft--; score += pellet.isPower ? 50 : 10; scoreTxt.setText(score); LK.setScore(score); if (pelletsLeft === 0) { // Switch to next maze if available, else loop back to first currentMazeIndex = (currentMazeIndex + 1) % MAZES.length; MAZE = MAZES[currentMazeIndex]; LK.showYouWin(); } } } } // --- Ghost Collisions --- function checkGhostCollisions() { for (var i = 0; i < ghosts.length; ++i) { var ghost = ghosts[i]; var dx = pacman.x - ghost.x; var dy = pacman.y - ghost.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < 60) { if (frightActive && ghost.frightened && ghost.visible !== false) { // Eat ghost: make invisible, respawn after fright ends ghost.visible = false; score += 200; scoreTxt.setText(score); LK.setScore(score); } else if (!frightActive && ghost.visible !== false) { // Pacman dies if (!lastGameOver) { LK.effects.flashScreen(0xff0000, 1000); LK.showGameOver(); lastGameOver = true; } } } } } // --- Frightened Mode Timer --- function updateFrightened() { if (frightActive) { frightTicks--; if (frightTicks <= 0) { frightActive = false; for (var g = 0; g < ghosts.length; ++g) { ghosts[g].setFrightened(false); // If ghost was eaten (invisible), respawn at home if (ghosts[g].visible === false) { // Respawn at original spawn for this ghost var spawnIdx = g; var ghostStarts = [{ col: 9, row: 11 }, { col: 8, row: 11 }, { col: 10, row: 11 }, { col: 9, row: 12 }]; // For maze 5 (index 4), ensure ghosts respawn in open cells near center if (currentMazeIndex === 4) { var preferred = [{ col: 9, row: 11 }, { col: 8, row: 11 }, { col: 10, row: 11 }, { col: 9, row: 12 }, { col: 8, row: 12 }, { col: 10, row: 12 }, { col: 9, row: 10 }, { col: 8, row: 10 }, { col: 10, row: 10 }]; var found = []; for (var i = 0; i < preferred.length && found.length < 4; ++i) { var c = preferred[i].col, r = preferred[i].row; if (MAZE[r] && typeof MAZE[r][c] !== "undefined" && MAZE[r][c] !== 1) { // Only add if not already used var already = false; for (var j = 0; j < found.length; ++j) { if (found[j].col === c && found[j].row === r) { already = true; break; } } if (!already) found.push({ col: c, row: r }); } } // If not enough, fill with any open cell if (found.length < 4) { for (var r = 0; r < MAZE_ROWS && found.length < 4; ++r) { for (var c = 0; c < MAZE_COLS && found.length < 4; ++c) { if (MAZE[r] && typeof MAZE[r][c] !== "undefined" && MAZE[r][c] !== 1) { // Only add if not already used var already = false; for (var j = 0; j < found.length; ++j) { if (found[j].col === c && found[j].row === r) { already = true; break; } } if (!already) found.push({ col: c, row: r }); } } } } if (found[spawnIdx]) ghostStarts[spawnIdx] = found[spawnIdx]; } var spawn = ghostStarts[spawnIdx]; var home = gridToPos(spawn.col, spawn.row); ghosts[g].x = home.x; ghosts[g].y = home.y; ghosts[g].dir = { x: 0, y: -1 }; ghosts[g].visible = true; ghosts[g].gridX = spawn.col; ghosts[g].gridY = spawn.row; } } } } } // --- Input: Swipe to set direction --- function getSwipeDir(start, end) { var dx = end.x - start.x; var dy = end.y - start.y; if (Math.abs(dx) > Math.abs(dy)) { if (dx > 32) return { x: 1, y: 0 }; if (dx < -32) return { x: -1, y: 0 }; } else { if (dy > 32) return { x: 0, y: 1 }; if (dy < -32) return { x: 0, y: -1 }; } return null; } game.down = function (x, y, obj) { swipeStart = { x: x, y: y }; dragNode = pacman; }; game.move = function (x, y, obj) { if (!swipeStart) return; var swipeEnd = { x: x, y: y }; var dir = getSwipeDir(swipeStart, swipeEnd); if (dir) { pacman.nextDir = dir; swipeStart = null; } }; game.up = function (x, y, obj) { swipeStart = null; dragNode = null; }; // --- Game Update Loop --- game.update = function () { if (!pacman) return; lastGameOver = false; updatePacmanDirection(); pacman.update(); for (var i = 0; i < ghosts.length; ++i) { updateGhostDirection(ghosts[i]); ghosts[i].update(); } // --- Cherry spawn logic --- // Spawn cherry after 1/3 pellets are eaten, if not already spawned if (!cherryActive && pelletsLeft > 0 && pelletsLeft <= Math.floor(pelletsLeft + cherries.length + pellets.length) * 2 / 3) { // Find a random empty cell (not wall, not pellet, not ghost spawn) var openCells = []; for (var row = 0; row < MAZE_ROWS; ++row) { for (var col = 0; col < MAZE_COLS; ++col) { if (MAZE[row] && typeof MAZE[row][col] !== "undefined" && MAZE[row][col] !== 1) { // Not wall var isGhostSpawn = row === 11 && (col === 8 || col === 9 || col === 10) || row === 12 && col === 9; if (!isGhostSpawn) { // Not already a pellet var pelletHere = false; for (var p = 0; p < pellets.length; ++p) { var grid = posToGrid(pellets[p].x, pellets[p].y); if (grid.col === col && grid.row === row) { pelletHere = true; break; } } // Not already a cherry var cherryHere = false; for (var c = 0; c < cherries.length; ++c) { var grid = posToGrid(cherries[c].x, cherries[c].y); if (grid.col === col && grid.row === row) { cherryHere = true; break; } } if (!pelletHere && !cherryHere) { openCells.push({ col: col, row: row }); } } } } } if (openCells.length > 0) { var idx = Math.floor(Math.random() * openCells.length); var pos = gridToPos(openCells[idx].col, openCells[idx].row); cherryObj = new Cherry(); cherryObj.x = pos.x; cherryObj.y = pos.y; game.addChild(cherryObj); cherries.push(cherryObj); cherryActive = true; cherryTimer = 600; // 10 seconds at 60fps } } // Cherry timer and removal if (cherryActive && cherryObj) { cherryTimer--; if (cherryTimer <= 0) { cherryObj.destroy(); cherries.splice(cherries.indexOf(cherryObj), 1); cherryObj = null; cherryActive = false; } } checkPelletEat(); // --- Cherry eat logic --- if (cherryActive && cherryObj) { var dx = pacman.x - cherryObj.x; var dy = pacman.y - cherryObj.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < 60) { // Eat cherry! cherryObj.destroy(); cherries.splice(cherries.indexOf(cherryObj), 1); cherryObj = null; cherryActive = false; score += 200; scoreTxt.setText(score); LK.setScore(score); } } // --- BonusObject spawn and update --- // Only spawn once per game session if (!bonusObjectActive) { // Spawn in a random open cell var openCells = []; for (var row = 0; row < MAZE_ROWS; ++row) { for (var col = 0; col < MAZE_COLS; ++col) { if (MAZE[row] && typeof MAZE[row][col] !== "undefined" && MAZE[row][col] !== 1) { // Not wall var isGhostSpawn = row === 11 && (col === 8 || col === 9 || col === 10) || row === 12 && col === 9; if (!isGhostSpawn) { openCells.push({ col: col, row: row }); } } } } if (openCells.length > 0) { var idx = Math.floor(Math.random() * openCells.length); var pos = gridToPos(openCells[idx].col, openCells[idx].row); bonusObject = new BonusObject(); bonusObject.x = pos.x; bonusObject.y = pos.y; // Randomize initial direction bonusObject.moveDir = { x: Math.random() < 0.5 ? 1 : -1, y: Math.random() < 0.5 ? 1 : -1 }; game.addChild(bonusObject); bonusObjectActive = true; bonusObjectPoints = 1000 + Math.floor(Math.random() * 1001); // 1000-2000 } } // Update BonusObject movement if (bonusObjectActive && bonusObject) { bonusObject.update(); // Check collision with Pacman var dx = pacman.x - bonusObject.x; var dy = pacman.y - bonusObject.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < 60) { // Pacman eats bonus object! if (bonusObject) { bonusObject.destroy(); bonusObject = null; bonusObjectActive = false; score += bonusObjectPoints; scoreTxt.setText(score); LK.setScore(score); } } } // --- PizzaDeliveryGhost update and tip logic --- if (pizzaDeliveryGhostActive && pizzaDeliveryGhost) { pizzaDeliveryGhost.update(); // If Pacman is close and tip not yet paid, check for payment if (pizzaDeliveryGhost.tipAsked && pizzaDeliveryGhost.tipText.visible && pacman) { var dx = pacman.x - pizzaDeliveryGhost.x; var dy = pacman.y - pizzaDeliveryGhost.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < 60 && score >= pizzaDeliveryGhost.tipAmount) { // Pay tip in pellets (score) score -= pizzaDeliveryGhost.tipAmount; scoreTxt.setText(score); LK.setScore(score); pizzaDeliveryGhost.tipText.setText('Thanks!'); // Hide text after a short time and remove pizzaDeliveryGhost LK.setTimeout(function () { if (pizzaDeliveryGhost && pizzaDeliveryGhost.tipText) { pizzaDeliveryGhost.tipText.visible = false; } if (pizzaDeliveryGhost) { pizzaDeliveryGhost.destroy(); pizzaDeliveryGhost = null; pizzaDeliveryGhostActive = false; } }, 1200); pizzaDeliveryGhost.tipAsked = false; // Only ask once per session } } } checkGhostCollisions(); updateFrightened(); }; // --- Game Start --- function startGame() { score = 0; LK.setScore(0); scoreTxt.setText('0'); // Do not reset currentMazeIndex here, so maze selection works MAZE = MAZES[currentMazeIndex]; buildMaze(); spawnPacman(); spawnGhosts(); // Reset all ghosts to spawn positions and visible, like at the beginning of the game var ghostStarts = [{ col: 9, row: 11 }, { col: 8, row: 11 }, { col: 10, row: 11 }, { col: 9, row: 12 }]; // For maze 5 (index 4), ensure ghosts reset in open cells near center if (currentMazeIndex === 4) { var preferred = [{ col: 9, row: 11 }, { col: 8, row: 11 }, { col: 10, row: 11 }, { col: 9, row: 12 }, { col: 8, row: 12 }, { col: 10, row: 12 }, { col: 9, row: 10 }, { col: 8, row: 10 }, { col: 10, row: 10 }]; var found = []; for (var i = 0; i < preferred.length && found.length < 4; ++i) { var c = preferred[i].col, r = preferred[i].row; if (MAZE[r] && typeof MAZE[r][c] !== "undefined" && MAZE[r][c] !== 1) { // Only add if not already used var already = false; for (var j = 0; j < found.length; ++j) { if (found[j].col === c && found[j].row === r) { already = true; break; } } if (!already) found.push({ col: c, row: r }); } } // If not enough, fill with any open cell if (found.length < 4) { for (var r = 0; r < MAZE_ROWS && found.length < 4; ++r) { for (var c = 0; c < MAZE_COLS && found.length < 4; ++c) { if (MAZE[r] && typeof MAZE[r][c] !== "undefined" && MAZE[r][c] !== 1) { // Only add if not already used var already = false; for (var j = 0; j < found.length; ++j) { if (found[j].col === c && found[j].row === r) { already = true; break; } } if (!already) found.push({ col: c, row: r }); } } } } for (var i = 0; i < 4; ++i) { if (found[i]) ghostStarts[i] = found[i]; } } for (var g = 0; g < ghosts.length; ++g) { var spawn = ghostStarts[g]; var home = gridToPos(spawn.col, spawn.row); ghosts[g].x = home.x; ghosts[g].y = home.y; ghosts[g].dir = { x: 0, y: -1 }; ghosts[g].visible = true; ghosts[g].gridX = spawn.col; ghosts[g].gridY = spawn.row; ghosts[g].setFrightened(false); } frightTicks = 0; frightActive = false; lastGameOver = false; // Reset cherry state for (var i = 0; i < cherries.length; ++i) cherries[i].destroy(); cherries = []; cherryActive = false; cherryTimer = 0; cherryObj = null; } // Reset pizza delivery ghost state if (pizzaDeliveryGhost) pizzaDeliveryGhost.destroy(); pizzaDeliveryGhost = null; pizzaDeliveryGhostActive = false; // Spawn PizzaDeliveryGhost on specific tiles that block required Pacman paths // Define a list of "blocking" tile positions for each maze (row, col) that force Pacman to go around var pizzaBlockTilesByMaze = [ // Maze 1: block a key corridor (e.g. row 7, col 9) [{ row: 7, col: 9 }, { row: 15, col: 9 }], // Maze 2: block a spiral entrance (e.g. row 10, col: 9) [{ row: 10, col: 9 }, { row: 12, col: 9 }], // Maze 3: block a corridor (e.g. row 7, col: 9) [{ row: 7, col: 9 }, { row: 15, col: 9 }], // Maze 4: block a zig-zag (e.g. row 7, col: 9) [{ row: 7, col: 9 }, { row: 15, col: 9 }], // Maze 5: block a diamond center (e.g. row 11, col: 9) [{ row: 11, col: 9 }, { row: 15, col: 9 }]]; var pizzaTiles = pizzaBlockTilesByMaze[currentMazeIndex] || []; // Find the first available blocking tile that is open in the current maze var foundPizzaTile = null; for (var i = 0; i < pizzaTiles.length; ++i) { var t = pizzaTiles[i]; if (MAZE[t.row] && typeof MAZE[t.row][t.col] !== "undefined" && MAZE[t.row][t.col] !== 1 // not a wall ) { foundPizzaTile = t; break; } } if (foundPizzaTile) { var pos = gridToPos(foundPizzaTile.col, foundPizzaTile.row); pizzaDeliveryGhost = new PizzaDeliveryGhost(); pizzaDeliveryGhost.x = pos.x; pizzaDeliveryGhost.y = pos.y; game.addChild(pizzaDeliveryGhost); pizzaDeliveryGhostActive = true; } startGame();
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// BonusObject class: animates through 5 assets and moves around, gives 1000-2000 points
var BonusObject = Container.expand(function () {
var self = Container.call(this);
self.assetNames = ['bonus1', 'bonus2', 'bonus3', 'bonus4', 'bonus5'];
self.assetIndex = 0;
self.bonusGfx = self.attachAsset(self.assetNames[0], {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = self.bonusGfx.width / 2;
self.moveDir = {
x: 1,
y: 1
};
self.speed = 10 + Math.floor(Math.random() * 6); // 10-15 px/tick
self.lastX = self.x;
self.lastY = self.y;
self._animTimer = LK.setInterval(function () {
self.assetIndex = (self.assetIndex + 1) % self.assetNames.length;
self.removeChildren();
self.bonusGfx = self.attachAsset(self.assetNames[self.assetIndex], {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = self.bonusGfx.width / 2;
}, 250);
self.destroy = function () {
if (self._animTimer) {
LK.clearInterval(self._animTimer);
self._animTimer = null;
}
Container.prototype.destroy.call(self);
};
self.update = function () {
// Save last position for edge/collision logic
self.lastX = self.x;
self.lastY = self.y;
// Move
self.x += self.moveDir.x * self.speed;
self.y += self.moveDir.y * self.speed;
// Bounce off maze edges (keep inside play area)
var minX = MAZE_OFFSET_X + self.radius;
var maxX = MAZE_OFFSET_X + MAZE_COLS * CELL_SIZE - self.radius;
var minY = MAZE_OFFSET_Y + self.radius;
var maxY = MAZE_OFFSET_Y + MAZE_ROWS * CELL_SIZE - self.radius;
if (self.x <= minX && self.moveDir.x < 0 || self.x >= maxX && self.moveDir.x > 0) {
self.moveDir.x *= -1;
self.x = Math.max(minX, Math.min(self.x, maxX));
}
if (self.y <= minY && self.moveDir.y < 0 || self.y >= maxY && self.moveDir.y > 0) {
self.moveDir.y *= -1;
self.y = Math.max(minY, Math.min(self.y, maxY));
}
};
return self;
});
// Cherry class
var Cherry = Container.expand(function () {
var self = Container.call(this);
self.attachAsset('cherry', {
anchorX: 0.5,
anchorY: 0.5
});
return self;
});
// Ghost class
var Ghost = Container.expand(function () {
var self = Container.call(this);
self.colorId = 'ghost_red'; // default, will be set on init
self.frightened = false;
self.dir = {
x: 0,
y: 0
};
self.speed = 7;
self.gridX = 0;
self.gridY = 0;
self.scatterTarget = {
x: 0,
y: 0
}; // for future AI
self.mode = 'chase'; // 'chase', 'scatter', 'frightened'
self.frightTimer = 0;
self.attach = function (colorId) {
self.colorId = colorId;
self.removeChildren();
var ghostGfx = self.attachAsset(colorId, {
anchorX: 0.5,
anchorY: 0.5
});
};
self.setFrightened = function (on) {
self.frightened = on;
self.removeChildren();
if (on) {
self.attachAsset('ghost_fright', {
anchorX: 0.5,
anchorY: 0.5
});
} else {
self.attachAsset(self.colorId, {
anchorX: 0.5,
anchorY: 0.5
});
}
};
self.update = function () {
// Save last position for collision checks
var prevX = self.x;
var prevY = self.y;
self.x += self.dir.x * self.speed;
self.y += self.dir.y * self.speed;
// Wall collision: check if ghost is inside a wall after moving, and correct if so
var grid = posToGrid(self.x, self.y);
if (isWall(grid.col, grid.row)) {
// Undo movement and stop at wall edge
self.x = prevX;
self.y = prevY;
// Snap to center of previous cell to avoid jitter
var center = gridToPos(posToGrid(self.x, self.y).col, posToGrid(self.x, self.y).row);
self.x = center.x;
self.y = center.y;
// Optionally, force ghost to pick a new direction next update
}
};
return self;
});
// Pacman class
var Pacman = Container.expand(function () {
var self = Container.call(this);
self.spriteIndex = 0;
self.pacmanGfx = self.attachAsset('pacman', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = self.pacmanGfx.width / 2;
self.dir = {
x: 1,
y: 0
}; // Start moving right
self.nextDir = {
x: 1,
y: 0
};
self.speed = 8; // pixels per tick
self.gridX = 0;
self.gridY = 0;
self.moving = true;
// Animation: swap sprite every 0.5s
self._animTimer = LK.setInterval(function () {
self.spriteIndex = 1 - self.spriteIndex;
self.removeChildren();
if (self.spriteIndex === 0) {
self.pacmanGfx = self.attachAsset('pacman', {
anchorX: 0.5,
anchorY: 0.5
});
} else {
self.pacmanGfx = self.attachAsset('pacman2', {
anchorX: 0.5,
anchorY: 0.5
});
}
self.radius = self.pacmanGfx.width / 2;
}, 500);
self.destroy = function () {
// Clean up animation timer
if (self._animTimer) {
LK.clearInterval(self._animTimer);
self._animTimer = null;
}
Container.prototype.destroy.call(self);
};
self.update = function () {
if (!self.moving) return;
// Move Pacman
self.x += self.dir.x * self.speed;
self.y += self.dir.y * self.speed;
// Flip Pacman when moving left, reset otherwise
if (self.dir.x < 0) {
self.pacmanGfx.scaleX = -1;
} else {
self.pacmanGfx.scaleX = 1;
}
};
return self;
});
// Pellet class
var Pellet = Container.expand(function () {
var self = Container.call(this);
self.isPower = false;
self.attach = function (isPower) {
self.isPower = isPower;
self.removeChildren();
if (isPower) {
self.attachAsset('powerpellet', {
anchorX: 0.5,
anchorY: 0.5
});
} else {
self.attachAsset('pellet', {
anchorX: 0.5,
anchorY: 0.5
});
}
};
return self;
});
// PizzaDeliveryGhost class: stationary, never despawns, asks for tip in pellets 2000-3000
var PizzaDeliveryGhost = Container.expand(function () {
var self = Container.call(this);
// Use ghost_orange asset for pizza ghost (could be changed to a pizza asset if available)
self.ghostGfx = self.attachAsset('ghost_orange', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = self.ghostGfx.width / 2;
self.lastX = self.x;
self.lastY = self.y;
self.tipAsked = false;
self.tipAmount = 2000 + Math.floor(Math.random() * 1001); // 2000-3000
// Text for tip request
self.tipText = new Text2('Tip: ' + self.tipAmount + ' pellets?', {
size: 60,
fill: "#fff"
});
self.tipText.anchor.set(0.5, 1.2);
self.tipText.visible = false;
self.addChild(self.tipText);
self.update = function () {
// Save last position for edge/collision logic
self.lastX = self.x;
self.lastY = self.y;
// No movement! Stationary pizza ghost
// Show tip text if Pacman is close and tip not yet asked
if (!self.tipAsked && pacman) {
var dx = pacman.x - self.x;
var dy = pacman.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 120) {
self.tipText.visible = true;
self.tipAsked = true;
}
}
// Hide tip text if Pacman moves away
if (self.tipAsked && pacman) {
var dx = pacman.x - self.x;
var dy = pacman.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 180) {
self.tipText.visible = false;
}
}
};
self.destroy = function () {
Container.prototype.destroy.call(self);
};
return self;
});
// Wall class
var Wall = Container.expand(function () {
var self = Container.call(this);
self.attachAsset('wall', {
anchorX: 0.5,
anchorY: 0.5
});
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x000000
});
/****
* Game Code
****/
// Frightened ghost (white)
// Ghosts (four colors)
// Wall (blue box)
// Power Pellet (bigger blue dot)
// Pellet (small white dot)
// Pacman (yellow circle)
// --- Maze Layout ---
// 0: empty, 1: wall, 2: pellet, 3: power pellet
// 19x23 grid (classic Pacman aspect, fits 2048x2732 well)
var MAZE_ROWS = 23;
var MAZE_COLS = 19;
var CELL_SIZE = 112; // 19*112=2128, 23*112=2576, fits in 2048x2732 with margin
var MAZE_OFFSET_X = (2048 - MAZE_COLS * CELL_SIZE) / 2;
var MAZE_OFFSET_Y = (2732 - MAZE_ROWS * CELL_SIZE) / 2 + 40;
// --- Multiple Mazes for More Content ---
var MAZES = [];
// Maze 1 (original)
MAZES[0] = [
// Open up top row for pellets: add pellets (2) and open up more wall spaces for access
[1, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 1], [1, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 1], [1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 1], [1, 3, 1, 2, 1, 2, 2, 2, 2, 1, 2, 2, 2, 1, 2, 1, 3, 1, 1], [1, 2, 1, 2, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 2, 1, 2, 2, 1], [1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1], [1, 2, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 2, 1], [0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0], [1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1],
// Open up middle (row 9, col 0, 9,10,11, 18)
[0, 2, 2, 2, 2, 2, 2, 1, 0, 0, 0, 1, 2, 2, 2, 2, 2, 2, 0], [0, 2, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 2, 0],
// Open up center of row 11 (row 11, col 9)
[1, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 1], [1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1], [1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1], [1, 2, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 2, 1],
// Open up middle (row 15, col 0, 9, 18)
[0, 3, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 3, 0],
// Open up bottom center (row 16, col 0, 9, 18)
[0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0], [0, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 0], [0, 2, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 2, 0], [0, 2, 1, 2, 1, 2, 2, 2, 2, 1, 2, 2, 2, 1, 2, 1, 2, 2, 0],
// Open up (column 10, row 17)
[1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 0, 1, 2, 2, 2, 2, 2, 2, 1],
// Open up bottom center (row 21, col 9)
[1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1],
// Open up bottom row (row 22, col 9)
[1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1]];
// Remove wall below ghost spawn (row 12, col 9) to allow escape
MAZES[0][12][9] = 0;
// Widen the gap below ghost spawn (row 13, cols 8, 9, 10)
MAZES[0][13][8] = 0;
MAZES[0][13][9] = 0;
MAZES[0][13][10] = 0;
// Maze 2 (significantly different, spiral and open center, more power pellets, fewer walls)
MAZES[1] = [[1, 3, 2, 1, 2, 1, 2, 1, 2, 0, 2, 1, 2, 1, 2, 1, 2, 3, 1], [1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1], [1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1], [1, 2, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 2, 1], [1, 2, 1, 3, 2, 2, 2, 1, 2, 1, 2, 1, 2, 2, 2, 3, 1, 2, 1], [1, 2, 1, 2, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 2, 1, 2, 1], [1, 2, 1, 2, 1, 2, 2, 2, 2, 1, 2, 2, 2, 2, 1, 2, 1, 2, 1], [1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1], [1, 2, 2, 2, 1, 2, 1, 2, 2, 0, 2, 2, 1, 2, 1, 2, 2, 2, 1], [1, 1, 1, 2, 1, 2, 1, 2, 1, 1, 1, 2, 1, 2, 1, 2, 1, 1, 1], [0, 2, 2, 2, 1, 2, 2, 2, 2, 0, 2, 2, 2, 2, 1, 2, 2, 2, 0], [0, 1, 1, 2, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 2, 1, 1, 0], [0, 2, 2, 2, 2, 2, 2, 1, 2, 0, 2, 1, 2, 2, 2, 2, 2, 2, 0], [1, 1, 1, 2, 1, 1, 2, 1, 1, 1, 1, 1, 2, 1, 1, 2, 1, 1, 1], [1, 2, 2, 2, 1, 2, 2, 2, 2, 1, 2, 2, 2, 2, 1, 2, 2, 2, 1], [1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1], [1, 2, 1, 2, 1, 2, 2, 2, 2, 1, 2, 2, 2, 2, 1, 2, 1, 2, 1], [1, 2, 1, 3, 2, 2, 2, 1, 2, 1, 2, 1, 2, 2, 2, 3, 1, 2, 1], [1, 2, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 2, 1], [1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1], [1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1], [1, 3, 2, 1, 2, 1, 2, 1, 2, 0, 2, 1, 2, 1, 2, 1, 2, 3, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1]];
// Remove wall below ghost spawn (row 12, col 9) to allow escape
MAZES[1][12][9] = 0;
// Widen the gap below ghost spawn (row 13, cols 8, 9, 10)
MAZES[1][13][8] = 0;
MAZES[1][13][9] = 0;
MAZES[1][13][10] = 0;
// Maze 3 (new, more open, with corridors and more power pellets)
MAZES[2] = [[1, 3, 2, 2, 2, 1, 1, 1, 2, 0, 2, 1, 1, 1, 2, 2, 2, 3, 1], [1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1], [1, 2, 2, 2, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 2, 2, 2, 1], [1, 2, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 2, 1], [1, 2, 1, 3, 2, 2, 2, 1, 2, 1, 2, 1, 2, 2, 2, 3, 1, 2, 1], [1, 2, 1, 2, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 2, 1, 2, 1], [1, 2, 1, 2, 1, 2, 2, 2, 2, 1, 2, 2, 2, 2, 1, 2, 1, 2, 1], [1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1], [1, 2, 2, 2, 1, 2, 1, 2, 2, 0, 2, 2, 1, 2, 1, 2, 2, 2, 1], [1, 1, 1, 2, 1, 2, 1, 2, 1, 1, 1, 2, 1, 2, 1, 2, 1, 1, 1], [0, 2, 2, 2, 1, 2, 2, 2, 2, 0, 2, 2, 2, 2, 1, 2, 2, 2, 0], [0, 1, 1, 2, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 2, 1, 1, 0], [0, 2, 2, 2, 2, 2, 2, 1, 2, 0, 2, 1, 2, 2, 2, 2, 2, 2, 0], [1, 1, 1, 2, 1, 1, 2, 1, 1, 1, 1, 1, 2, 1, 1, 2, 1, 1, 1], [1, 2, 2, 2, 1, 2, 2, 2, 2, 1, 2, 2, 2, 2, 1, 2, 2, 2, 1], [1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1], [1, 2, 1, 2, 1, 2, 2, 2, 2, 1, 2, 2, 2, 2, 1, 2, 1, 2, 1], [1, 2, 1, 3, 2, 2, 2, 1, 2, 1, 2, 1, 2, 2, 2, 3, 1, 2, 1], [1, 2, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 2, 1], [1, 2, 2, 2, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 2, 2, 2, 1], [1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 1, 1], [1, 3, 2, 2, 2, 1, 1, 1, 2, 0, 2, 1, 1, 1, 2, 2, 2, 3, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1]];
// Remove wall below ghost spawn (row 12, col 9) to allow escape
MAZES[2][12][9] = 0;
// Widen the gap below ghost spawn (row 13, cols 8, 9, 10)
MAZES[2][13][8] = 0;
MAZES[2][13][9] = 0;
MAZES[2][13][10] = 0;
// Maze 4 (unique: zig-zag corridors, more open, more power pellets, new pattern)
MAZES[3] = [[1, 3, 2, 2, 2, 1, 2, 2, 2, 0, 2, 2, 2, 1, 2, 2, 2, 3, 1], [1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 1, 1], [1, 2, 2, 2, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 2, 2, 2, 1], [1, 2, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 2, 1], [1, 2, 1, 3, 2, 2, 2, 1, 2, 1, 2, 1, 2, 2, 2, 3, 1, 2, 1], [1, 2, 1, 2, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 2, 1, 2, 1], [1, 2, 1, 2, 1, 2, 2, 2, 2, 1, 2, 2, 2, 2, 1, 2, 1, 2, 1], [1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1], [1, 2, 2, 2, 1, 2, 1, 2, 2, 0, 2, 2, 1, 2, 1, 2, 2, 2, 1], [1, 1, 1, 2, 1, 2, 1, 2, 1, 1, 1, 2, 1, 2, 1, 2, 1, 1, 1], [0, 2, 2, 2, 1, 2, 2, 2, 2, 0, 2, 2, 2, 2, 1, 2, 2, 2, 0], [0, 1, 1, 2, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 2, 1, 1, 0], [0, 2, 2, 2, 2, 2, 2, 1, 2, 0, 2, 1, 2, 2, 2, 2, 2, 2, 0], [1, 1, 1, 2, 1, 1, 2, 1, 1, 1, 1, 1, 2, 1, 1, 2, 1, 1, 1], [1, 2, 2, 2, 1, 2, 2, 2, 2, 1, 2, 2, 2, 2, 1, 2, 2, 2, 1], [1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1], [1, 2, 1, 2, 1, 2, 2, 2, 2, 1, 2, 2, 2, 2, 1, 2, 1, 2, 1], [1, 2, 1, 3, 2, 2, 2, 1, 2, 1, 2, 1, 2, 2, 2, 3, 1, 2, 1], [1, 2, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 2, 1], [1, 2, 2, 2, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 2, 2, 2, 1], [1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 1, 1], [1, 3, 2, 2, 2, 1, 2, 2, 2, 0, 2, 2, 2, 1, 2, 2, 2, 3, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1]];
// Remove wall below ghost spawn (row 12, col 9) to allow escape
MAZES[3][12][9] = 0;
// Widen the gap below ghost spawn (row 13, cols 8, 9, 10)
MAZES[3][13][8] = 0;
MAZES[3][13][9] = 0;
MAZES[3][13][10] = 0;
// Maze 5 (new: diamond pattern, more open, more power pellets, unique layout)
MAZES[4] = [[1, 3, 2, 2, 2, 1, 1, 1, 2, 0, 2, 1, 1, 1, 2, 2, 2, 3, 1], [1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 1, 1], [1, 2, 2, 2, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 2, 2, 2, 1], [1, 2, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 2, 1], [1, 2, 1, 3, 2, 2, 2, 1, 2, 1, 2, 1, 2, 2, 2, 3, 1, 2, 1], [1, 2, 1, 2, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 2, 1, 2, 1], [1, 2, 1, 2, 1, 2, 2, 2, 2, 1, 2, 2, 2, 2, 1, 2, 1, 2, 1], [1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1], [1, 2, 2, 2, 1, 2, 1, 2, 2, 0, 2, 2, 1, 2, 1, 2, 2, 2, 1], [1, 1, 1, 2, 1, 2, 1, 2, 1, 1, 1, 2, 1, 2, 1, 2, 1, 1, 1], [0, 2, 2, 2, 1, 2, 2, 2, 2, 0, 2, 2, 2, 2, 1, 2, 2, 2, 0], [0, 1, 1, 2, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 2, 1, 1, 0], [0, 2, 2, 2, 2, 2, 2, 1, 2, 0, 2, 1, 2, 2, 2, 2, 2, 2, 0], [1, 1, 1, 2, 1, 1, 2, 1, 1, 1, 1, 1, 2, 1, 1, 2, 1, 1, 1], [1, 2, 2, 2, 1, 2, 2, 2, 2, 1, 2, 2, 2, 2, 1, 2, 2, 2, 1], [1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1], [1, 2, 1, 2, 1, 2, 2, 2, 2, 1, 2, 2, 2, 2, 1, 2, 1, 2, 1], [1, 2, 1, 3, 2, 2, 2, 1, 2, 1, 2, 1, 2, 2, 2, 3, 1, 2, 1], [1, 2, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 2, 1], [1, 2, 2, 2, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 2, 2, 2, 1], [1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 1, 1], [1, 3, 2, 2, 2, 1, 1, 1, 2, 0, 2, 1, 1, 1, 2, 2, 2, 3, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1]];
// Remove wall below ghost spawn (row 12, col 9) to allow escape
MAZES[4][12][9] = 0;
// Widen the gap below ghost spawn (row 13, cols 8, 9, 10)
MAZES[4][13][8] = 0;
MAZES[4][13][9] = 0;
MAZES[4][13][10] = 0;
// Current maze index
var currentMazeIndex = 0;
var MAZE = MAZES[currentMazeIndex];
// --- Game State ---
var pellets = [];
var walls = [];
var ghosts = [];
var cherries = [];
var pacman = null;
var score = 0;
var pelletsLeft = 0;
var frightTicks = 0;
var frightActive = false;
var frightDuration = 360; // 6 seconds at 60fps
var swipeStart = null;
var dragNode = null;
var lastGameOver = false;
// Cherry spawn state
var cherryActive = false;
var cherryTimer = 0;
var cherryObj = null;
// --- Score Display ---
var scoreTxt = new Text2('0', {
size: 120,
fill: "#fff"
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// --- Bonus Object State ---
var bonusObject = null;
var bonusObjectActive = false;
var bonusObjectPoints = 0;
// --- Pizza Delivery Ghost State ---
var pizzaDeliveryGhost = null;
var pizzaDeliveryGhostActive = false;
// --- Maze Selection UI ---
var mazeSelectBtn = new Container();
var mazeBtnGfx = LK.getAsset('wall', {
anchorX: 0.5,
anchorY: 0.5
});
mazeBtnGfx.width = 90;
mazeBtnGfx.height = 90;
mazeBtnGfx.tint = 0x00bfff;
mazeSelectBtn.addChild(mazeBtnGfx);
var mazeBtnTxt = new Text2('≡', {
size: 70,
fill: "#fff"
});
mazeBtnTxt.anchor.set(0.5, 0.5);
mazeBtnTxt.x = 0;
mazeBtnTxt.y = 0;
mazeSelectBtn.addChild(mazeBtnTxt);
// Place in top right, with margin, moved down a bit
mazeSelectBtn.x = -100;
mazeSelectBtn.y = 120;
LK.gui.topRight.addChild(mazeSelectBtn);
// Maze selection popup
var mazePopup = new Container();
mazePopup.visible = false;
mazePopup.zIndex = 1000; // ensure on top
var popupBg = LK.getAsset('wall', {
anchorX: 0.5,
anchorY: 0.5
});
popupBg.width = 900;
popupBg.height = 1050;
popupBg.tint = 0x222244;
mazePopup.addChild(popupBg);
// Add "Choose a maze" text at the top of the popup
var chooseMazeTxt = new Text2('Choose a maze', {
size: 70,
fill: "#fff"
});
chooseMazeTxt.anchor.set(0.5, 0.5);
chooseMazeTxt.x = 0;
chooseMazeTxt.y = -400;
mazePopup.addChild(chooseMazeTxt);
var maze1Btn = new Container();
var maze1Gfx = LK.getAsset('wall', {
anchorX: 0.5,
anchorY: 0.5
});
maze1Gfx.width = 220;
maze1Gfx.height = 100;
maze1Gfx.tint = 0x00bfff;
maze1Btn.addChild(maze1Gfx);
var maze1Txt = new Text2('Maze 1', {
size: 60,
fill: "#fff"
});
maze1Txt.anchor.set(0.5, 0.5);
maze1Btn.addChild(maze1Txt);
maze1Btn.x = 0;
maze1Btn.y = -180;
mazePopup.addChild(maze1Btn);
var maze2Btn = new Container();
var maze2Gfx = LK.getAsset('wall', {
anchorX: 0.5,
anchorY: 0.5
});
maze2Gfx.width = 220;
maze2Gfx.height = 100;
maze2Gfx.tint = 0x00bfff;
maze2Btn.addChild(maze2Gfx);
var maze2Txt = new Text2('Maze 2', {
size: 60,
fill: "#fff"
});
maze2Txt.anchor.set(0.5, 0.5);
maze2Btn.addChild(maze2Txt);
maze2Btn.x = 0;
maze2Btn.y = -60;
mazePopup.addChild(maze2Btn);
// Maze 3 button
var maze3Btn = new Container();
var maze3Gfx = LK.getAsset('wall', {
anchorX: 0.5,
anchorY: 0.5
});
maze3Gfx.width = 220;
maze3Gfx.height = 100;
maze3Gfx.tint = 0x00bfff;
maze3Btn.addChild(maze3Gfx);
var maze3Txt = new Text2('Maze 3', {
size: 60,
fill: "#fff"
});
maze3Txt.anchor.set(0.5, 0.5);
maze3Btn.addChild(maze3Txt);
maze3Btn.x = 0;
maze3Btn.y = 60;
mazePopup.addChild(maze3Btn);
// Maze 4 button
var maze4Btn = new Container();
var maze4Gfx = LK.getAsset('wall', {
anchorX: 0.5,
anchorY: 0.5
});
maze4Gfx.width = 220;
maze4Gfx.height = 100;
maze4Gfx.tint = 0x00bfff;
maze4Btn.addChild(maze4Gfx);
var maze4Txt = new Text2('Maze 4', {
size: 60,
fill: "#fff"
});
maze4Txt.anchor.set(0.5, 0.5);
maze4Btn.addChild(maze4Txt);
maze4Btn.x = 0;
maze4Btn.y = 180;
mazePopup.addChild(maze4Btn);
// Maze 5 button
var maze5Btn = new Container();
var maze5Gfx = LK.getAsset('wall', {
anchorX: 0.5,
anchorY: 0.5
});
maze5Gfx.width = 220;
maze5Gfx.height = 100;
maze5Gfx.tint = 0x00bfff;
maze5Btn.addChild(maze5Gfx);
var maze5Txt = new Text2('Maze 5', {
size: 60,
fill: "#fff"
});
maze5Txt.anchor.set(0.5, 0.5);
maze5Btn.addChild(maze5Txt);
maze5Btn.x = 0;
maze5Btn.y = 300;
mazePopup.addChild(maze5Btn);
// Center popup in GUI
mazePopup.x = 0;
mazePopup.y = 0;
LK.gui.center.addChild(mazePopup);
// Button event handlers
mazeSelectBtn.down = function (x, y, obj) {
mazePopup.visible = true;
};
maze1Btn.down = function (x, y, obj) {
if (currentMazeIndex !== 0) {
currentMazeIndex = 0;
MAZE = MAZES[currentMazeIndex];
startGame();
}
mazePopup.visible = false;
};
maze2Btn.down = function (x, y, obj) {
if (currentMazeIndex !== 1) {
currentMazeIndex = 1;
MAZE = MAZES[currentMazeIndex];
startGame();
}
mazePopup.visible = false;
};
// Maze 3 button event handler
maze3Btn.down = function (x, y, obj) {
if (currentMazeIndex !== 2) {
currentMazeIndex = 2;
MAZE = MAZES[currentMazeIndex];
startGame();
}
mazePopup.visible = false;
};
// Maze 4 button event handler
maze4Btn.down = function (x, y, obj) {
if (currentMazeIndex !== 3) {
currentMazeIndex = 3;
MAZE = MAZES[currentMazeIndex];
startGame();
}
mazePopup.visible = false;
};
// Maze 5 button event handler
maze5Btn.down = function (x, y, obj) {
if (currentMazeIndex !== 4) {
currentMazeIndex = 4;
MAZE = MAZES[currentMazeIndex];
startGame();
}
mazePopup.visible = false;
};
// Hide popup if user taps outside popup area
mazePopup.down = function (x, y, obj) {
// Only close if not on a button
var localX = x - mazePopup.x;
var localY = y - mazePopup.y;
// If not inside any button, close
var inBtn = function inBtn(btn) {
return localX > btn.x - 110 && localX < btn.x + 110 && localY > btn.y - 50 && localY < btn.y + 50;
};
if (!(inBtn(maze1Btn) || inBtn(maze2Btn) || inBtn(maze3Btn) || inBtn(maze4Btn) || inBtn(maze5Btn))) {
mazePopup.visible = false;
}
};
// --- Maze Build ---
function buildMaze() {
// Clear previous
for (var i = 0; i < pellets.length; ++i) pellets[i].destroy();
for (var i = 0; i < walls.length; ++i) walls[i].destroy();
for (var i = 0; i < cherries.length; ++i) cherries[i].destroy();
pellets = [];
walls = [];
cherries = [];
cherryActive = false;
cherryTimer = 0;
cherryObj = null;
pelletsLeft = 0;
for (var row = 0; row < MAZE_ROWS; ++row) {
// Defensive: skip if MAZE or row is not defined
if (!MAZE || !MAZE[row]) continue;
for (var col = 0; col < MAZE_COLS; ++col) {
// Defensive: skip if column is not defined
if (typeof MAZE[row][col] === "undefined") continue;
var cell = MAZE[row][col];
var x = MAZE_OFFSET_X + col * CELL_SIZE + CELL_SIZE / 2;
var y = MAZE_OFFSET_Y + row * CELL_SIZE + CELL_SIZE / 2;
if (cell === 1) {
var wall = new Wall();
wall.x = x;
wall.y = y;
game.addChild(wall);
walls.push(wall);
} else if (cell === 2 || cell === 3) {
// Prevent pellets from spawning at ghost spawn locations (center of maze)
// Ghosts spawn at (row 11, col 9), (row 11, col 8), (row 11, col 10), (row 12, col 9)
var isGhostSpawn = row === 11 && (col === 8 || col === 9 || col === 10) || row === 12 && col === 9;
if (!isGhostSpawn) {
var pellet = new Pellet();
pellet.x = x;
pellet.y = y;
pellet.attach(cell === 3);
game.addChild(pellet);
pellets.push(pellet);
pelletsLeft++;
}
}
}
}
}
// --- Pacman Init ---
function spawnPacman() {
if (pacman) pacman.destroy();
pacman = new Pacman();
// Start position: center of maze, row 15, col 9 for maze 1
// For maze 2, pick a valid open cell (not a wall)
if (currentMazeIndex === 1 || currentMazeIndex === 2) {
// Find a non-wall cell near the center, prefer row 15, col 9, else search outward
var found = false;
var tryOrder = [{
col: 9,
row: 15
}, {
col: 9,
row: 14
}, {
col: 9,
row: 16
}, {
col: 8,
row: 15
}, {
col: 10,
row: 15
}, {
col: 8,
row: 14
}, {
col: 10,
row: 14
}, {
col: 8,
row: 16
}, {
col: 10,
row: 16
}];
for (var i = 0; i < tryOrder.length; ++i) {
var c = tryOrder[i].col,
r = tryOrder[i].row;
if (MAZE[r] && typeof MAZE[r][c] !== "undefined" && MAZE[r][c] !== 1) {
pacman.gridX = c;
pacman.gridY = r;
found = true;
break;
}
}
if (!found) {
// fallback: scan for first open cell
for (var r = 0; r < MAZE_ROWS; ++r) {
for (var c = 0; c < MAZE_COLS; ++c) {
if (MAZE[r] && typeof MAZE[r][c] !== "undefined" && MAZE[r][c] !== 1) {
pacman.gridX = c;
pacman.gridY = r;
found = true;
break;
}
}
if (found) break;
}
}
} else if (currentMazeIndex === 3 || currentMazeIndex === 4) {
// For maze 4 and 5, pick a valid open cell (not a wall), like maze 1/2
var found = false;
var tryOrder = [{
col: 9,
row: 15
}, {
col: 9,
row: 14
}, {
col: 9,
row: 16
}, {
col: 8,
row: 15
}, {
col: 10,
row: 15
}, {
col: 8,
row: 14
}, {
col: 10,
row: 14
}, {
col: 8,
row: 16
}, {
col: 10,
row: 16
}];
for (var i = 0; i < tryOrder.length; ++i) {
var c = tryOrder[i].col,
r = tryOrder[i].row;
if (MAZE[r] && typeof MAZE[r][c] !== "undefined" && MAZE[r][c] !== 1) {
pacman.gridX = c;
pacman.gridY = r;
found = true;
break;
}
}
if (!found) {
// fallback: scan for first open cell
for (var r = 0; r < MAZE_ROWS; ++r) {
for (var c = 0; c < MAZE_COLS; ++c) {
if (MAZE[r] && typeof MAZE[r][c] !== "undefined" && MAZE[r][c] !== 1) {
pacman.gridX = c;
pacman.gridY = r;
found = true;
break;
}
}
if (found) break;
}
}
} else {
pacman.gridX = 9;
pacman.gridY = 15;
}
pacman.x = MAZE_OFFSET_X + pacman.gridX * CELL_SIZE + CELL_SIZE / 2;
pacman.y = MAZE_OFFSET_Y + pacman.gridY * CELL_SIZE + CELL_SIZE / 2;
pacman.dir = {
x: 1,
y: 0
};
pacman.nextDir = {
x: 1,
y: 0
};
pacman.moving = true;
game.addChild(pacman);
}
// --- Ghosts Init ---
function spawnGhosts() {
// Remove old ghosts
for (var i = 0; i < ghosts.length; ++i) ghosts[i].destroy();
ghosts = [];
// Four ghosts, different colors, start in center
var ghostColors = ['ghost_red', 'ghost_pink', 'ghost_blue', 'ghost_orange'];
var ghostStarts = [{
col: 9,
row: 11
}, {
col: 8,
row: 11
}, {
col: 10,
row: 11
}, {
col: 9,
row: 12
}];
// For maze 5 (index 4), ensure ghosts spawn in open cells near center
if (currentMazeIndex === 4) {
// Try to find open cells near the classic spawn points
var preferred = [{
col: 9,
row: 11
}, {
col: 8,
row: 11
}, {
col: 10,
row: 11
}, {
col: 9,
row: 12
}, {
col: 8,
row: 12
}, {
col: 10,
row: 12
}, {
col: 9,
row: 10
}, {
col: 8,
row: 10
}, {
col: 10,
row: 10
}];
var found = [];
for (var i = 0; i < preferred.length && found.length < 4; ++i) {
var c = preferred[i].col,
r = preferred[i].row;
if (MAZE[r] && typeof MAZE[r][c] !== "undefined" && MAZE[r][c] !== 1) {
// Only add if not already used
var already = false;
for (var j = 0; j < found.length; ++j) {
if (found[j].col === c && found[j].row === r) {
already = true;
break;
}
}
if (!already) found.push({
col: c,
row: r
});
}
}
// If not enough, fill with any open cell
if (found.length < 4) {
for (var r = 0; r < MAZE_ROWS && found.length < 4; ++r) {
for (var c = 0; c < MAZE_COLS && found.length < 4; ++c) {
if (MAZE[r] && typeof MAZE[r][c] !== "undefined" && MAZE[r][c] !== 1) {
// Only add if not already used
var already = false;
for (var j = 0; j < found.length; ++j) {
if (found[j].col === c && found[j].row === r) {
already = true;
break;
}
}
if (!already) found.push({
col: c,
row: r
});
}
}
}
}
// Use found as ghostStarts
for (var i = 0; i < 4; ++i) {
if (found[i]) ghostStarts[i] = found[i];
}
}
for (var i = 0; i < 4; ++i) {
var ghost = new Ghost();
ghost.attach(ghostColors[i]);
ghost.gridX = ghostStarts[i].col;
ghost.gridY = ghostStarts[i].row;
ghost.x = MAZE_OFFSET_X + ghost.gridX * CELL_SIZE + CELL_SIZE / 2;
ghost.y = MAZE_OFFSET_Y + ghost.gridY * CELL_SIZE + CELL_SIZE / 2;
// Initial direction: up or left/right
if (i === 0) ghost.dir = {
x: 0,
y: -1
};else if (i === 1) ghost.dir = {
x: -1,
y: 0
};else if (i === 2) ghost.dir = {
x: 1,
y: 0
};else ghost.dir = {
x: 0,
y: 1
};
ghost.frightened = false;
ghosts.push(ghost);
game.addChild(ghost);
}
}
// --- Utility: Grid <-> Pixel ---
function posToGrid(x, y) {
var col = Math.floor((x - MAZE_OFFSET_X) / CELL_SIZE);
var row = Math.floor((y - MAZE_OFFSET_Y) / CELL_SIZE);
return {
col: col,
row: row
};
}
function gridToPos(col, row) {
return {
x: MAZE_OFFSET_X + col * CELL_SIZE + CELL_SIZE / 2,
y: MAZE_OFFSET_Y + row * CELL_SIZE + CELL_SIZE / 2
};
}
function isWall(col, row) {
if (col < 0 || col >= MAZE_COLS || row < 0 || row >= MAZE_ROWS) return true;
// Defensive: check MAZE and MAZE[row] exist, and col is defined
if (!MAZE || !MAZE[row] || typeof MAZE[row][col] === "undefined") return true;
return MAZE[row][col] === 1;
}
// --- Pacman Movement & Input ---
function canMove(col, row, dir) {
var nextCol = col + dir.x;
var nextRow = row + dir.y;
// Prevent Pacman from moving through PizzaDeliveryGhost by treating him as a wall
if (pizzaDeliveryGhostActive && pizzaDeliveryGhost) {
var pizzaGrid = posToGrid(pizzaDeliveryGhost.x, pizzaDeliveryGhost.y);
if (nextCol === pizzaGrid.col && nextRow === pizzaGrid.row) {
return false;
}
}
return !isWall(nextCol, nextRow);
}
function updatePacmanDirection() {
var grid = posToGrid(pacman.x, pacman.y);
// Snap to center of cell
var center = gridToPos(grid.col, grid.row);
var dx = Math.abs(pacman.x - center.x);
var dy = Math.abs(pacman.y - center.y);
if (dx < 4 && dy < 4) {
// At center, can change direction
if (canMove(grid.col, grid.row, pacman.nextDir)) {
pacman.dir = {
x: pacman.nextDir.x,
y: pacman.nextDir.y
};
}
// If can't move in current dir, stop
if (!canMove(grid.col, grid.row, pacman.dir)) {
pacman.moving = false;
} else {
// Prevent Pacman from being stuck inside PizzaDeliveryGhost's tile
if (pizzaDeliveryGhostActive && pizzaDeliveryGhost) {
var pizzaGrid = posToGrid(pizzaDeliveryGhost.x, pizzaDeliveryGhost.y);
if (grid.col === pizzaGrid.col && grid.row === pizzaGrid.row) {
pacman.moving = false;
// Optionally, snap Pacman out of the tile (push back)
// Try to move Pacman to previous cell if possible
var prevCol = grid.col - pacman.dir.x;
var prevRow = grid.row - pacman.dir.y;
if (!isWall(prevCol, prevRow)) {
var prevPos = gridToPos(prevCol, prevRow);
pacman.x = prevPos.x;
pacman.y = prevPos.y;
}
} else {
pacman.moving = true;
}
} else {
pacman.moving = true;
}
}
// Snap to center
pacman.x = center.x;
pacman.y = center.y;
}
}
// --- Ghost Movement (Random for MVP, smarter later) ---
function getValidGhostDirs(ghost) {
var grid = posToGrid(ghost.x, ghost.y);
var dirs = [{
x: 1,
y: 0
}, {
x: -1,
y: 0
}, {
x: 0,
y: 1
}, {
x: 0,
y: -1
}];
var valid = [];
for (var i = 0; i < dirs.length; ++i) {
var d = dirs[i];
// Don't reverse direction
if (ghost.dir.x === -d.x && ghost.dir.y === -d.y) continue;
if (!isWall(grid.col + d.x, grid.row + d.y)) valid.push(d);
}
return valid;
}
function updateGhostDirection(ghost) {
var grid = posToGrid(ghost.x, ghost.y);
var center = gridToPos(grid.col, grid.row);
var dx = Math.abs(ghost.x - center.x);
var dy = Math.abs(ghost.y - center.y);
if (dx < 4 && dy < 4) {
// At center, pick new direction
var valid = getValidGhostDirs(ghost);
if (valid.length === 0) {
ghost.dir = {
x: -ghost.dir.x,
y: -ghost.dir.y
};
} else {
// MVP: random direction
var idx = Math.floor(Math.random() * valid.length);
ghost.dir = {
x: valid[idx].x,
y: valid[idx].y
};
}
// Snap to center
ghost.x = center.x;
ghost.y = center.y;
}
}
// --- Pellet Eating ---
function checkPelletEat() {
for (var i = pellets.length - 1; i >= 0; --i) {
var pellet = pellets[i];
var dx = pacman.x - pellet.x;
var dy = pacman.y - pellet.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 48) {
// Eat pellet
if (pellet.isPower) {
frightTicks = frightDuration;
frightActive = true;
for (var g = 0; g < ghosts.length; ++g) {
ghosts[g].setFrightened(true);
}
}
pellet.destroy();
pellets.splice(i, 1);
pelletsLeft--;
score += pellet.isPower ? 50 : 10;
scoreTxt.setText(score);
LK.setScore(score);
if (pelletsLeft === 0) {
// Switch to next maze if available, else loop back to first
currentMazeIndex = (currentMazeIndex + 1) % MAZES.length;
MAZE = MAZES[currentMazeIndex];
LK.showYouWin();
}
}
}
}
// --- Ghost Collisions ---
function checkGhostCollisions() {
for (var i = 0; i < ghosts.length; ++i) {
var ghost = ghosts[i];
var dx = pacman.x - ghost.x;
var dy = pacman.y - ghost.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 60) {
if (frightActive && ghost.frightened && ghost.visible !== false) {
// Eat ghost: make invisible, respawn after fright ends
ghost.visible = false;
score += 200;
scoreTxt.setText(score);
LK.setScore(score);
} else if (!frightActive && ghost.visible !== false) {
// Pacman dies
if (!lastGameOver) {
LK.effects.flashScreen(0xff0000, 1000);
LK.showGameOver();
lastGameOver = true;
}
}
}
}
}
// --- Frightened Mode Timer ---
function updateFrightened() {
if (frightActive) {
frightTicks--;
if (frightTicks <= 0) {
frightActive = false;
for (var g = 0; g < ghosts.length; ++g) {
ghosts[g].setFrightened(false);
// If ghost was eaten (invisible), respawn at home
if (ghosts[g].visible === false) {
// Respawn at original spawn for this ghost
var spawnIdx = g;
var ghostStarts = [{
col: 9,
row: 11
}, {
col: 8,
row: 11
}, {
col: 10,
row: 11
}, {
col: 9,
row: 12
}];
// For maze 5 (index 4), ensure ghosts respawn in open cells near center
if (currentMazeIndex === 4) {
var preferred = [{
col: 9,
row: 11
}, {
col: 8,
row: 11
}, {
col: 10,
row: 11
}, {
col: 9,
row: 12
}, {
col: 8,
row: 12
}, {
col: 10,
row: 12
}, {
col: 9,
row: 10
}, {
col: 8,
row: 10
}, {
col: 10,
row: 10
}];
var found = [];
for (var i = 0; i < preferred.length && found.length < 4; ++i) {
var c = preferred[i].col,
r = preferred[i].row;
if (MAZE[r] && typeof MAZE[r][c] !== "undefined" && MAZE[r][c] !== 1) {
// Only add if not already used
var already = false;
for (var j = 0; j < found.length; ++j) {
if (found[j].col === c && found[j].row === r) {
already = true;
break;
}
}
if (!already) found.push({
col: c,
row: r
});
}
}
// If not enough, fill with any open cell
if (found.length < 4) {
for (var r = 0; r < MAZE_ROWS && found.length < 4; ++r) {
for (var c = 0; c < MAZE_COLS && found.length < 4; ++c) {
if (MAZE[r] && typeof MAZE[r][c] !== "undefined" && MAZE[r][c] !== 1) {
// Only add if not already used
var already = false;
for (var j = 0; j < found.length; ++j) {
if (found[j].col === c && found[j].row === r) {
already = true;
break;
}
}
if (!already) found.push({
col: c,
row: r
});
}
}
}
}
if (found[spawnIdx]) ghostStarts[spawnIdx] = found[spawnIdx];
}
var spawn = ghostStarts[spawnIdx];
var home = gridToPos(spawn.col, spawn.row);
ghosts[g].x = home.x;
ghosts[g].y = home.y;
ghosts[g].dir = {
x: 0,
y: -1
};
ghosts[g].visible = true;
ghosts[g].gridX = spawn.col;
ghosts[g].gridY = spawn.row;
}
}
}
}
}
// --- Input: Swipe to set direction ---
function getSwipeDir(start, end) {
var dx = end.x - start.x;
var dy = end.y - start.y;
if (Math.abs(dx) > Math.abs(dy)) {
if (dx > 32) return {
x: 1,
y: 0
};
if (dx < -32) return {
x: -1,
y: 0
};
} else {
if (dy > 32) return {
x: 0,
y: 1
};
if (dy < -32) return {
x: 0,
y: -1
};
}
return null;
}
game.down = function (x, y, obj) {
swipeStart = {
x: x,
y: y
};
dragNode = pacman;
};
game.move = function (x, y, obj) {
if (!swipeStart) return;
var swipeEnd = {
x: x,
y: y
};
var dir = getSwipeDir(swipeStart, swipeEnd);
if (dir) {
pacman.nextDir = dir;
swipeStart = null;
}
};
game.up = function (x, y, obj) {
swipeStart = null;
dragNode = null;
};
// --- Game Update Loop ---
game.update = function () {
if (!pacman) return;
lastGameOver = false;
updatePacmanDirection();
pacman.update();
for (var i = 0; i < ghosts.length; ++i) {
updateGhostDirection(ghosts[i]);
ghosts[i].update();
}
// --- Cherry spawn logic ---
// Spawn cherry after 1/3 pellets are eaten, if not already spawned
if (!cherryActive && pelletsLeft > 0 && pelletsLeft <= Math.floor(pelletsLeft + cherries.length + pellets.length) * 2 / 3) {
// Find a random empty cell (not wall, not pellet, not ghost spawn)
var openCells = [];
for (var row = 0; row < MAZE_ROWS; ++row) {
for (var col = 0; col < MAZE_COLS; ++col) {
if (MAZE[row] && typeof MAZE[row][col] !== "undefined" && MAZE[row][col] !== 1) {
// Not wall
var isGhostSpawn = row === 11 && (col === 8 || col === 9 || col === 10) || row === 12 && col === 9;
if (!isGhostSpawn) {
// Not already a pellet
var pelletHere = false;
for (var p = 0; p < pellets.length; ++p) {
var grid = posToGrid(pellets[p].x, pellets[p].y);
if (grid.col === col && grid.row === row) {
pelletHere = true;
break;
}
}
// Not already a cherry
var cherryHere = false;
for (var c = 0; c < cherries.length; ++c) {
var grid = posToGrid(cherries[c].x, cherries[c].y);
if (grid.col === col && grid.row === row) {
cherryHere = true;
break;
}
}
if (!pelletHere && !cherryHere) {
openCells.push({
col: col,
row: row
});
}
}
}
}
}
if (openCells.length > 0) {
var idx = Math.floor(Math.random() * openCells.length);
var pos = gridToPos(openCells[idx].col, openCells[idx].row);
cherryObj = new Cherry();
cherryObj.x = pos.x;
cherryObj.y = pos.y;
game.addChild(cherryObj);
cherries.push(cherryObj);
cherryActive = true;
cherryTimer = 600; // 10 seconds at 60fps
}
}
// Cherry timer and removal
if (cherryActive && cherryObj) {
cherryTimer--;
if (cherryTimer <= 0) {
cherryObj.destroy();
cherries.splice(cherries.indexOf(cherryObj), 1);
cherryObj = null;
cherryActive = false;
}
}
checkPelletEat();
// --- Cherry eat logic ---
if (cherryActive && cherryObj) {
var dx = pacman.x - cherryObj.x;
var dy = pacman.y - cherryObj.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 60) {
// Eat cherry!
cherryObj.destroy();
cherries.splice(cherries.indexOf(cherryObj), 1);
cherryObj = null;
cherryActive = false;
score += 200;
scoreTxt.setText(score);
LK.setScore(score);
}
}
// --- BonusObject spawn and update ---
// Only spawn once per game session
if (!bonusObjectActive) {
// Spawn in a random open cell
var openCells = [];
for (var row = 0; row < MAZE_ROWS; ++row) {
for (var col = 0; col < MAZE_COLS; ++col) {
if (MAZE[row] && typeof MAZE[row][col] !== "undefined" && MAZE[row][col] !== 1) {
// Not wall
var isGhostSpawn = row === 11 && (col === 8 || col === 9 || col === 10) || row === 12 && col === 9;
if (!isGhostSpawn) {
openCells.push({
col: col,
row: row
});
}
}
}
}
if (openCells.length > 0) {
var idx = Math.floor(Math.random() * openCells.length);
var pos = gridToPos(openCells[idx].col, openCells[idx].row);
bonusObject = new BonusObject();
bonusObject.x = pos.x;
bonusObject.y = pos.y;
// Randomize initial direction
bonusObject.moveDir = {
x: Math.random() < 0.5 ? 1 : -1,
y: Math.random() < 0.5 ? 1 : -1
};
game.addChild(bonusObject);
bonusObjectActive = true;
bonusObjectPoints = 1000 + Math.floor(Math.random() * 1001); // 1000-2000
}
}
// Update BonusObject movement
if (bonusObjectActive && bonusObject) {
bonusObject.update();
// Check collision with Pacman
var dx = pacman.x - bonusObject.x;
var dy = pacman.y - bonusObject.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 60) {
// Pacman eats bonus object!
if (bonusObject) {
bonusObject.destroy();
bonusObject = null;
bonusObjectActive = false;
score += bonusObjectPoints;
scoreTxt.setText(score);
LK.setScore(score);
}
}
}
// --- PizzaDeliveryGhost update and tip logic ---
if (pizzaDeliveryGhostActive && pizzaDeliveryGhost) {
pizzaDeliveryGhost.update();
// If Pacman is close and tip not yet paid, check for payment
if (pizzaDeliveryGhost.tipAsked && pizzaDeliveryGhost.tipText.visible && pacman) {
var dx = pacman.x - pizzaDeliveryGhost.x;
var dy = pacman.y - pizzaDeliveryGhost.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 60 && score >= pizzaDeliveryGhost.tipAmount) {
// Pay tip in pellets (score)
score -= pizzaDeliveryGhost.tipAmount;
scoreTxt.setText(score);
LK.setScore(score);
pizzaDeliveryGhost.tipText.setText('Thanks!');
// Hide text after a short time and remove pizzaDeliveryGhost
LK.setTimeout(function () {
if (pizzaDeliveryGhost && pizzaDeliveryGhost.tipText) {
pizzaDeliveryGhost.tipText.visible = false;
}
if (pizzaDeliveryGhost) {
pizzaDeliveryGhost.destroy();
pizzaDeliveryGhost = null;
pizzaDeliveryGhostActive = false;
}
}, 1200);
pizzaDeliveryGhost.tipAsked = false; // Only ask once per session
}
}
}
checkGhostCollisions();
updateFrightened();
};
// --- Game Start ---
function startGame() {
score = 0;
LK.setScore(0);
scoreTxt.setText('0');
// Do not reset currentMazeIndex here, so maze selection works
MAZE = MAZES[currentMazeIndex];
buildMaze();
spawnPacman();
spawnGhosts();
// Reset all ghosts to spawn positions and visible, like at the beginning of the game
var ghostStarts = [{
col: 9,
row: 11
}, {
col: 8,
row: 11
}, {
col: 10,
row: 11
}, {
col: 9,
row: 12
}];
// For maze 5 (index 4), ensure ghosts reset in open cells near center
if (currentMazeIndex === 4) {
var preferred = [{
col: 9,
row: 11
}, {
col: 8,
row: 11
}, {
col: 10,
row: 11
}, {
col: 9,
row: 12
}, {
col: 8,
row: 12
}, {
col: 10,
row: 12
}, {
col: 9,
row: 10
}, {
col: 8,
row: 10
}, {
col: 10,
row: 10
}];
var found = [];
for (var i = 0; i < preferred.length && found.length < 4; ++i) {
var c = preferred[i].col,
r = preferred[i].row;
if (MAZE[r] && typeof MAZE[r][c] !== "undefined" && MAZE[r][c] !== 1) {
// Only add if not already used
var already = false;
for (var j = 0; j < found.length; ++j) {
if (found[j].col === c && found[j].row === r) {
already = true;
break;
}
}
if (!already) found.push({
col: c,
row: r
});
}
}
// If not enough, fill with any open cell
if (found.length < 4) {
for (var r = 0; r < MAZE_ROWS && found.length < 4; ++r) {
for (var c = 0; c < MAZE_COLS && found.length < 4; ++c) {
if (MAZE[r] && typeof MAZE[r][c] !== "undefined" && MAZE[r][c] !== 1) {
// Only add if not already used
var already = false;
for (var j = 0; j < found.length; ++j) {
if (found[j].col === c && found[j].row === r) {
already = true;
break;
}
}
if (!already) found.push({
col: c,
row: r
});
}
}
}
}
for (var i = 0; i < 4; ++i) {
if (found[i]) ghostStarts[i] = found[i];
}
}
for (var g = 0; g < ghosts.length; ++g) {
var spawn = ghostStarts[g];
var home = gridToPos(spawn.col, spawn.row);
ghosts[g].x = home.x;
ghosts[g].y = home.y;
ghosts[g].dir = {
x: 0,
y: -1
};
ghosts[g].visible = true;
ghosts[g].gridX = spawn.col;
ghosts[g].gridY = spawn.row;
ghosts[g].setFrightened(false);
}
frightTicks = 0;
frightActive = false;
lastGameOver = false;
// Reset cherry state
for (var i = 0; i < cherries.length; ++i) cherries[i].destroy();
cherries = [];
cherryActive = false;
cherryTimer = 0;
cherryObj = null;
}
// Reset pizza delivery ghost state
if (pizzaDeliveryGhost) pizzaDeliveryGhost.destroy();
pizzaDeliveryGhost = null;
pizzaDeliveryGhostActive = false;
// Spawn PizzaDeliveryGhost on specific tiles that block required Pacman paths
// Define a list of "blocking" tile positions for each maze (row, col) that force Pacman to go around
var pizzaBlockTilesByMaze = [
// Maze 1: block a key corridor (e.g. row 7, col 9)
[{
row: 7,
col: 9
}, {
row: 15,
col: 9
}],
// Maze 2: block a spiral entrance (e.g. row 10, col: 9)
[{
row: 10,
col: 9
}, {
row: 12,
col: 9
}],
// Maze 3: block a corridor (e.g. row 7, col: 9)
[{
row: 7,
col: 9
}, {
row: 15,
col: 9
}],
// Maze 4: block a zig-zag (e.g. row 7, col: 9)
[{
row: 7,
col: 9
}, {
row: 15,
col: 9
}],
// Maze 5: block a diamond center (e.g. row 11, col: 9)
[{
row: 11,
col: 9
}, {
row: 15,
col: 9
}]];
var pizzaTiles = pizzaBlockTilesByMaze[currentMazeIndex] || [];
// Find the first available blocking tile that is open in the current maze
var foundPizzaTile = null;
for (var i = 0; i < pizzaTiles.length; ++i) {
var t = pizzaTiles[i];
if (MAZE[t.row] && typeof MAZE[t.row][t.col] !== "undefined" && MAZE[t.row][t.col] !== 1 // not a wall
) {
foundPizzaTile = t;
break;
}
}
if (foundPizzaTile) {
var pos = gridToPos(foundPizzaTile.col, foundPizzaTile.row);
pizzaDeliveryGhost = new PizzaDeliveryGhost();
pizzaDeliveryGhost.x = pos.x;
pizzaDeliveryGhost.y = pos.y;
game.addChild(pizzaDeliveryGhost);
pizzaDeliveryGhostActive = true;
}
startGame();
Pacman red ghost. In-Game asset. 2d. High contrast. No shadows
Pacman pink ghost. In-Game asset. 2d. High contrast. No shadows
Pacman blue ghost. In-Game asset. 2d. High contrast. No shadows
Pacman orange ghost. In-Game asset. 2d. High contrast. No shadows
Pacman dark blue ghost with plain white eyes. In-Game asset. 2d. High contrast. No shadows
Pacman with mouth closed pixilated . No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
Pacman. In-Game asset. 2d. High contrast. shadows. Outline. Pixilated
Pacman with mouth opened pixilated . No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
Pacman cherry. In-Game asset. 2d. High contrast. No shadows
Pixilated ice cream. In-Game asset. 2d. High contrast. No shadows
Pixilated pizza. In-Game asset. 2d. High contrast. No shadows