/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// --- Dot (Pellet) ---
var Dot = Container.expand(function () {
var self = Container.call(this);
var dotAsset = self.attachAsset('dot', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = dotAsset.width / 2;
return self;
});
// --- Ghost ---
var Ghost = Container.expand(function () {
var self = Container.call(this);
// Attach ghost asset (color set later)
var ghostAsset = self.attachAsset('ghostBody', {
anchorX: 0.5,
anchorY: 0.5
});
// Ghost color
self.color = 0xffff00;
ghostAsset.tint = self.color;
// Movement speed
self.speed = 6.4; // 20% slower than previous (was 8)
// Direction
self.dir = {
x: 0,
y: 0
};
// Scatter/Chase mode
self.mode = 'chase'; // or 'scatter' or 'frightened'
self.frightenedTicks = 0;
// For collision
self.radius = ghostAsset.width / 2;
// Ghost AI type: 'blinky', 'pinky', 'inky', 'clyde'
self.ghostType = 'blinky';
// Set color
self.setColor = function (color) {
self.color = color;
ghostAsset.tint = color;
};
// Set type
self.setType = function (type) {
self.ghostType = type;
};
// Set mode
self.setMode = function (mode) {
self.mode = mode;
if (mode === 'frightened') {
ghostAsset.tint = 0x2222ff;
} else {
ghostAsset.tint = self.color;
}
};
// Move to tile
self.moveToTile = function (tile) {
self.x = tile.x * tileSize + tileSize / 2 + mazeOffsetX;
self.y = tile.y * tileSize + tileSize / 2 + mazeOffsetY;
};
// Get current tile
self.getTile = function () {
return {
x: Math.floor((self.x - mazeOffsetX) / tileSize),
y: Math.floor((self.y - mazeOffsetY) / tileSize)
};
};
// AI: get target tile
self.getTargetTile = function () {
// If player exists, target the player's current tile (chase mode)
if (typeof player !== "undefined" && player && typeof player.getTile === "function") {
var targetTile = player.getTile();
return {
x: targetTile.x,
y: targetTile.y
};
}
// Fallback: random movement as before
var curTile = self.getTile();
var dirs = [{
x: 0,
y: -1
},
// up
{
x: 1,
y: 0
},
// right
{
x: 0,
y: 1
},
// down
{
x: -1,
y: 0
} // left
];
// Try to keep going in the same direction if possible
if (canMove(curTile.x, curTile.y, self.dir.x, self.dir.y)) {
return {
x: curTile.x + self.dir.x,
y: curTile.y + self.dir.y
};
}
// Otherwise, pick a random valid direction
var options = [];
for (var i = 0; i < dirs.length; i++) {
var d = dirs[i];
if (canMove(curTile.x, curTile.y, d.x, d.y)) {
options.push(d);
}
}
if (options.length > 0) {
var idx = Math.floor(Math.random() * options.length);
var d = options[idx];
return {
x: curTile.x + d.x,
y: curTile.y + d.y
};
}
// If stuck, stay in place
return curTile;
};
// AI: choose direction
self.chooseDir = function () {
var curTile = self.getTile();
var target = self.getTargetTile();
// All possible directions
var dirs = [{
x: 0,
y: -1
},
// up
{
x: 1,
y: 0
},
// right
{
x: 0,
y: 1
},
// down
{
x: -1,
y: 0
} // left
];
// Don't reverse
var opp = {
x: -self.dir.x,
y: -self.dir.y
};
// For frightened, pick random
if (self.mode === 'frightened') {
var options = [];
for (var i = 0; i < dirs.length; i++) {
var d = dirs[i];
if ((d.x !== opp.x || d.y !== opp.y) && canMove(curTile.x, curTile.y, d.x, d.y)) {
options.push(d);
}
}
if (options.length > 0) {
var idx = Math.floor(Math.random() * options.length);
return options[idx];
}
return self.dir;
}
// Otherwise, pick direction that minimizes distance to target
var minDist = 99999,
bestDir = self.dir;
for (var i = 0; i < dirs.length; i++) {
var d = dirs[i];
if ((d.x !== opp.x || d.y !== opp.y) && canMove(curTile.x, curTile.y, d.x, d.y)) {
var nx = curTile.x + d.x,
ny = curTile.y + d.y;
var dist = (target.x - nx) * (target.x - nx) + (target.y - ny) * (target.y - ny);
if (dist < minDist) {
minDist = dist;
bestDir = d;
}
}
}
return bestDir;
};
// Update per tick
self.update = function () {
// If in tunnel, wrap
if (self.x < mazeOffsetX - tileSize / 2) {
self.x = mazeOffsetX + (mazeCols - 1) * tileSize + tileSize / 2;
}
if (self.x > mazeOffsetX + (mazeCols - 1) * tileSize + tileSize / 2) {
self.x = mazeOffsetX - tileSize / 2;
}
// Frightened mode timer
if (self.mode === 'frightened') {
self.frightenedTicks--;
if (self.frightenedTicks <= 0) {
self.setMode('chase');
}
}
// At center of tile, choose new dir
var curTile = self.getTile();
var cx = curTile.x * tileSize + tileSize / 2 + mazeOffsetX;
var cy = curTile.y * tileSize + tileSize / 2 + mazeOffsetY;
var dist = Math.abs(self.x - cx) + Math.abs(self.y - cy);
if (dist < 2) {
// Always choose direction based on AI (chase, scatter, frightened)
var newDir = self.chooseDir();
self.dir = newDir;
// Snap to center
self.x = cx;
self.y = cy;
}
// Move if possible
if (canMove(curTile.x, curTile.y, self.dir.x, self.dir.y)) {
self.x += self.dir.x * self.speed;
self.y += self.dir.y * self.speed;
}
};
return self;
});
// --- Player (Pac) ---
var Player = Container.expand(function () {
var self = Container.call(this);
// Attach yellow circle asset for player
var playerAsset = self.attachAsset('playerCircle', {
anchorX: 0.5,
anchorY: 0.5
});
// Movement speed (pixels per tick)
self.speed = 10;
// Current direction: {x: -1, y:0} etc.
self.dir = {
x: 0,
y: 0
};
self.nextDir = {
x: 0,
y: 0
};
// For smooth movement
self.targetTile = null;
// For mouth animation
self.mouthOpen = true;
self.mouthTween = null;
// For collision
self.radius = playerAsset.width / 2;
// Animate mouth (simple scaleX)
function animateMouth() {
if (self.mouthTween) tween.stop(playerAsset, {
scaleX: true
});
playerAsset.scaleX = 1;
self.mouthTween = tween(playerAsset, {
scaleX: 0.7
}, {
duration: 120,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(playerAsset, {
scaleX: 1
}, {
duration: 120,
easing: tween.easeInOut,
onFinish: animateMouth
});
}
});
}
animateMouth();
// Set direction
self.setDir = function (dx, dy) {
self.nextDir = {
x: dx,
y: dy
};
};
// Move to tile
self.moveToTile = function (tile) {
self.x = tile.x * tileSize + tileSize / 2 + mazeOffsetX;
self.y = tile.y * tileSize + tileSize / 2 + mazeOffsetY;
};
// Get current tile
self.getTile = function () {
return {
x: Math.floor((self.x - mazeOffsetX) / tileSize),
y: Math.floor((self.y - mazeOffsetY) / tileSize)
};
};
// Update per tick
self.update = function () {
// If in tunnel, wrap
if (self.x < mazeOffsetX - tileSize / 2) {
self.x = mazeOffsetX + (mazeCols - 1) * tileSize + tileSize / 2;
}
if (self.x > mazeOffsetX + (mazeCols - 1) * tileSize + tileSize / 2) {
self.x = mazeOffsetX - tileSize / 2;
}
// Try to turn if possible
var curTile = self.getTile();
if (canMove(curTile.x, curTile.y, self.nextDir.x, self.nextDir.y)) {
self.dir = {
x: self.nextDir.x,
y: self.nextDir.y
};
}
// Move if possible
if (canMove(curTile.x, curTile.y, self.dir.x, self.dir.y)) {
self.x += self.dir.x * self.speed;
self.y += self.dir.y * self.speed;
} else {
// Snap to center of tile
self.moveToTile(curTile);
}
};
return self;
});
// --- PowerDot (Power Pellet) ---
var PowerDot = Container.expand(function () {
var self = Container.call(this);
var pdAsset = self.attachAsset('powerDot', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = pdAsset.width / 2;
return self;
});
// --- Wall ---
var Wall = Container.expand(function () {
var self = Container.call(this);
var wallAsset = self.attachAsset('wallBlock', {
anchorX: 0.5,
anchorY: 0.5
});
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x000000
});
/****
* Game Code
****/
// Example: A new, more open map with a different wall and dot pattern
// --- Maze Layout ---
// 0: empty, 1: wall, 2: dot, 3: power dot, 4: player start, 5: ghost start, 6: tunnel
// Simple 19x21 maze (Pac-Man like, but smaller for MVP)
var mazeData = [[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1], [1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1], [1, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 1], [1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1], [1, 2, 1, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 1, 2, 1], [1, 2, 1, 2, 1, 2, 1, 1, 1, 5, 1, 1, 1, 2, 1, 2, 1, 2, 1], [1, 2, 1, 2, 1, 2, 1, 2, 2, 2, 2, 2, 1, 2, 1, 2, 1, 2, 1], [1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1], [1, 2, 2, 2, 1, 2, 2, 2, 1, 4, 1, 2, 2, 2, 1, 2, 2, 2, 1], [1, 1, 1, 2, 1, 1, 1, 2, 1, 1, 1, 2, 1, 1, 1, 2, 1, 1, 1], [1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1], [1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1], [1, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 1], [1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1], [1, 2, 1, 2, 1, 2, 2, 2, 2, 2, 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, 1, 2, 1, 2, 1, 2, 2, 2, 2, 2, 1, 2, 1, 2, 1, 2, 1], [1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1], [1, 2, 2, 2, 2, 2, 2, 2, 2, 3, 2, 2, 2, 2, 2, 2, 2, 2, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]];
var mazeRows = mazeData.length;
var mazeCols = mazeData[0].length;
// --- Maze Rendering ---
var tileSize = 90;
var mazeWidth = mazeCols * tileSize;
var mazeHeight = mazeRows * tileSize;
var mazeOffsetX = Math.floor((2048 - mazeWidth) / 2);
var mazeOffsetY = Math.floor((2732 - mazeHeight) / 2);
// --- Asset Initialization ---
// --- Game State ---
var player = null;
var ghosts = [];
var dots = [];
var powerDots = [];
var walls = [];
var lives = 3;
var level = 1;
var score = 0;
var gameActive = true;
var frightenedTicksMax = 360; // 6 seconds at 60fps
// --- Difficulty Levels ---
var difficulty = "Normal"; // Options: "Easy", "Normal", "Hard"
// You can change this value to "Easy" or "Hard" to test
function getGhostSpeed() {
if (difficulty === "Easy") return 5.2; // 35% slower than original
if (difficulty === "Hard") return 8.8; // 10% faster than original
return 6.4; // Normal (20% slower than original)
}
// --- GUI ---
var scoreTxt = new Text2('0', {
size: 100,
fill: "#fff"
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
var livesTxt = new Text2('♥♥♥', {
size: 80,
fill: 0xFF4444
});
livesTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(livesTxt);
livesTxt.y = 110;
// Show difficulty in GUI (optional)
var diffTxt = new Text2('Difficulty: ' + difficulty, {
size: 60,
fill: "#fff"
});
diffTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(diffTxt);
diffTxt.y = 200;
// --- Settings Button (⚙️) ---
var settingsBtn = new Container();
var settingsBg = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.1,
scaleY: 1.1,
tint: 0x222222
});
settingsBtn.addChild(settingsBg);
var settingsTxt = new Text2("⚙️", {
size: 80,
fill: "#fff"
});
settingsTxt.anchor.set(0.5, 0.5);
settingsBtn.addChild(settingsTxt);
// Place in top-right, with margin
settingsBtn.x = 2048 - 100;
settingsBtn.y = 100;
LK.gui.topRight.addChild(settingsBtn);
settingsBtn.zIndex = 1000; // ensure on top
// --- Helper: canMove ---
function canMove(x, y, dx, dy) {
var nx = x + dx,
ny = y + dy;
if (nx < 0) nx = mazeCols - 1;
if (nx >= mazeCols) nx = 0;
if (ny < 0 || ny >= mazeRows) return false;
if (mazeData[ny][nx] === 1) return false;
return true;
}
// --- Helper: reset maze ---
function resetMaze() {
// Remove old
for (var i = 0; i < dots.length; i++) dots[i].destroy();
for (var i = 0; i < powerDots.length; i++) powerDots[i].destroy();
for (var i = 0; i < walls.length; i++) walls[i].destroy();
dots = [];
powerDots = [];
walls = [];
// Place maze
for (var y = 0; y < mazeRows; y++) {
for (var x = 0; x < mazeCols; x++) {
var v = mazeData[y][x];
var px = x * tileSize + tileSize / 2 + mazeOffsetX;
var py = y * tileSize + tileSize / 2 + mazeOffsetY;
if (v === 1) {
var wall = new Wall();
wall.x = px;
wall.y = py;
game.addChild(wall);
walls.push(wall);
} else if (v === 2) {
var dot = new Dot();
dot.x = px;
dot.y = py;
game.addChild(dot);
dots.push(dot);
} else if (v === 3) {
var pd = new PowerDot();
pd.x = px;
pd.y = py;
game.addChild(pd);
powerDots.push(pd);
}
}
}
}
// --- Helper: reset player and ghosts ---
function resetActors() {
// Remove old
if (player) player.destroy();
for (var i = 0; i < ghosts.length; i++) ghosts[i].destroy();
ghosts = [];
// Place player
// Start player at the first open black area (not a wall) from the top, scanning left to right
var foundStart = false;
for (var y = 0; y < mazeRows && !foundStart; y++) {
for (var x = 0; x < mazeCols && !foundStart; x++) {
if (mazeData[y][x] !== 1) {
// not a wall
player = new Player();
player.x = x * tileSize + tileSize / 2 + mazeOffsetX;
player.y = y * tileSize + tileSize / 2 + mazeOffsetY;
game.addChild(player);
foundStart = true;
}
}
}
// Fallback in case all are walls (should never happen)
if (!player) {
player = new Player();
player.x = 1 * tileSize + tileSize / 2 + mazeOffsetX;
player.y = 1 * tileSize + tileSize / 2 + mazeOffsetY;
game.addChild(player);
}
// Place ghosts
var ghostColors = [0xffff00, 0xffff00, 0xffff00, 0xffff00];
var ghostTypes = ['blinky', 'pinky', 'inky', 'clyde'];
var ghostCount = 4;
var gidx = 0;
for (var y = 0; y < mazeRows; y++) {
for (var x = 0; x < mazeCols; x++) {
if (mazeData[y][x] === 5 && gidx < ghostCount) {
var ghost = new Ghost();
ghost.x = x * tileSize + tileSize / 2 + mazeOffsetX;
ghost.y = y * tileSize + tileSize / 2 + mazeOffsetY;
ghost.setColor(ghostColors[gidx]);
ghost.setType(ghostTypes[gidx]);
ghost.speed = getGhostSpeed(); // Set speed based on difficulty
ghost.dir = {
x: 0,
y: -1
};
game.addChild(ghost);
ghosts.push(ghost);
gidx++;
}
}
}
}
// --- Helper: update GUI ---
function updateGUI() {
scoreTxt.setText(score);
var lstr = '';
for (var i = 0; i < lives; i++) lstr += '♥';
livesTxt.setText(lstr);
}
// --- Helper: start level ---
function startLevel() {
resetMaze();
resetActors();
updateGUI();
gameActive = true;
}
// --- Helper: lose life ---
function loseLife() {
lives--;
updateGUI();
// Play loseLife sound if available, otherwise fallback to fail or error
var loseLifeSound = LK.getSound('loseLife');
if (loseLifeSound && typeof loseLifeSound.play === "function") {
loseLifeSound.play();
} else {
var failSound = LK.getSound('fail');
if (failSound && typeof failSound.play === "function") {
failSound.play();
} else {
var errorSound = LK.getSound('error');
if (errorSound && typeof errorSound.play === "function") errorSound.play();
}
}
if (lives <= 0) {
gameActive = false;
LK.effects.flashScreen(0xff0000, 1000);
LK.showGameOver();
return;
}
// Reset player/ghosts only
resetActors();
}
// --- Helper: next level ---
function nextLevel() {
level++;
// Increase ghost speed a bit, but keep within difficulty bounds
for (var i = 0; i < ghosts.length; i++) {
ghosts[i].speed = getGhostSpeed() + (level - 1) * 0.5; // Slightly increase per level, but base on difficulty
}
startLevel();
}
// --- Helper: frighten ghosts ---
function frightenGhosts() {
for (var i = 0; i < ghosts.length; i++) {
ghosts[i].setMode('frightened');
ghosts[i].frightenedTicks = frightenedTicksMax;
}
}
// --- Helper: check collision (circle) ---
function circlesCollide(a, b) {
var dx = a.x - b.x,
dy = a.y - b.y;
var r = a.radius + b.radius;
return dx * dx + dy * dy < r * r;
}
// --- Input Handling (swipe) ---
var touchStart = null;
game.down = function (x, y, obj) {
touchStart = {
x: x,
y: y
};
};
game.up = function (x, y, obj) {
if (!touchStart) return;
var dx = x - touchStart.x,
dy = y - touchStart.y;
if (Math.abs(dx) > Math.abs(dy)) {
if (dx > 30) player.setDir(1, 0);else if (dx < -30) player.setDir(-1, 0);
} else {
if (dy > 30) player.setDir(0, 1);else if (dy < -30) player.setDir(0, -1);
}
touchStart = null;
};
// --- Main Update Loop ---
// --- Ghost Pathfinding Timer ---
// Ghosts will check the player's position and update their path every 0.5 seconds (30 ticks)
var ghostPathTimer = 0;
game.update = function () {
if (!gameActive) return;
// Update player
if (player) player.update();
// --- Ghost Pathfinding: update every 0.5s (30 ticks) ---
ghostPathTimer++;
if (ghostPathTimer >= 30) {
ghostPathTimer = 0;
for (var gi = 0; gi < ghosts.length; gi++) {
var ghost = ghosts[gi];
// Only chase if not frightened
if (ghost.mode !== 'frightened') {
// Find shortest path to player using BFS
var startTile = ghost.getTile();
var targetTile = player.getTile();
var queue = [];
var visited = [];
for (var y = 0; y < mazeRows; y++) {
visited[y] = [];
for (var x = 0; x < mazeCols; x++) {
visited[y][x] = false;
}
}
queue.push({
x: startTile.x,
y: startTile.y,
path: []
});
visited[startTile.y][startTile.x] = true;
var found = false;
var path = [];
var dirs = [{
x: 0,
y: -1
},
// up
{
x: 1,
y: 0
},
// right
{
x: 0,
y: 1
},
// down
{
x: -1,
y: 0
} // left
];
while (queue.length > 0 && !found) {
var node = queue.shift();
if (node.x === targetTile.x && node.y === targetTile.y) {
path = node.path;
found = true;
break;
}
for (var d = 0; d < dirs.length; d++) {
var nx = node.x + dirs[d].x;
var ny = node.y + dirs[d].y;
// Wrap horizontally (tunnel)
if (nx < 0) nx = mazeCols - 1;
if (nx >= mazeCols) nx = 0;
if (ny < 0 || ny >= mazeRows) continue;
if (mazeData[ny][nx] === 1) continue; // wall
if (visited[ny][nx]) continue;
visited[ny][nx] = true;
queue.push({
x: nx,
y: ny,
path: node.path.concat([{
x: nx,
y: ny
}])
});
}
}
// If found, set direction toward first step in path
if (found && path.length > 0) {
var nextStep = path[0];
var dx = nextStep.x - startTile.x;
var dy = nextStep.y - startTile.y;
// Handle tunnel wrap
if (dx > 1) dx = -1;
if (dx < -1) dx = 1;
ghost.dir = {
x: dx,
y: dy
};
} else {
// If blocked, pick a random open direction
var curTile = ghost.getTile();
var options = [];
for (var d = 0; d < dirs.length; d++) {
var nx = curTile.x + dirs[d].x;
var ny = curTile.y + dirs[d].y;
if (nx < 0) nx = mazeCols - 1;
if (nx >= mazeCols) nx = 0;
if (ny < 0 || ny >= mazeRows) continue;
if (mazeData[ny][nx] === 1) continue;
options.push(dirs[d]);
}
if (options.length > 0) {
var idx = Math.floor(Math.random() * options.length);
ghost.dir = {
x: options[idx].x,
y: options[idx].y
};
}
}
}
}
}
// Update ghosts
for (var i = 0; i < ghosts.length; i++) ghosts[i].update();
// --- Collisions: Player <-> Dots ---
for (var i = dots.length - 1; i >= 0; i--) {
if (circlesCollide(player, dots[i])) {
score += 10;
dots[i].destroy();
dots.splice(i, 1);
updateGUI();
// Play dotBeep if available, otherwise play beep
var dotBeepSound = LK.getSound('dotBeep');
if (dotBeepSound && typeof dotBeepSound.play === "function") {
dotBeepSound.play();
} else {
var beepSound = LK.getSound('beep');
if (beepSound && typeof beepSound.play === "function") beepSound.play();
}
}
}
// --- Collisions: Player <-> PowerDots ---
// Find the blue powerDot (assume only one, at most)
var bluePowerDot = null;
for (var i = 0; i < powerDots.length; i++) {
// The blue powerDot is the one with color 0x00ffff (from asset definition)
// We can check by checking the attached asset's tint or just assume the first is blue if only one
// For robustness, check radius (blue is 40, others could be different)
if (powerDots[i].radius === 20) {
// 40/2 = 20
bluePowerDot = powerDots[i];
break;
}
}
// Find the red powerDot (assume only one, at most)
var redPowerDot = null;
for (var i = 0; i < powerDots.length; i++) {
if (powerDots[i] !== bluePowerDot) {
redPowerDot = powerDots[i];
break;
}
}
// Now handle collisions for all powerDots
for (var i = powerDots.length - 1; i >= 0; i--) {
if (circlesCollide(player, powerDots[i])) {
score += 50;
// Store respawn position before destroying
var respawnX = powerDots[i].x;
var respawnY = powerDots[i].y;
powerDots[i].destroy();
powerDots.splice(i, 1);
updateGUI();
frightenGhosts();
// Play dotBeep if available, otherwise play beep
var dotBeepSound = LK.getSound('dotBeep');
if (dotBeepSound && typeof dotBeepSound.play === "function") {
dotBeepSound.play();
} else {
var beepSound = LK.getSound('beep');
if (beepSound && typeof beepSound.play === "function") beepSound.play();
}
// Respawn powerDot at same location after 10 seconds (600 ticks)
(function (x, y) {
LK.setTimeout(function () {
// Only respawn if the game is still active and not already a powerDot at this location
if (gameActive) {
// Check if a powerDot already exists at this location (avoid duplicates)
var exists = false;
for (var j = 0; j < powerDots.length; j++) {
if (Math.abs(powerDots[j].x - x) < 1 && Math.abs(powerDots[j].y - y) < 1) {
exists = true;
break;
}
}
if (!exists) {
var pd = new PowerDot();
pd.x = x;
pd.y = y;
game.addChild(pd);
powerDots.push(pd);
}
}
}, 10000); // 10 seconds
})(respawnX, respawnY);
}
}
// --- Collisions: Player <-> Ghosts ---
for (var i = 0; i < ghosts.length; i++) {
if (circlesCollide(player, ghosts[i])) {
if (ghosts[i].mode === 'frightened') {
score += 200;
ghosts[i].setMode('chase');
ghosts[i].moveToTile({
x: 9,
y: 6
}); // send to ghost start
updateGUI();
} else {
loseLife();
return;
}
}
}
// --- Win Condition: all dots eaten ---
if (dots.length === 0 && powerDots.length === 0) {
gameActive = false;
LK.effects.flashScreen(0x00ff00, 1000);
LK.showYouWin();
// Change mazeData for new level
if (level === 1) {
// Level 2: new maze layout, more open and with more power dots
mazeData = [[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1], [1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1], [1, 2, 1, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 1, 2, 1], [1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1], [1, 2, 1, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 1, 2, 1], [1, 2, 1, 2, 1, 2, 1, 1, 1, 5, 1, 1, 1, 2, 1, 2, 1, 2, 1], [1, 2, 1, 2, 1, 2, 1, 2, 2, 2, 2, 2, 1, 2, 1, 2, 1, 2, 1], [1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1], [1, 2, 2, 2, 1, 2, 2, 2, 1, 4, 1, 2, 2, 2, 1, 2, 2, 2, 1], [1, 1, 1, 2, 1, 1, 1, 2, 1, 1, 1, 2, 1, 1, 1, 2, 1, 1, 1], [1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1], [1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1], [1, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 1], [1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1], [1, 2, 1, 2, 1, 2, 2, 2, 2, 2, 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, 1, 2, 1, 2, 1, 2, 2, 2, 2, 2, 1, 2, 1, 2, 1, 2, 1], [1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1], [1, 3, 2, 2, 2, 2, 2, 2, 2, 3, 2, 2, 2, 2, 2, 2, 2, 3, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]];
mazeRows = mazeData.length;
mazeCols = mazeData[0].length;
mazeWidth = mazeCols * tileSize;
mazeHeight = mazeRows * tileSize;
mazeOffsetX = Math.floor((2048 - mazeWidth) / 2);
mazeOffsetY = Math.floor((2732 - mazeHeight) / 2);
}
nextLevel();
}
};
// --- Difficulty Selection Menu ---
var menuContainer = new Container();
game.addChild(menuContainer);
// Center of screen
var centerX = 2048 / 2;
var centerY = 2732 / 2;
// Button style
function makeButton(label, y, selected) {
var btn = new Container();
var bg = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 4,
scaleY: 2.2,
tint: selected ? 0x44ff44 : 0x222222
});
btn.addChild(bg);
var txt = new Text2(label, {
size: 120,
fill: selected ? "#222" : "#fff"
});
txt.anchor.set(0.5, 0.5);
btn.addChild(txt);
btn.x = centerX;
btn.y = y;
btn.bg = bg;
btn.txt = txt;
return btn;
}
// Track selected difficulty
var menuDifficulty = "Normal";
var easyBtn = makeButton("Easy", centerY - 220, false);
var normalBtn = makeButton("⚪ Normal", centerY, true);
var hardBtn = makeButton("Hard", centerY + 220, false);
menuContainer.addChild(easyBtn);
menuContainer.addChild(normalBtn);
menuContainer.addChild(hardBtn);
var startBtn = null;
// Button tap logic
function updateMenuButtons() {
easyBtn.bg.tint = menuDifficulty === "Easy" ? 0x44ff44 : 0x222222;
if (typeof easyBtn.txt.setFill === "function") {
easyBtn.txt.setFill(menuDifficulty === "Easy" ? "#222" : "#fff");
}
normalBtn.bg.tint = menuDifficulty === "Normal" ? 0x44ff44 : 0x222222;
if (typeof normalBtn.txt.setFill === "function") {
normalBtn.txt.setFill(menuDifficulty === "Normal" ? "#222" : "#fff");
}
hardBtn.bg.tint = menuDifficulty === "Hard" ? 0x44ff44 : 0x222222;
if (typeof hardBtn.txt.setFill === "function") {
hardBtn.txt.setFill(menuDifficulty === "Hard" ? "#222" : "#fff");
}
}
easyBtn.down = function (x, y, obj) {
menuDifficulty = "Easy";
updateMenuButtons();
if (!startBtn) showStartBtn();
};
normalBtn.down = function (x, y, obj) {
menuDifficulty = "Normal";
updateMenuButtons();
if (!startBtn) showStartBtn();
};
hardBtn.down = function (x, y, obj) {
menuDifficulty = "Hard";
updateMenuButtons();
if (!startBtn) showStartBtn();
};
function showStartBtn() {
startBtn = makeButton("Start Game", centerY + 500, false);
startBtn.bg.tint = 0x0080ff;
if (typeof startBtn.txt.setFill === "function") {
startBtn.txt.setFill("#fff");
}
menuContainer.addChild(startBtn);
startBtn.down = function (x, y, obj) {
// Set global difficulty and enemy speed
if (menuDifficulty === "Easy") {
difficulty = "Easy";
} else if (menuDifficulty === "Hard") {
difficulty = "Hard";
} else {
difficulty = "Normal";
}
// Set music per difficulty
if (difficulty === "Easy") {
LK.playMusic('calmTrack', {
loop: true
});
} else if (difficulty === "Hard") {
LK.playMusic('intenseTrack', {
loop: true
});
} else {
LK.playMusic('normalTrack', {
loop: true
});
}
// Hide menu
menuContainer.visible = false;
// Update difficulty label
diffTxt.setText('Difficulty: ' + difficulty);
// Start game
startLevel();
};
}
// Only show menu at start
gameActive = false;
// Hide GUI elements until game starts
scoreTxt.visible = false;
livesTxt.visible = false;
diffTxt.visible = false;
// Show GUI when game starts
var oldStartLevel = startLevel;
startLevel = function startLevel() {
scoreTxt.visible = true;
livesTxt.visible = true;
diffTxt.visible = true;
oldStartLevel();
};
// --- Settings Popup for In-Game Difficulty Change ---
var settingsPopup = new Container();
settingsPopup.visible = false;
game.addChild(settingsPopup);
// Dim background
var dimBg = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 30,
scaleY: 40,
tint: 0x000000
});
dimBg.alpha = 0.45;
dimBg.x = centerX;
dimBg.y = centerY;
settingsPopup.addChild(dimBg);
// Popup box
var popupBox = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 7,
scaleY: 7,
tint: 0x222222
});
popupBox.x = centerX;
popupBox.y = centerY;
settingsPopup.addChild(popupBox);
// Popup label
var popupLabel = new Text2("Change Difficulty", {
size: 90,
fill: "#fff"
});
popupLabel.anchor.set(0.5, 0.5);
popupLabel.x = centerX;
popupLabel.y = centerY - 260;
settingsPopup.addChild(popupLabel);
// Popup buttons
function makePopupBtn(label, y, selected) {
var btn = new Container();
var bg = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 4,
scaleY: 2.2,
tint: selected ? 0x44ff44 : 0x222222
});
btn.addChild(bg);
var txt = new Text2(label, {
size: 120,
fill: selected ? "#222" : "#fff"
});
txt.anchor.set(0.5, 0.5);
btn.addChild(txt);
btn.x = centerX;
btn.y = y;
btn.bg = bg;
btn.txt = txt;
return btn;
}
var popupEasyBtn = makePopupBtn("Easy", centerY - 100, false);
var popupNormalBtn = makePopupBtn("⚪ Normal", centerY + 60, true);
var popupHardBtn = makePopupBtn("Hard", centerY + 220, false);
settingsPopup.addChild(popupEasyBtn);
settingsPopup.addChild(popupNormalBtn);
settingsPopup.addChild(popupHardBtn);
// Helper to update popup button visuals
function updatePopupBtns() {
popupEasyBtn.bg.tint = difficulty === "Easy" ? 0x44ff44 : 0x222222;
if (typeof popupEasyBtn.txt.setFill === "function") {
popupEasyBtn.txt.setFill(difficulty === "Easy" ? "#222" : "#fff");
}
popupNormalBtn.bg.tint = difficulty === "Normal" ? 0x44ff44 : 0x222222;
if (typeof popupNormalBtn.txt.setFill === "function") {
popupNormalBtn.txt.setFill(difficulty === "Normal" ? "#222" : "#fff");
}
popupHardBtn.bg.tint = difficulty === "Hard" ? 0x44ff44 : 0x222222;
if (typeof popupHardBtn.txt.setFill === "function") {
popupHardBtn.txt.setFill(difficulty === "Hard" ? "#222" : "#fff");
}
}
// Popup logic: change difficulty and apply instantly (no pause/resume)
function popupSetDifficulty(newDiff) {
if (difficulty === newDiff) return;
difficulty = newDiff;
diffTxt.setText('Difficulty: ' + difficulty);
// Update ghost speed immediately
for (var i = 0; i < ghosts.length; i++) {
ghosts[i].speed = getGhostSpeed();
}
// Change music if needed
if (difficulty === "Easy") {
LK.playMusic('calmTrack', {
loop: true
});
} else if (difficulty === "Hard") {
LK.playMusic('intenseTrack', {
loop: true
});
} else {
LK.playMusic('normalTrack', {
loop: true
});
}
updatePopupBtns();
settingsPopup.visible = false;
}
// Button handlers
popupEasyBtn.down = function () {
popupSetDifficulty("Easy");
};
popupNormalBtn.down = function () {
popupSetDifficulty("Normal");
};
popupHardBtn.down = function () {
popupSetDifficulty("Hard");
};
// Settings button handler
settingsBtn.down = function () {
if (!gameActive) return;
settingsPopup.visible = true;
updatePopupBtns();
};
// Allow closing popup by tapping dim background (does not change difficulty)
dimBg.down = function () {
settingsPopup.visible = false;
}; /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// --- Dot (Pellet) ---
var Dot = Container.expand(function () {
var self = Container.call(this);
var dotAsset = self.attachAsset('dot', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = dotAsset.width / 2;
return self;
});
// --- Ghost ---
var Ghost = Container.expand(function () {
var self = Container.call(this);
// Attach ghost asset (color set later)
var ghostAsset = self.attachAsset('ghostBody', {
anchorX: 0.5,
anchorY: 0.5
});
// Ghost color
self.color = 0xffff00;
ghostAsset.tint = self.color;
// Movement speed
self.speed = 6.4; // 20% slower than previous (was 8)
// Direction
self.dir = {
x: 0,
y: 0
};
// Scatter/Chase mode
self.mode = 'chase'; // or 'scatter' or 'frightened'
self.frightenedTicks = 0;
// For collision
self.radius = ghostAsset.width / 2;
// Ghost AI type: 'blinky', 'pinky', 'inky', 'clyde'
self.ghostType = 'blinky';
// Set color
self.setColor = function (color) {
self.color = color;
ghostAsset.tint = color;
};
// Set type
self.setType = function (type) {
self.ghostType = type;
};
// Set mode
self.setMode = function (mode) {
self.mode = mode;
if (mode === 'frightened') {
ghostAsset.tint = 0x2222ff;
} else {
ghostAsset.tint = self.color;
}
};
// Move to tile
self.moveToTile = function (tile) {
self.x = tile.x * tileSize + tileSize / 2 + mazeOffsetX;
self.y = tile.y * tileSize + tileSize / 2 + mazeOffsetY;
};
// Get current tile
self.getTile = function () {
return {
x: Math.floor((self.x - mazeOffsetX) / tileSize),
y: Math.floor((self.y - mazeOffsetY) / tileSize)
};
};
// AI: get target tile
self.getTargetTile = function () {
// If player exists, target the player's current tile (chase mode)
if (typeof player !== "undefined" && player && typeof player.getTile === "function") {
var targetTile = player.getTile();
return {
x: targetTile.x,
y: targetTile.y
};
}
// Fallback: random movement as before
var curTile = self.getTile();
var dirs = [{
x: 0,
y: -1
},
// up
{
x: 1,
y: 0
},
// right
{
x: 0,
y: 1
},
// down
{
x: -1,
y: 0
} // left
];
// Try to keep going in the same direction if possible
if (canMove(curTile.x, curTile.y, self.dir.x, self.dir.y)) {
return {
x: curTile.x + self.dir.x,
y: curTile.y + self.dir.y
};
}
// Otherwise, pick a random valid direction
var options = [];
for (var i = 0; i < dirs.length; i++) {
var d = dirs[i];
if (canMove(curTile.x, curTile.y, d.x, d.y)) {
options.push(d);
}
}
if (options.length > 0) {
var idx = Math.floor(Math.random() * options.length);
var d = options[idx];
return {
x: curTile.x + d.x,
y: curTile.y + d.y
};
}
// If stuck, stay in place
return curTile;
};
// AI: choose direction
self.chooseDir = function () {
var curTile = self.getTile();
var target = self.getTargetTile();
// All possible directions
var dirs = [{
x: 0,
y: -1
},
// up
{
x: 1,
y: 0
},
// right
{
x: 0,
y: 1
},
// down
{
x: -1,
y: 0
} // left
];
// Don't reverse
var opp = {
x: -self.dir.x,
y: -self.dir.y
};
// For frightened, pick random
if (self.mode === 'frightened') {
var options = [];
for (var i = 0; i < dirs.length; i++) {
var d = dirs[i];
if ((d.x !== opp.x || d.y !== opp.y) && canMove(curTile.x, curTile.y, d.x, d.y)) {
options.push(d);
}
}
if (options.length > 0) {
var idx = Math.floor(Math.random() * options.length);
return options[idx];
}
return self.dir;
}
// Otherwise, pick direction that minimizes distance to target
var minDist = 99999,
bestDir = self.dir;
for (var i = 0; i < dirs.length; i++) {
var d = dirs[i];
if ((d.x !== opp.x || d.y !== opp.y) && canMove(curTile.x, curTile.y, d.x, d.y)) {
var nx = curTile.x + d.x,
ny = curTile.y + d.y;
var dist = (target.x - nx) * (target.x - nx) + (target.y - ny) * (target.y - ny);
if (dist < minDist) {
minDist = dist;
bestDir = d;
}
}
}
return bestDir;
};
// Update per tick
self.update = function () {
// If in tunnel, wrap
if (self.x < mazeOffsetX - tileSize / 2) {
self.x = mazeOffsetX + (mazeCols - 1) * tileSize + tileSize / 2;
}
if (self.x > mazeOffsetX + (mazeCols - 1) * tileSize + tileSize / 2) {
self.x = mazeOffsetX - tileSize / 2;
}
// Frightened mode timer
if (self.mode === 'frightened') {
self.frightenedTicks--;
if (self.frightenedTicks <= 0) {
self.setMode('chase');
}
}
// At center of tile, choose new dir
var curTile = self.getTile();
var cx = curTile.x * tileSize + tileSize / 2 + mazeOffsetX;
var cy = curTile.y * tileSize + tileSize / 2 + mazeOffsetY;
var dist = Math.abs(self.x - cx) + Math.abs(self.y - cy);
if (dist < 2) {
// Always choose direction based on AI (chase, scatter, frightened)
var newDir = self.chooseDir();
self.dir = newDir;
// Snap to center
self.x = cx;
self.y = cy;
}
// Move if possible
if (canMove(curTile.x, curTile.y, self.dir.x, self.dir.y)) {
self.x += self.dir.x * self.speed;
self.y += self.dir.y * self.speed;
}
};
return self;
});
// --- Player (Pac) ---
var Player = Container.expand(function () {
var self = Container.call(this);
// Attach yellow circle asset for player
var playerAsset = self.attachAsset('playerCircle', {
anchorX: 0.5,
anchorY: 0.5
});
// Movement speed (pixels per tick)
self.speed = 10;
// Current direction: {x: -1, y:0} etc.
self.dir = {
x: 0,
y: 0
};
self.nextDir = {
x: 0,
y: 0
};
// For smooth movement
self.targetTile = null;
// For mouth animation
self.mouthOpen = true;
self.mouthTween = null;
// For collision
self.radius = playerAsset.width / 2;
// Animate mouth (simple scaleX)
function animateMouth() {
if (self.mouthTween) tween.stop(playerAsset, {
scaleX: true
});
playerAsset.scaleX = 1;
self.mouthTween = tween(playerAsset, {
scaleX: 0.7
}, {
duration: 120,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(playerAsset, {
scaleX: 1
}, {
duration: 120,
easing: tween.easeInOut,
onFinish: animateMouth
});
}
});
}
animateMouth();
// Set direction
self.setDir = function (dx, dy) {
self.nextDir = {
x: dx,
y: dy
};
};
// Move to tile
self.moveToTile = function (tile) {
self.x = tile.x * tileSize + tileSize / 2 + mazeOffsetX;
self.y = tile.y * tileSize + tileSize / 2 + mazeOffsetY;
};
// Get current tile
self.getTile = function () {
return {
x: Math.floor((self.x - mazeOffsetX) / tileSize),
y: Math.floor((self.y - mazeOffsetY) / tileSize)
};
};
// Update per tick
self.update = function () {
// If in tunnel, wrap
if (self.x < mazeOffsetX - tileSize / 2) {
self.x = mazeOffsetX + (mazeCols - 1) * tileSize + tileSize / 2;
}
if (self.x > mazeOffsetX + (mazeCols - 1) * tileSize + tileSize / 2) {
self.x = mazeOffsetX - tileSize / 2;
}
// Try to turn if possible
var curTile = self.getTile();
if (canMove(curTile.x, curTile.y, self.nextDir.x, self.nextDir.y)) {
self.dir = {
x: self.nextDir.x,
y: self.nextDir.y
};
}
// Move if possible
if (canMove(curTile.x, curTile.y, self.dir.x, self.dir.y)) {
self.x += self.dir.x * self.speed;
self.y += self.dir.y * self.speed;
} else {
// Snap to center of tile
self.moveToTile(curTile);
}
};
return self;
});
// --- PowerDot (Power Pellet) ---
var PowerDot = Container.expand(function () {
var self = Container.call(this);
var pdAsset = self.attachAsset('powerDot', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = pdAsset.width / 2;
return self;
});
// --- Wall ---
var Wall = Container.expand(function () {
var self = Container.call(this);
var wallAsset = self.attachAsset('wallBlock', {
anchorX: 0.5,
anchorY: 0.5
});
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x000000
});
/****
* Game Code
****/
// Example: A new, more open map with a different wall and dot pattern
// --- Maze Layout ---
// 0: empty, 1: wall, 2: dot, 3: power dot, 4: player start, 5: ghost start, 6: tunnel
// Simple 19x21 maze (Pac-Man like, but smaller for MVP)
var mazeData = [[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1], [1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1], [1, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 1], [1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1], [1, 2, 1, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 1, 2, 1], [1, 2, 1, 2, 1, 2, 1, 1, 1, 5, 1, 1, 1, 2, 1, 2, 1, 2, 1], [1, 2, 1, 2, 1, 2, 1, 2, 2, 2, 2, 2, 1, 2, 1, 2, 1, 2, 1], [1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1], [1, 2, 2, 2, 1, 2, 2, 2, 1, 4, 1, 2, 2, 2, 1, 2, 2, 2, 1], [1, 1, 1, 2, 1, 1, 1, 2, 1, 1, 1, 2, 1, 1, 1, 2, 1, 1, 1], [1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1], [1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1], [1, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 1], [1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1], [1, 2, 1, 2, 1, 2, 2, 2, 2, 2, 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, 1, 2, 1, 2, 1, 2, 2, 2, 2, 2, 1, 2, 1, 2, 1, 2, 1], [1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1], [1, 2, 2, 2, 2, 2, 2, 2, 2, 3, 2, 2, 2, 2, 2, 2, 2, 2, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]];
var mazeRows = mazeData.length;
var mazeCols = mazeData[0].length;
// --- Maze Rendering ---
var tileSize = 90;
var mazeWidth = mazeCols * tileSize;
var mazeHeight = mazeRows * tileSize;
var mazeOffsetX = Math.floor((2048 - mazeWidth) / 2);
var mazeOffsetY = Math.floor((2732 - mazeHeight) / 2);
// --- Asset Initialization ---
// --- Game State ---
var player = null;
var ghosts = [];
var dots = [];
var powerDots = [];
var walls = [];
var lives = 3;
var level = 1;
var score = 0;
var gameActive = true;
var frightenedTicksMax = 360; // 6 seconds at 60fps
// --- Difficulty Levels ---
var difficulty = "Normal"; // Options: "Easy", "Normal", "Hard"
// You can change this value to "Easy" or "Hard" to test
function getGhostSpeed() {
if (difficulty === "Easy") return 5.2; // 35% slower than original
if (difficulty === "Hard") return 8.8; // 10% faster than original
return 6.4; // Normal (20% slower than original)
}
// --- GUI ---
var scoreTxt = new Text2('0', {
size: 100,
fill: "#fff"
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
var livesTxt = new Text2('♥♥♥', {
size: 80,
fill: 0xFF4444
});
livesTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(livesTxt);
livesTxt.y = 110;
// Show difficulty in GUI (optional)
var diffTxt = new Text2('Difficulty: ' + difficulty, {
size: 60,
fill: "#fff"
});
diffTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(diffTxt);
diffTxt.y = 200;
// --- Settings Button (⚙️) ---
var settingsBtn = new Container();
var settingsBg = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.1,
scaleY: 1.1,
tint: 0x222222
});
settingsBtn.addChild(settingsBg);
var settingsTxt = new Text2("⚙️", {
size: 80,
fill: "#fff"
});
settingsTxt.anchor.set(0.5, 0.5);
settingsBtn.addChild(settingsTxt);
// Place in top-right, with margin
settingsBtn.x = 2048 - 100;
settingsBtn.y = 100;
LK.gui.topRight.addChild(settingsBtn);
settingsBtn.zIndex = 1000; // ensure on top
// --- Helper: canMove ---
function canMove(x, y, dx, dy) {
var nx = x + dx,
ny = y + dy;
if (nx < 0) nx = mazeCols - 1;
if (nx >= mazeCols) nx = 0;
if (ny < 0 || ny >= mazeRows) return false;
if (mazeData[ny][nx] === 1) return false;
return true;
}
// --- Helper: reset maze ---
function resetMaze() {
// Remove old
for (var i = 0; i < dots.length; i++) dots[i].destroy();
for (var i = 0; i < powerDots.length; i++) powerDots[i].destroy();
for (var i = 0; i < walls.length; i++) walls[i].destroy();
dots = [];
powerDots = [];
walls = [];
// Place maze
for (var y = 0; y < mazeRows; y++) {
for (var x = 0; x < mazeCols; x++) {
var v = mazeData[y][x];
var px = x * tileSize + tileSize / 2 + mazeOffsetX;
var py = y * tileSize + tileSize / 2 + mazeOffsetY;
if (v === 1) {
var wall = new Wall();
wall.x = px;
wall.y = py;
game.addChild(wall);
walls.push(wall);
} else if (v === 2) {
var dot = new Dot();
dot.x = px;
dot.y = py;
game.addChild(dot);
dots.push(dot);
} else if (v === 3) {
var pd = new PowerDot();
pd.x = px;
pd.y = py;
game.addChild(pd);
powerDots.push(pd);
}
}
}
}
// --- Helper: reset player and ghosts ---
function resetActors() {
// Remove old
if (player) player.destroy();
for (var i = 0; i < ghosts.length; i++) ghosts[i].destroy();
ghosts = [];
// Place player
// Start player at the first open black area (not a wall) from the top, scanning left to right
var foundStart = false;
for (var y = 0; y < mazeRows && !foundStart; y++) {
for (var x = 0; x < mazeCols && !foundStart; x++) {
if (mazeData[y][x] !== 1) {
// not a wall
player = new Player();
player.x = x * tileSize + tileSize / 2 + mazeOffsetX;
player.y = y * tileSize + tileSize / 2 + mazeOffsetY;
game.addChild(player);
foundStart = true;
}
}
}
// Fallback in case all are walls (should never happen)
if (!player) {
player = new Player();
player.x = 1 * tileSize + tileSize / 2 + mazeOffsetX;
player.y = 1 * tileSize + tileSize / 2 + mazeOffsetY;
game.addChild(player);
}
// Place ghosts
var ghostColors = [0xffff00, 0xffff00, 0xffff00, 0xffff00];
var ghostTypes = ['blinky', 'pinky', 'inky', 'clyde'];
var ghostCount = 4;
var gidx = 0;
for (var y = 0; y < mazeRows; y++) {
for (var x = 0; x < mazeCols; x++) {
if (mazeData[y][x] === 5 && gidx < ghostCount) {
var ghost = new Ghost();
ghost.x = x * tileSize + tileSize / 2 + mazeOffsetX;
ghost.y = y * tileSize + tileSize / 2 + mazeOffsetY;
ghost.setColor(ghostColors[gidx]);
ghost.setType(ghostTypes[gidx]);
ghost.speed = getGhostSpeed(); // Set speed based on difficulty
ghost.dir = {
x: 0,
y: -1
};
game.addChild(ghost);
ghosts.push(ghost);
gidx++;
}
}
}
}
// --- Helper: update GUI ---
function updateGUI() {
scoreTxt.setText(score);
var lstr = '';
for (var i = 0; i < lives; i++) lstr += '♥';
livesTxt.setText(lstr);
}
// --- Helper: start level ---
function startLevel() {
resetMaze();
resetActors();
updateGUI();
gameActive = true;
}
// --- Helper: lose life ---
function loseLife() {
lives--;
updateGUI();
// Play loseLife sound if available, otherwise fallback to fail or error
var loseLifeSound = LK.getSound('loseLife');
if (loseLifeSound && typeof loseLifeSound.play === "function") {
loseLifeSound.play();
} else {
var failSound = LK.getSound('fail');
if (failSound && typeof failSound.play === "function") {
failSound.play();
} else {
var errorSound = LK.getSound('error');
if (errorSound && typeof errorSound.play === "function") errorSound.play();
}
}
if (lives <= 0) {
gameActive = false;
LK.effects.flashScreen(0xff0000, 1000);
LK.showGameOver();
return;
}
// Reset player/ghosts only
resetActors();
}
// --- Helper: next level ---
function nextLevel() {
level++;
// Increase ghost speed a bit, but keep within difficulty bounds
for (var i = 0; i < ghosts.length; i++) {
ghosts[i].speed = getGhostSpeed() + (level - 1) * 0.5; // Slightly increase per level, but base on difficulty
}
startLevel();
}
// --- Helper: frighten ghosts ---
function frightenGhosts() {
for (var i = 0; i < ghosts.length; i++) {
ghosts[i].setMode('frightened');
ghosts[i].frightenedTicks = frightenedTicksMax;
}
}
// --- Helper: check collision (circle) ---
function circlesCollide(a, b) {
var dx = a.x - b.x,
dy = a.y - b.y;
var r = a.radius + b.radius;
return dx * dx + dy * dy < r * r;
}
// --- Input Handling (swipe) ---
var touchStart = null;
game.down = function (x, y, obj) {
touchStart = {
x: x,
y: y
};
};
game.up = function (x, y, obj) {
if (!touchStart) return;
var dx = x - touchStart.x,
dy = y - touchStart.y;
if (Math.abs(dx) > Math.abs(dy)) {
if (dx > 30) player.setDir(1, 0);else if (dx < -30) player.setDir(-1, 0);
} else {
if (dy > 30) player.setDir(0, 1);else if (dy < -30) player.setDir(0, -1);
}
touchStart = null;
};
// --- Main Update Loop ---
// --- Ghost Pathfinding Timer ---
// Ghosts will check the player's position and update their path every 0.5 seconds (30 ticks)
var ghostPathTimer = 0;
game.update = function () {
if (!gameActive) return;
// Update player
if (player) player.update();
// --- Ghost Pathfinding: update every 0.5s (30 ticks) ---
ghostPathTimer++;
if (ghostPathTimer >= 30) {
ghostPathTimer = 0;
for (var gi = 0; gi < ghosts.length; gi++) {
var ghost = ghosts[gi];
// Only chase if not frightened
if (ghost.mode !== 'frightened') {
// Find shortest path to player using BFS
var startTile = ghost.getTile();
var targetTile = player.getTile();
var queue = [];
var visited = [];
for (var y = 0; y < mazeRows; y++) {
visited[y] = [];
for (var x = 0; x < mazeCols; x++) {
visited[y][x] = false;
}
}
queue.push({
x: startTile.x,
y: startTile.y,
path: []
});
visited[startTile.y][startTile.x] = true;
var found = false;
var path = [];
var dirs = [{
x: 0,
y: -1
},
// up
{
x: 1,
y: 0
},
// right
{
x: 0,
y: 1
},
// down
{
x: -1,
y: 0
} // left
];
while (queue.length > 0 && !found) {
var node = queue.shift();
if (node.x === targetTile.x && node.y === targetTile.y) {
path = node.path;
found = true;
break;
}
for (var d = 0; d < dirs.length; d++) {
var nx = node.x + dirs[d].x;
var ny = node.y + dirs[d].y;
// Wrap horizontally (tunnel)
if (nx < 0) nx = mazeCols - 1;
if (nx >= mazeCols) nx = 0;
if (ny < 0 || ny >= mazeRows) continue;
if (mazeData[ny][nx] === 1) continue; // wall
if (visited[ny][nx]) continue;
visited[ny][nx] = true;
queue.push({
x: nx,
y: ny,
path: node.path.concat([{
x: nx,
y: ny
}])
});
}
}
// If found, set direction toward first step in path
if (found && path.length > 0) {
var nextStep = path[0];
var dx = nextStep.x - startTile.x;
var dy = nextStep.y - startTile.y;
// Handle tunnel wrap
if (dx > 1) dx = -1;
if (dx < -1) dx = 1;
ghost.dir = {
x: dx,
y: dy
};
} else {
// If blocked, pick a random open direction
var curTile = ghost.getTile();
var options = [];
for (var d = 0; d < dirs.length; d++) {
var nx = curTile.x + dirs[d].x;
var ny = curTile.y + dirs[d].y;
if (nx < 0) nx = mazeCols - 1;
if (nx >= mazeCols) nx = 0;
if (ny < 0 || ny >= mazeRows) continue;
if (mazeData[ny][nx] === 1) continue;
options.push(dirs[d]);
}
if (options.length > 0) {
var idx = Math.floor(Math.random() * options.length);
ghost.dir = {
x: options[idx].x,
y: options[idx].y
};
}
}
}
}
}
// Update ghosts
for (var i = 0; i < ghosts.length; i++) ghosts[i].update();
// --- Collisions: Player <-> Dots ---
for (var i = dots.length - 1; i >= 0; i--) {
if (circlesCollide(player, dots[i])) {
score += 10;
dots[i].destroy();
dots.splice(i, 1);
updateGUI();
// Play dotBeep if available, otherwise play beep
var dotBeepSound = LK.getSound('dotBeep');
if (dotBeepSound && typeof dotBeepSound.play === "function") {
dotBeepSound.play();
} else {
var beepSound = LK.getSound('beep');
if (beepSound && typeof beepSound.play === "function") beepSound.play();
}
}
}
// --- Collisions: Player <-> PowerDots ---
// Find the blue powerDot (assume only one, at most)
var bluePowerDot = null;
for (var i = 0; i < powerDots.length; i++) {
// The blue powerDot is the one with color 0x00ffff (from asset definition)
// We can check by checking the attached asset's tint or just assume the first is blue if only one
// For robustness, check radius (blue is 40, others could be different)
if (powerDots[i].radius === 20) {
// 40/2 = 20
bluePowerDot = powerDots[i];
break;
}
}
// Find the red powerDot (assume only one, at most)
var redPowerDot = null;
for (var i = 0; i < powerDots.length; i++) {
if (powerDots[i] !== bluePowerDot) {
redPowerDot = powerDots[i];
break;
}
}
// Now handle collisions for all powerDots
for (var i = powerDots.length - 1; i >= 0; i--) {
if (circlesCollide(player, powerDots[i])) {
score += 50;
// Store respawn position before destroying
var respawnX = powerDots[i].x;
var respawnY = powerDots[i].y;
powerDots[i].destroy();
powerDots.splice(i, 1);
updateGUI();
frightenGhosts();
// Play dotBeep if available, otherwise play beep
var dotBeepSound = LK.getSound('dotBeep');
if (dotBeepSound && typeof dotBeepSound.play === "function") {
dotBeepSound.play();
} else {
var beepSound = LK.getSound('beep');
if (beepSound && typeof beepSound.play === "function") beepSound.play();
}
// Respawn powerDot at same location after 10 seconds (600 ticks)
(function (x, y) {
LK.setTimeout(function () {
// Only respawn if the game is still active and not already a powerDot at this location
if (gameActive) {
// Check if a powerDot already exists at this location (avoid duplicates)
var exists = false;
for (var j = 0; j < powerDots.length; j++) {
if (Math.abs(powerDots[j].x - x) < 1 && Math.abs(powerDots[j].y - y) < 1) {
exists = true;
break;
}
}
if (!exists) {
var pd = new PowerDot();
pd.x = x;
pd.y = y;
game.addChild(pd);
powerDots.push(pd);
}
}
}, 10000); // 10 seconds
})(respawnX, respawnY);
}
}
// --- Collisions: Player <-> Ghosts ---
for (var i = 0; i < ghosts.length; i++) {
if (circlesCollide(player, ghosts[i])) {
if (ghosts[i].mode === 'frightened') {
score += 200;
ghosts[i].setMode('chase');
ghosts[i].moveToTile({
x: 9,
y: 6
}); // send to ghost start
updateGUI();
} else {
loseLife();
return;
}
}
}
// --- Win Condition: all dots eaten ---
if (dots.length === 0 && powerDots.length === 0) {
gameActive = false;
LK.effects.flashScreen(0x00ff00, 1000);
LK.showYouWin();
// Change mazeData for new level
if (level === 1) {
// Level 2: new maze layout, more open and with more power dots
mazeData = [[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1], [1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1], [1, 2, 1, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 1, 2, 1], [1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1], [1, 2, 1, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 1, 2, 1], [1, 2, 1, 2, 1, 2, 1, 1, 1, 5, 1, 1, 1, 2, 1, 2, 1, 2, 1], [1, 2, 1, 2, 1, 2, 1, 2, 2, 2, 2, 2, 1, 2, 1, 2, 1, 2, 1], [1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1], [1, 2, 2, 2, 1, 2, 2, 2, 1, 4, 1, 2, 2, 2, 1, 2, 2, 2, 1], [1, 1, 1, 2, 1, 1, 1, 2, 1, 1, 1, 2, 1, 1, 1, 2, 1, 1, 1], [1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1], [1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1], [1, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 1], [1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1], [1, 2, 1, 2, 1, 2, 2, 2, 2, 2, 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, 1, 2, 1, 2, 1, 2, 2, 2, 2, 2, 1, 2, 1, 2, 1, 2, 1], [1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1], [1, 3, 2, 2, 2, 2, 2, 2, 2, 3, 2, 2, 2, 2, 2, 2, 2, 3, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]];
mazeRows = mazeData.length;
mazeCols = mazeData[0].length;
mazeWidth = mazeCols * tileSize;
mazeHeight = mazeRows * tileSize;
mazeOffsetX = Math.floor((2048 - mazeWidth) / 2);
mazeOffsetY = Math.floor((2732 - mazeHeight) / 2);
}
nextLevel();
}
};
// --- Difficulty Selection Menu ---
var menuContainer = new Container();
game.addChild(menuContainer);
// Center of screen
var centerX = 2048 / 2;
var centerY = 2732 / 2;
// Button style
function makeButton(label, y, selected) {
var btn = new Container();
var bg = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 4,
scaleY: 2.2,
tint: selected ? 0x44ff44 : 0x222222
});
btn.addChild(bg);
var txt = new Text2(label, {
size: 120,
fill: selected ? "#222" : "#fff"
});
txt.anchor.set(0.5, 0.5);
btn.addChild(txt);
btn.x = centerX;
btn.y = y;
btn.bg = bg;
btn.txt = txt;
return btn;
}
// Track selected difficulty
var menuDifficulty = "Normal";
var easyBtn = makeButton("Easy", centerY - 220, false);
var normalBtn = makeButton("⚪ Normal", centerY, true);
var hardBtn = makeButton("Hard", centerY + 220, false);
menuContainer.addChild(easyBtn);
menuContainer.addChild(normalBtn);
menuContainer.addChild(hardBtn);
var startBtn = null;
// Button tap logic
function updateMenuButtons() {
easyBtn.bg.tint = menuDifficulty === "Easy" ? 0x44ff44 : 0x222222;
if (typeof easyBtn.txt.setFill === "function") {
easyBtn.txt.setFill(menuDifficulty === "Easy" ? "#222" : "#fff");
}
normalBtn.bg.tint = menuDifficulty === "Normal" ? 0x44ff44 : 0x222222;
if (typeof normalBtn.txt.setFill === "function") {
normalBtn.txt.setFill(menuDifficulty === "Normal" ? "#222" : "#fff");
}
hardBtn.bg.tint = menuDifficulty === "Hard" ? 0x44ff44 : 0x222222;
if (typeof hardBtn.txt.setFill === "function") {
hardBtn.txt.setFill(menuDifficulty === "Hard" ? "#222" : "#fff");
}
}
easyBtn.down = function (x, y, obj) {
menuDifficulty = "Easy";
updateMenuButtons();
if (!startBtn) showStartBtn();
};
normalBtn.down = function (x, y, obj) {
menuDifficulty = "Normal";
updateMenuButtons();
if (!startBtn) showStartBtn();
};
hardBtn.down = function (x, y, obj) {
menuDifficulty = "Hard";
updateMenuButtons();
if (!startBtn) showStartBtn();
};
function showStartBtn() {
startBtn = makeButton("Start Game", centerY + 500, false);
startBtn.bg.tint = 0x0080ff;
if (typeof startBtn.txt.setFill === "function") {
startBtn.txt.setFill("#fff");
}
menuContainer.addChild(startBtn);
startBtn.down = function (x, y, obj) {
// Set global difficulty and enemy speed
if (menuDifficulty === "Easy") {
difficulty = "Easy";
} else if (menuDifficulty === "Hard") {
difficulty = "Hard";
} else {
difficulty = "Normal";
}
// Set music per difficulty
if (difficulty === "Easy") {
LK.playMusic('calmTrack', {
loop: true
});
} else if (difficulty === "Hard") {
LK.playMusic('intenseTrack', {
loop: true
});
} else {
LK.playMusic('normalTrack', {
loop: true
});
}
// Hide menu
menuContainer.visible = false;
// Update difficulty label
diffTxt.setText('Difficulty: ' + difficulty);
// Start game
startLevel();
};
}
// Only show menu at start
gameActive = false;
// Hide GUI elements until game starts
scoreTxt.visible = false;
livesTxt.visible = false;
diffTxt.visible = false;
// Show GUI when game starts
var oldStartLevel = startLevel;
startLevel = function startLevel() {
scoreTxt.visible = true;
livesTxt.visible = true;
diffTxt.visible = true;
oldStartLevel();
};
// --- Settings Popup for In-Game Difficulty Change ---
var settingsPopup = new Container();
settingsPopup.visible = false;
game.addChild(settingsPopup);
// Dim background
var dimBg = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 30,
scaleY: 40,
tint: 0x000000
});
dimBg.alpha = 0.45;
dimBg.x = centerX;
dimBg.y = centerY;
settingsPopup.addChild(dimBg);
// Popup box
var popupBox = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 7,
scaleY: 7,
tint: 0x222222
});
popupBox.x = centerX;
popupBox.y = centerY;
settingsPopup.addChild(popupBox);
// Popup label
var popupLabel = new Text2("Change Difficulty", {
size: 90,
fill: "#fff"
});
popupLabel.anchor.set(0.5, 0.5);
popupLabel.x = centerX;
popupLabel.y = centerY - 260;
settingsPopup.addChild(popupLabel);
// Popup buttons
function makePopupBtn(label, y, selected) {
var btn = new Container();
var bg = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 4,
scaleY: 2.2,
tint: selected ? 0x44ff44 : 0x222222
});
btn.addChild(bg);
var txt = new Text2(label, {
size: 120,
fill: selected ? "#222" : "#fff"
});
txt.anchor.set(0.5, 0.5);
btn.addChild(txt);
btn.x = centerX;
btn.y = y;
btn.bg = bg;
btn.txt = txt;
return btn;
}
var popupEasyBtn = makePopupBtn("Easy", centerY - 100, false);
var popupNormalBtn = makePopupBtn("⚪ Normal", centerY + 60, true);
var popupHardBtn = makePopupBtn("Hard", centerY + 220, false);
settingsPopup.addChild(popupEasyBtn);
settingsPopup.addChild(popupNormalBtn);
settingsPopup.addChild(popupHardBtn);
// Helper to update popup button visuals
function updatePopupBtns() {
popupEasyBtn.bg.tint = difficulty === "Easy" ? 0x44ff44 : 0x222222;
if (typeof popupEasyBtn.txt.setFill === "function") {
popupEasyBtn.txt.setFill(difficulty === "Easy" ? "#222" : "#fff");
}
popupNormalBtn.bg.tint = difficulty === "Normal" ? 0x44ff44 : 0x222222;
if (typeof popupNormalBtn.txt.setFill === "function") {
popupNormalBtn.txt.setFill(difficulty === "Normal" ? "#222" : "#fff");
}
popupHardBtn.bg.tint = difficulty === "Hard" ? 0x44ff44 : 0x222222;
if (typeof popupHardBtn.txt.setFill === "function") {
popupHardBtn.txt.setFill(difficulty === "Hard" ? "#222" : "#fff");
}
}
// Popup logic: change difficulty and apply instantly (no pause/resume)
function popupSetDifficulty(newDiff) {
if (difficulty === newDiff) return;
difficulty = newDiff;
diffTxt.setText('Difficulty: ' + difficulty);
// Update ghost speed immediately
for (var i = 0; i < ghosts.length; i++) {
ghosts[i].speed = getGhostSpeed();
}
// Change music if needed
if (difficulty === "Easy") {
LK.playMusic('calmTrack', {
loop: true
});
} else if (difficulty === "Hard") {
LK.playMusic('intenseTrack', {
loop: true
});
} else {
LK.playMusic('normalTrack', {
loop: true
});
}
updatePopupBtns();
settingsPopup.visible = false;
}
// Button handlers
popupEasyBtn.down = function () {
popupSetDifficulty("Easy");
};
popupNormalBtn.down = function () {
popupSetDifficulty("Normal");
};
popupHardBtn.down = function () {
popupSetDifficulty("Hard");
};
// Settings button handler
settingsBtn.down = function () {
if (!gameActive) return;
settingsPopup.visible = true;
updatePopupBtns();
};
// Allow closing popup by tapping dim background (does not change difficulty)
dimBg.down = function () {
settingsPopup.visible = false;
};