/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var Ball = Container.expand(function () {
var self = Container.call(this);
var ballShadow = self.attachAsset('ballShadow', {
anchorX: 0.5,
anchorY: 0.5
});
ballShadow.alpha = 0.5;
ballShadow.y = 8; // Offset shadow below ball for heavier appearance
var ballGraphics = self.attachAsset('ball', {
anchorX: 0.5,
anchorY: 0.5
});
self.velocityX = 0;
self.velocityY = 0;
self.gravity = 1.2; // Stronger gravity for realistic falling
self.airResistance = 0.995; // Air resistance instead of friction
self.bounceForce = 0.8; // More realistic bounce with gravity
self.maxSpeed = 20;
self.mass = 1.0; // Add mass for realistic physics
self.update = function () {
// Apply realistic gravity acceleration
self.velocityY += self.gravity;
// Apply air resistance to horizontal movement only
self.velocityX *= self.airResistance;
// No air resistance on vertical movement to allow proper falling
// Natural velocity dampening (no artificial cutoffs)
var currentSpeed = Math.sqrt(self.velocityX * self.velocityX + self.velocityY * self.velocityY);
// Limit max speed with realistic scaling
if (currentSpeed > self.maxSpeed) {
var scale = self.maxSpeed / currentSpeed;
self.velocityX *= scale;
self.velocityY *= scale;
}
// Store last position for impact detection
if (self.lastY === undefined) self.lastY = self.y;
var wasMovingDown = self.velocityY > 0;
// Apply velocity directly for natural gravity-based movement
self.x += self.velocityX;
self.y += self.velocityY;
// Ball remains rigid - no landing effects
self.lastY = self.y;
// Check maze boundaries and collisions
checkBallCollisions();
};
self.addForce = function (forceX, forceY) {
// Apply force based on mass (F = ma, so a = F/m)
var accelerationX = forceX / self.mass;
var accelerationY = forceY / self.mass;
// Add to velocity (realistic momentum)
self.velocityX += accelerationX;
self.velocityY += accelerationY;
// Stop any existing movement tweens to prevent conflicts
tween.stop(self, {
x: true,
y: true
});
};
return self;
});
var MazeCell = Container.expand(function (cellType) {
var self = Container.call(this);
self.cellType = cellType || 'wall';
var cellGraphics;
if (self.cellType === 'wall') {
cellGraphics = self.attachAsset('wall', {
anchorX: 0.5,
anchorY: 0.5
});
} else if (self.cellType === 'path') {
cellGraphics = self.attachAsset('path', {
anchorX: 0.5,
anchorY: 0.5
});
} else if (self.cellType === 'blackhole') {
cellGraphics = self.attachAsset('blackhole', {
anchorX: 0.5,
anchorY: 0.5
});
}
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x222222
});
/****
* Game Code
****/
// Add background
var background = game.addChild(LK.getAsset('background', {
x: 0,
y: 0,
width: 2048,
height: 2732
}));
var ball;
var maze = [];
var mazeWidth = 20;
var mazeHeight = 30;
var cellSize = 60;
var mazeContainer;
var blackholeX, blackholeY;
var timeLeft = 60;
var gameWon = false;
// UI Elements
var timerText = new Text2('60', {
size: 80,
fill: 0xFFFFFF
});
timerText.anchor.set(0.5, 0);
LK.gui.top.addChild(timerText);
var scoreText = new Text2('Level: 1', {
size: 60,
fill: 0xFFFFFF
});
scoreText.anchor.set(0, 0);
scoreText.x = 150;
scoreText.y = 20;
LK.gui.topLeft.addChild(scoreText);
// Initialize maze container
mazeContainer = game.addChild(new Container());
mazeContainer.x = (2048 - mazeWidth * cellSize) / 2;
mazeContainer.y = (2732 - mazeHeight * cellSize) / 2;
// Generate initial maze
function generateMaze() {
// Clear existing maze
for (var i = 0; i < maze.length; i++) {
for (var j = 0; j < maze[i].length; j++) {
if (maze[i][j]) {
maze[i][j].destroy();
}
}
}
maze = [];
// Initialize maze grid (all walls initially)
var mazeGrid = [];
for (var y = 0; y < mazeHeight; y++) {
mazeGrid[y] = [];
maze[y] = [];
for (var x = 0; x < mazeWidth; x++) {
// 0 = wall, 1 = path, 2 = visited
mazeGrid[y][x] = 0;
maze[y][x] = null;
}
}
// Recursive backtracking maze generation
var stack = [];
var currentX = 1;
var currentY = 1;
mazeGrid[currentY][currentX] = 1; // Mark starting cell as path
stack.push({
x: currentX,
y: currentY
});
while (stack.length > 0) {
var neighbors = [];
var directions = [{
x: 0,
y: -2
},
// North
{
x: 2,
y: 0
},
// East
{
x: 0,
y: 2
},
// South
{
x: -2,
y: 0
} // West
];
// Find unvisited neighbors
for (var i = 0; i < directions.length; i++) {
var nx = currentX + directions[i].x;
var ny = currentY + directions[i].y;
if (nx > 0 && nx < mazeWidth - 1 && ny > 0 && ny < mazeHeight - 1 && mazeGrid[ny][nx] === 0) {
neighbors.push({
x: nx,
y: ny,
dir: directions[i]
});
}
}
if (neighbors.length > 0) {
// Choose random neighbor
var randomNeighbor = neighbors[Math.floor(Math.random() * neighbors.length)];
var nextX = randomNeighbor.x;
var nextY = randomNeighbor.y;
var wallX = currentX + randomNeighbor.dir.x / 2;
var wallY = currentY + randomNeighbor.dir.y / 2;
// Remove wall between current and next cell
mazeGrid[wallY][wallX] = 1;
mazeGrid[nextY][nextX] = 1;
// Move to next cell
currentX = nextX;
currentY = nextY;
stack.push({
x: currentX,
y: currentY
});
} else {
// Backtrack
var prev = stack.pop();
if (stack.length > 0) {
currentX = stack[stack.length - 1].x;
currentY = stack[stack.length - 1].y;
}
}
}
// Add some random openings for better gameplay
for (var y = 2; y < mazeHeight - 2; y += 2) {
for (var x = 2; x < mazeWidth - 2; x += 2) {
if (Math.random() < 0.15) {
// Create random opening
var openingDirs = [{
x: 1,
y: 0
}, {
x: -1,
y: 0
}, {
x: 0,
y: 1
}, {
x: 0,
y: -1
}];
var randomDir = openingDirs[Math.floor(Math.random() * openingDirs.length)];
var openX = x + randomDir.x;
var openY = y + randomDir.y;
if (openX > 0 && openX < mazeWidth - 1 && openY > 0 && openY < mazeHeight - 1) {
mazeGrid[openY][openX] = 1;
}
}
}
}
// Create visual maze from grid
for (var y = 0; y < mazeHeight; y++) {
for (var x = 0; x < mazeWidth; x++) {
var cellType = mazeGrid[y][x] === 1 ? 'path' : 'wall';
var cell = new MazeCell(cellType);
cell.x = x * cellSize + cellSize / 2;
cell.y = y * cellSize + cellSize / 2;
maze[y][x] = cell;
mazeContainer.addChild(cell);
}
}
// Place black hole exit
placeBlackHole();
}
function placeBlackHole() {
// Find all path cells in the bottom areas of the maze
var pathCells = [];
// Start from bottom and work upward to find suitable path cells
// Ensure we stay within maze bounds
for (var y = Math.max(1, mazeHeight - 6); y <= Math.min(mazeHeight - 2, mazeHeight - 1); y++) {
for (var x = 1; x < mazeWidth - 1; x++) {
// Additional boundary check
if (x >= 0 && x < mazeWidth && y >= 0 && y < mazeHeight && maze[y] && maze[y][x] && maze[y][x].cellType === 'path') {
// Count how many neighboring cells are paths
var neighborCount = 0;
var directions = [{
x: 0,
y: -1
}, {
x: 1,
y: 0
}, {
x: 0,
y: 1
}, {
x: -1,
y: 0
}];
for (var i = 0; i < directions.length; i++) {
var nx = x + directions[i].x;
var ny = y + directions[i].y;
if (nx >= 0 && nx < mazeWidth && ny >= 0 && ny < mazeHeight && maze[ny] && maze[ny][nx] && maze[ny][nx].cellType === 'path') {
neighborCount++;
}
}
pathCells.push({
x: x,
y: y,
neighbors: neighborCount,
priority: (mazeHeight - y) * 10 + (4 - neighborCount) // Prefer bottom cells with fewer neighbors
});
}
}
}
// If no path cells found in bottom area, expand search to entire maze
if (pathCells.length === 0) {
for (var y = 1; y < mazeHeight - 1; y++) {
for (var x = 1; x < mazeWidth - 1; x++) {
// Additional boundary check
if (x >= 0 && x < mazeWidth && y >= 0 && y < mazeHeight && maze[y] && maze[y][x] && maze[y][x].cellType === 'path') {
pathCells.push({
x: x,
y: y,
neighbors: 1,
priority: 1
});
}
}
}
}
// If still no path cells found, create one by forcing a path within bounds
if (pathCells.length === 0) {
// Find middle of bottom row and force it to be a path, ensuring it's within bounds
var bottomY = Math.max(1, Math.min(mazeHeight - 2, mazeHeight - 2));
var middleX = Math.max(1, Math.min(mazeWidth - 2, Math.floor(mazeWidth / 2)));
if (maze[bottomY] && maze[bottomY][middleX]) {
maze[bottomY][middleX].destroy();
var pathCell = new MazeCell('path');
pathCell.x = middleX * cellSize + cellSize / 2;
pathCell.y = bottomY * cellSize + cellSize / 2;
maze[bottomY][middleX] = pathCell;
mazeContainer.addChild(pathCell);
pathCells.push({
x: middleX,
y: bottomY,
neighbors: 1,
priority: 100
});
}
}
if (pathCells.length > 0) {
// Choose the path cell with highest priority (bottom-most with fewest neighbors)
pathCells.sort(function (a, b) {
return b.priority - a.priority;
});
var bestCell = pathCells[0];
// Ensure black hole coordinates are within maze bounds
blackholeX = Math.max(0, Math.min(mazeWidth - 1, bestCell.x));
blackholeY = Math.max(0, Math.min(mazeHeight - 1, bestCell.y));
// Replace path with black hole, with additional boundary checks
if (blackholeX >= 0 && blackholeX < mazeWidth && blackholeY >= 0 && blackholeY < mazeHeight && maze[blackholeY] && maze[blackholeY][blackholeX]) {
maze[blackholeY][blackholeX].destroy();
var blackhole = new MazeCell('blackhole');
blackhole.x = blackholeX * cellSize + cellSize / 2;
blackhole.y = blackholeY * cellSize + cellSize / 2;
maze[blackholeY][blackholeX] = blackhole;
mazeContainer.addChild(blackhole);
}
}
}
function addBounceEffect() {
// Ball remains rigid - no bounce effects
}
function checkBallCollisions() {
var ballMazeX = Math.floor((ball.x - mazeContainer.x) / cellSize);
var ballMazeY = Math.floor((ball.y - mazeContainer.y) / cellSize);
// Check if ball is trying to move outside maze boundaries
if (ballMazeX < 0 || ballMazeX >= mazeWidth || ballMazeY < 0 || ballMazeY >= mazeHeight) {
// Keep ball within maze bounds and bounce
if (ball.x < mazeContainer.x + cellSize / 2) {
tween(ball, {
x: mazeContainer.x + cellSize / 2
}, {
duration: 50,
easing: tween.easeOut
});
ball.velocityX = Math.abs(ball.velocityX) * ball.bounceForce;
// Add slight random variation for realism
ball.velocityY += (Math.random() - 0.5) * 0.5;
}
if (ball.x > mazeContainer.x + (mazeWidth - 1) * cellSize + cellSize / 2) {
tween(ball, {
x: mazeContainer.x + (mazeWidth - 1) * cellSize + cellSize / 2
}, {
duration: 50,
easing: tween.easeOut
});
ball.velocityX = -Math.abs(ball.velocityX) * ball.bounceForce;
// Add slight random variation for realism
ball.velocityY += (Math.random() - 0.5) * 0.5;
}
if (ball.y < mazeContainer.y + cellSize / 2) {
tween(ball, {
y: mazeContainer.y + cellSize / 2
}, {
duration: 50,
easing: tween.easeOut
});
ball.velocityY = Math.abs(ball.velocityY) * ball.bounceForce;
// Add slight random variation for realism
ball.velocityX += (Math.random() - 0.5) * 0.5;
}
if (ball.y > mazeContainer.y + (mazeHeight - 1) * cellSize + cellSize / 2) {
tween(ball, {
y: mazeContainer.y + (mazeHeight - 1) * cellSize + cellSize / 2
}, {
duration: 50,
easing: tween.easeOut
});
ball.velocityY = -Math.abs(ball.velocityY) * ball.bounceForce;
// Add slight random variation for realism
ball.velocityX += (Math.random() - 0.5) * 0.5;
}
// Ball remains rigid - no shake effects
LK.getSound('bounce').play();
return;
}
var currentCell = maze[ballMazeY][ballMazeX];
// Check if ball is in a wall cell - if so, push it back to nearest path
if (currentCell && currentCell.cellType === 'wall') {
// Find the nearest path cell
var nearestPath = null;
var minDistance = Infinity;
for (var y = 0; y < mazeHeight; y++) {
for (var x = 0; x < mazeWidth; x++) {
if (maze[y] && maze[y][x] && (maze[y][x].cellType === 'path' || maze[y][x].cellType === 'blackhole')) {
var pathCenterX = mazeContainer.x + x * cellSize + cellSize / 2;
var pathCenterY = mazeContainer.y + y * cellSize + cellSize / 2;
var distance = Math.sqrt(Math.pow(ball.x - pathCenterX, 2) + Math.pow(ball.y - pathCenterY, 2));
if (distance < minDistance) {
minDistance = distance;
nearestPath = {
x: pathCenterX,
y: pathCenterY
};
}
}
}
}
if (nearestPath) {
// Push ball to nearest path
var directionX = nearestPath.x - ball.x;
var directionY = nearestPath.y - ball.y;
var distance = Math.sqrt(directionX * directionX + directionY * directionY);
if (distance > 0) {
directionX /= distance;
directionY /= distance;
var targetX = nearestPath.x - directionX * 10;
var targetY = nearestPath.y - directionY * 10;
// Smooth tween to new position
tween(ball, {
x: targetX,
y: targetY
}, {
duration: 100,
easing: tween.easeOut
});
}
// Realistic wall collision with momentum conservation
var impactSpeed = Math.sqrt(ball.velocityX * ball.velocityX + ball.velocityY * ball.velocityY);
ball.velocityX = -ball.velocityX * ball.bounceForce;
ball.velocityY = -ball.velocityY * ball.bounceForce;
// Add energy loss and slight randomness for realism
var energyLoss = 0.9;
ball.velocityX *= energyLoss;
ball.velocityY *= energyLoss;
ball.velocityX += (Math.random() - 0.5) * 0.3;
ball.velocityY += (Math.random() - 0.5) * 0.3;
}
// Ball remains rigid - no shake effects
LK.getSound('bounce').play();
} else if (currentCell && currentCell.cellType === 'blackhole') {
// Check if ball is close enough to black hole center
var blackholeCenterX = mazeContainer.x + blackholeX * cellSize + cellSize / 2;
var blackholeCenterY = mazeContainer.y + blackholeY * cellSize + cellSize / 2;
var distance = Math.sqrt(Math.pow(ball.x - blackholeCenterX, 2) + Math.pow(ball.y - blackholeCenterY, 2));
if (distance < 30 && !gameWon) {
gameWon = true;
LK.getSound('win').play();
LK.setScore(LK.getScore() + 1);
scoreText.setText('Level: ' + (LK.getScore() + 1));
// Flash effect
LK.effects.flashScreen(0x00ff00, 1000);
// Reset for next level
LK.setTimeout(function () {
resetLevel();
}, 1000);
}
}
}
function resetLevel() {
gameWon = false;
timeLeft = 60;
// Reset ball position
ball.x = mazeContainer.x + cellSize * 1.5;
ball.y = mazeContainer.y + cellSize * 1.5;
ball.velocityX = 0;
ball.velocityY = 0;
// Generate new maze
generateMaze();
}
// Initialize ball
ball = game.addChild(new Ball());
ball.x = mazeContainer.x + cellSize * 1.5;
ball.y = mazeContainer.y + cellSize * 1.5;
// Touch controls
game.down = function (x, y, obj) {
if (gameWon) return;
// Determine tap location and apply directional force
var screenWidth = 2048;
var tapX = x;
var jumpForce = -15; // Realistic upward impulse
var sideForce = 8; // Realistic horizontal impulse
if (tapX < screenWidth / 3) {
// Left tap - bounce left and up
ball.addForce(-sideForce, jumpForce);
} else if (tapX > screenWidth * 2 / 3) {
// Right tap - bounce right and up
ball.addForce(sideForce, jumpForce);
} else {
// Center tap - bounce straight up
ball.addForce(0, jumpForce);
}
};
// Generate initial maze
generateMaze();
// Timer countdown
var timerInterval = LK.setInterval(function () {
if (gameWon) return;
timeLeft--;
timerText.setText(timeLeft.toString());
if (timeLeft <= 0) {
timeLeft = 60;
generateMaze();
timerText.setText('60');
// Flash red to indicate maze change
LK.effects.flashScreen(0xff0000, 500);
}
}, 1000);
// Start background music
LK.playMusic('1tik');
game.update = function () {
// Game updates are handled by individual object update methods
}; /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var Ball = Container.expand(function () {
var self = Container.call(this);
var ballShadow = self.attachAsset('ballShadow', {
anchorX: 0.5,
anchorY: 0.5
});
ballShadow.alpha = 0.5;
ballShadow.y = 8; // Offset shadow below ball for heavier appearance
var ballGraphics = self.attachAsset('ball', {
anchorX: 0.5,
anchorY: 0.5
});
self.velocityX = 0;
self.velocityY = 0;
self.gravity = 1.2; // Stronger gravity for realistic falling
self.airResistance = 0.995; // Air resistance instead of friction
self.bounceForce = 0.8; // More realistic bounce with gravity
self.maxSpeed = 20;
self.mass = 1.0; // Add mass for realistic physics
self.update = function () {
// Apply realistic gravity acceleration
self.velocityY += self.gravity;
// Apply air resistance to horizontal movement only
self.velocityX *= self.airResistance;
// No air resistance on vertical movement to allow proper falling
// Natural velocity dampening (no artificial cutoffs)
var currentSpeed = Math.sqrt(self.velocityX * self.velocityX + self.velocityY * self.velocityY);
// Limit max speed with realistic scaling
if (currentSpeed > self.maxSpeed) {
var scale = self.maxSpeed / currentSpeed;
self.velocityX *= scale;
self.velocityY *= scale;
}
// Store last position for impact detection
if (self.lastY === undefined) self.lastY = self.y;
var wasMovingDown = self.velocityY > 0;
// Apply velocity directly for natural gravity-based movement
self.x += self.velocityX;
self.y += self.velocityY;
// Ball remains rigid - no landing effects
self.lastY = self.y;
// Check maze boundaries and collisions
checkBallCollisions();
};
self.addForce = function (forceX, forceY) {
// Apply force based on mass (F = ma, so a = F/m)
var accelerationX = forceX / self.mass;
var accelerationY = forceY / self.mass;
// Add to velocity (realistic momentum)
self.velocityX += accelerationX;
self.velocityY += accelerationY;
// Stop any existing movement tweens to prevent conflicts
tween.stop(self, {
x: true,
y: true
});
};
return self;
});
var MazeCell = Container.expand(function (cellType) {
var self = Container.call(this);
self.cellType = cellType || 'wall';
var cellGraphics;
if (self.cellType === 'wall') {
cellGraphics = self.attachAsset('wall', {
anchorX: 0.5,
anchorY: 0.5
});
} else if (self.cellType === 'path') {
cellGraphics = self.attachAsset('path', {
anchorX: 0.5,
anchorY: 0.5
});
} else if (self.cellType === 'blackhole') {
cellGraphics = self.attachAsset('blackhole', {
anchorX: 0.5,
anchorY: 0.5
});
}
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x222222
});
/****
* Game Code
****/
// Add background
var background = game.addChild(LK.getAsset('background', {
x: 0,
y: 0,
width: 2048,
height: 2732
}));
var ball;
var maze = [];
var mazeWidth = 20;
var mazeHeight = 30;
var cellSize = 60;
var mazeContainer;
var blackholeX, blackholeY;
var timeLeft = 60;
var gameWon = false;
// UI Elements
var timerText = new Text2('60', {
size: 80,
fill: 0xFFFFFF
});
timerText.anchor.set(0.5, 0);
LK.gui.top.addChild(timerText);
var scoreText = new Text2('Level: 1', {
size: 60,
fill: 0xFFFFFF
});
scoreText.anchor.set(0, 0);
scoreText.x = 150;
scoreText.y = 20;
LK.gui.topLeft.addChild(scoreText);
// Initialize maze container
mazeContainer = game.addChild(new Container());
mazeContainer.x = (2048 - mazeWidth * cellSize) / 2;
mazeContainer.y = (2732 - mazeHeight * cellSize) / 2;
// Generate initial maze
function generateMaze() {
// Clear existing maze
for (var i = 0; i < maze.length; i++) {
for (var j = 0; j < maze[i].length; j++) {
if (maze[i][j]) {
maze[i][j].destroy();
}
}
}
maze = [];
// Initialize maze grid (all walls initially)
var mazeGrid = [];
for (var y = 0; y < mazeHeight; y++) {
mazeGrid[y] = [];
maze[y] = [];
for (var x = 0; x < mazeWidth; x++) {
// 0 = wall, 1 = path, 2 = visited
mazeGrid[y][x] = 0;
maze[y][x] = null;
}
}
// Recursive backtracking maze generation
var stack = [];
var currentX = 1;
var currentY = 1;
mazeGrid[currentY][currentX] = 1; // Mark starting cell as path
stack.push({
x: currentX,
y: currentY
});
while (stack.length > 0) {
var neighbors = [];
var directions = [{
x: 0,
y: -2
},
// North
{
x: 2,
y: 0
},
// East
{
x: 0,
y: 2
},
// South
{
x: -2,
y: 0
} // West
];
// Find unvisited neighbors
for (var i = 0; i < directions.length; i++) {
var nx = currentX + directions[i].x;
var ny = currentY + directions[i].y;
if (nx > 0 && nx < mazeWidth - 1 && ny > 0 && ny < mazeHeight - 1 && mazeGrid[ny][nx] === 0) {
neighbors.push({
x: nx,
y: ny,
dir: directions[i]
});
}
}
if (neighbors.length > 0) {
// Choose random neighbor
var randomNeighbor = neighbors[Math.floor(Math.random() * neighbors.length)];
var nextX = randomNeighbor.x;
var nextY = randomNeighbor.y;
var wallX = currentX + randomNeighbor.dir.x / 2;
var wallY = currentY + randomNeighbor.dir.y / 2;
// Remove wall between current and next cell
mazeGrid[wallY][wallX] = 1;
mazeGrid[nextY][nextX] = 1;
// Move to next cell
currentX = nextX;
currentY = nextY;
stack.push({
x: currentX,
y: currentY
});
} else {
// Backtrack
var prev = stack.pop();
if (stack.length > 0) {
currentX = stack[stack.length - 1].x;
currentY = stack[stack.length - 1].y;
}
}
}
// Add some random openings for better gameplay
for (var y = 2; y < mazeHeight - 2; y += 2) {
for (var x = 2; x < mazeWidth - 2; x += 2) {
if (Math.random() < 0.15) {
// Create random opening
var openingDirs = [{
x: 1,
y: 0
}, {
x: -1,
y: 0
}, {
x: 0,
y: 1
}, {
x: 0,
y: -1
}];
var randomDir = openingDirs[Math.floor(Math.random() * openingDirs.length)];
var openX = x + randomDir.x;
var openY = y + randomDir.y;
if (openX > 0 && openX < mazeWidth - 1 && openY > 0 && openY < mazeHeight - 1) {
mazeGrid[openY][openX] = 1;
}
}
}
}
// Create visual maze from grid
for (var y = 0; y < mazeHeight; y++) {
for (var x = 0; x < mazeWidth; x++) {
var cellType = mazeGrid[y][x] === 1 ? 'path' : 'wall';
var cell = new MazeCell(cellType);
cell.x = x * cellSize + cellSize / 2;
cell.y = y * cellSize + cellSize / 2;
maze[y][x] = cell;
mazeContainer.addChild(cell);
}
}
// Place black hole exit
placeBlackHole();
}
function placeBlackHole() {
// Find all path cells in the bottom areas of the maze
var pathCells = [];
// Start from bottom and work upward to find suitable path cells
// Ensure we stay within maze bounds
for (var y = Math.max(1, mazeHeight - 6); y <= Math.min(mazeHeight - 2, mazeHeight - 1); y++) {
for (var x = 1; x < mazeWidth - 1; x++) {
// Additional boundary check
if (x >= 0 && x < mazeWidth && y >= 0 && y < mazeHeight && maze[y] && maze[y][x] && maze[y][x].cellType === 'path') {
// Count how many neighboring cells are paths
var neighborCount = 0;
var directions = [{
x: 0,
y: -1
}, {
x: 1,
y: 0
}, {
x: 0,
y: 1
}, {
x: -1,
y: 0
}];
for (var i = 0; i < directions.length; i++) {
var nx = x + directions[i].x;
var ny = y + directions[i].y;
if (nx >= 0 && nx < mazeWidth && ny >= 0 && ny < mazeHeight && maze[ny] && maze[ny][nx] && maze[ny][nx].cellType === 'path') {
neighborCount++;
}
}
pathCells.push({
x: x,
y: y,
neighbors: neighborCount,
priority: (mazeHeight - y) * 10 + (4 - neighborCount) // Prefer bottom cells with fewer neighbors
});
}
}
}
// If no path cells found in bottom area, expand search to entire maze
if (pathCells.length === 0) {
for (var y = 1; y < mazeHeight - 1; y++) {
for (var x = 1; x < mazeWidth - 1; x++) {
// Additional boundary check
if (x >= 0 && x < mazeWidth && y >= 0 && y < mazeHeight && maze[y] && maze[y][x] && maze[y][x].cellType === 'path') {
pathCells.push({
x: x,
y: y,
neighbors: 1,
priority: 1
});
}
}
}
}
// If still no path cells found, create one by forcing a path within bounds
if (pathCells.length === 0) {
// Find middle of bottom row and force it to be a path, ensuring it's within bounds
var bottomY = Math.max(1, Math.min(mazeHeight - 2, mazeHeight - 2));
var middleX = Math.max(1, Math.min(mazeWidth - 2, Math.floor(mazeWidth / 2)));
if (maze[bottomY] && maze[bottomY][middleX]) {
maze[bottomY][middleX].destroy();
var pathCell = new MazeCell('path');
pathCell.x = middleX * cellSize + cellSize / 2;
pathCell.y = bottomY * cellSize + cellSize / 2;
maze[bottomY][middleX] = pathCell;
mazeContainer.addChild(pathCell);
pathCells.push({
x: middleX,
y: bottomY,
neighbors: 1,
priority: 100
});
}
}
if (pathCells.length > 0) {
// Choose the path cell with highest priority (bottom-most with fewest neighbors)
pathCells.sort(function (a, b) {
return b.priority - a.priority;
});
var bestCell = pathCells[0];
// Ensure black hole coordinates are within maze bounds
blackholeX = Math.max(0, Math.min(mazeWidth - 1, bestCell.x));
blackholeY = Math.max(0, Math.min(mazeHeight - 1, bestCell.y));
// Replace path with black hole, with additional boundary checks
if (blackholeX >= 0 && blackholeX < mazeWidth && blackholeY >= 0 && blackholeY < mazeHeight && maze[blackholeY] && maze[blackholeY][blackholeX]) {
maze[blackholeY][blackholeX].destroy();
var blackhole = new MazeCell('blackhole');
blackhole.x = blackholeX * cellSize + cellSize / 2;
blackhole.y = blackholeY * cellSize + cellSize / 2;
maze[blackholeY][blackholeX] = blackhole;
mazeContainer.addChild(blackhole);
}
}
}
function addBounceEffect() {
// Ball remains rigid - no bounce effects
}
function checkBallCollisions() {
var ballMazeX = Math.floor((ball.x - mazeContainer.x) / cellSize);
var ballMazeY = Math.floor((ball.y - mazeContainer.y) / cellSize);
// Check if ball is trying to move outside maze boundaries
if (ballMazeX < 0 || ballMazeX >= mazeWidth || ballMazeY < 0 || ballMazeY >= mazeHeight) {
// Keep ball within maze bounds and bounce
if (ball.x < mazeContainer.x + cellSize / 2) {
tween(ball, {
x: mazeContainer.x + cellSize / 2
}, {
duration: 50,
easing: tween.easeOut
});
ball.velocityX = Math.abs(ball.velocityX) * ball.bounceForce;
// Add slight random variation for realism
ball.velocityY += (Math.random() - 0.5) * 0.5;
}
if (ball.x > mazeContainer.x + (mazeWidth - 1) * cellSize + cellSize / 2) {
tween(ball, {
x: mazeContainer.x + (mazeWidth - 1) * cellSize + cellSize / 2
}, {
duration: 50,
easing: tween.easeOut
});
ball.velocityX = -Math.abs(ball.velocityX) * ball.bounceForce;
// Add slight random variation for realism
ball.velocityY += (Math.random() - 0.5) * 0.5;
}
if (ball.y < mazeContainer.y + cellSize / 2) {
tween(ball, {
y: mazeContainer.y + cellSize / 2
}, {
duration: 50,
easing: tween.easeOut
});
ball.velocityY = Math.abs(ball.velocityY) * ball.bounceForce;
// Add slight random variation for realism
ball.velocityX += (Math.random() - 0.5) * 0.5;
}
if (ball.y > mazeContainer.y + (mazeHeight - 1) * cellSize + cellSize / 2) {
tween(ball, {
y: mazeContainer.y + (mazeHeight - 1) * cellSize + cellSize / 2
}, {
duration: 50,
easing: tween.easeOut
});
ball.velocityY = -Math.abs(ball.velocityY) * ball.bounceForce;
// Add slight random variation for realism
ball.velocityX += (Math.random() - 0.5) * 0.5;
}
// Ball remains rigid - no shake effects
LK.getSound('bounce').play();
return;
}
var currentCell = maze[ballMazeY][ballMazeX];
// Check if ball is in a wall cell - if so, push it back to nearest path
if (currentCell && currentCell.cellType === 'wall') {
// Find the nearest path cell
var nearestPath = null;
var minDistance = Infinity;
for (var y = 0; y < mazeHeight; y++) {
for (var x = 0; x < mazeWidth; x++) {
if (maze[y] && maze[y][x] && (maze[y][x].cellType === 'path' || maze[y][x].cellType === 'blackhole')) {
var pathCenterX = mazeContainer.x + x * cellSize + cellSize / 2;
var pathCenterY = mazeContainer.y + y * cellSize + cellSize / 2;
var distance = Math.sqrt(Math.pow(ball.x - pathCenterX, 2) + Math.pow(ball.y - pathCenterY, 2));
if (distance < minDistance) {
minDistance = distance;
nearestPath = {
x: pathCenterX,
y: pathCenterY
};
}
}
}
}
if (nearestPath) {
// Push ball to nearest path
var directionX = nearestPath.x - ball.x;
var directionY = nearestPath.y - ball.y;
var distance = Math.sqrt(directionX * directionX + directionY * directionY);
if (distance > 0) {
directionX /= distance;
directionY /= distance;
var targetX = nearestPath.x - directionX * 10;
var targetY = nearestPath.y - directionY * 10;
// Smooth tween to new position
tween(ball, {
x: targetX,
y: targetY
}, {
duration: 100,
easing: tween.easeOut
});
}
// Realistic wall collision with momentum conservation
var impactSpeed = Math.sqrt(ball.velocityX * ball.velocityX + ball.velocityY * ball.velocityY);
ball.velocityX = -ball.velocityX * ball.bounceForce;
ball.velocityY = -ball.velocityY * ball.bounceForce;
// Add energy loss and slight randomness for realism
var energyLoss = 0.9;
ball.velocityX *= energyLoss;
ball.velocityY *= energyLoss;
ball.velocityX += (Math.random() - 0.5) * 0.3;
ball.velocityY += (Math.random() - 0.5) * 0.3;
}
// Ball remains rigid - no shake effects
LK.getSound('bounce').play();
} else if (currentCell && currentCell.cellType === 'blackhole') {
// Check if ball is close enough to black hole center
var blackholeCenterX = mazeContainer.x + blackholeX * cellSize + cellSize / 2;
var blackholeCenterY = mazeContainer.y + blackholeY * cellSize + cellSize / 2;
var distance = Math.sqrt(Math.pow(ball.x - blackholeCenterX, 2) + Math.pow(ball.y - blackholeCenterY, 2));
if (distance < 30 && !gameWon) {
gameWon = true;
LK.getSound('win').play();
LK.setScore(LK.getScore() + 1);
scoreText.setText('Level: ' + (LK.getScore() + 1));
// Flash effect
LK.effects.flashScreen(0x00ff00, 1000);
// Reset for next level
LK.setTimeout(function () {
resetLevel();
}, 1000);
}
}
}
function resetLevel() {
gameWon = false;
timeLeft = 60;
// Reset ball position
ball.x = mazeContainer.x + cellSize * 1.5;
ball.y = mazeContainer.y + cellSize * 1.5;
ball.velocityX = 0;
ball.velocityY = 0;
// Generate new maze
generateMaze();
}
// Initialize ball
ball = game.addChild(new Ball());
ball.x = mazeContainer.x + cellSize * 1.5;
ball.y = mazeContainer.y + cellSize * 1.5;
// Touch controls
game.down = function (x, y, obj) {
if (gameWon) return;
// Determine tap location and apply directional force
var screenWidth = 2048;
var tapX = x;
var jumpForce = -15; // Realistic upward impulse
var sideForce = 8; // Realistic horizontal impulse
if (tapX < screenWidth / 3) {
// Left tap - bounce left and up
ball.addForce(-sideForce, jumpForce);
} else if (tapX > screenWidth * 2 / 3) {
// Right tap - bounce right and up
ball.addForce(sideForce, jumpForce);
} else {
// Center tap - bounce straight up
ball.addForce(0, jumpForce);
}
};
// Generate initial maze
generateMaze();
// Timer countdown
var timerInterval = LK.setInterval(function () {
if (gameWon) return;
timeLeft--;
timerText.setText(timeLeft.toString());
if (timeLeft <= 0) {
timeLeft = 60;
generateMaze();
timerText.setText('60');
// Flash red to indicate maze change
LK.effects.flashScreen(0xff0000, 500);
}
}, 1000);
// Start background music
LK.playMusic('1tik');
game.update = function () {
// Game updates are handled by individual object update methods
};
lubang warp penuh warna. In-Game asset. 2d. High contrast. No shadows
warna pink dinding In-Game asset. 2d. High contrast. No shadows
bola penuh hitam. In-Game asset. 2d. High contrast. No shadows
malam bulan purnama diatas hutan dataran tinggi. di kejauhan nampak kota di malam hari. In-Game asset. 2d. High contrast. No shadows