User prompt
Kırmızı topu mavi topa yakın konuma koy
User prompt
Mavi olan yemi kırmızıya renkli yeme yakın tut
User prompt
Tüm yollar birbirine bağlı olmalı
User prompt
Haritayı yeniden yarat
User prompt
Haritayı değiştir
User prompt
Düşman karakter gibi hareket edebilmeli rastgele alanda gezmeli
User prompt
Düşman rastgele gezmeli
User prompt
Düşman takip etmeli
User prompt
Siyah alanın içinde başlasın
User prompt
Sol üstten başlasın karakterin konunu
Code edit (1 edits merged)
Please save this source code
User prompt
Maze Munch: Neon Chase
Initial prompt
Create a 2D top-down maze chase game inspired by classic Pac-Man. The player controls a yellow circular character that moves through a maze, collecting small dots to gain points. Four enemy ghosts roam the maze with unique AI patterns: one chases directly, one ambushes, one moves randomly, and one follows but stays distant. When the player collects a large power dot, all ghosts become vulnerable and can be eaten for extra points for a short time. The game ends if a ghost catches the player when not vulnerable. The map should have narrow corridors, walls, and corners, and should loop from left to right (teleport tunnels). Add a score counter, lives (3), and increasing difficulty after each level. Style: Classic arcade, retro neon visuals. Sound: 8-bit arcade sounds and background music. Controls: Arrow keys or swipe for mobile.
/****
* 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;
};