/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Represents a single collectible in the corridor
var Collectible = Container.expand(function () {
var self = Container.call(this);
var sprite = self.attachAsset('collectible', {
anchorX: 0.5,
anchorY: 1
});
self.lane = 1;
self.depth = 0;
self.type = 'collectible';
self.active = true;
self.getHitbox = function () {
return {
x: self.x - sprite.width / 2,
y: self.y - sprite.height,
w: sprite.width,
h: sprite.height
};
};
return self;
});
// Represents a single obstacle in the corridor
var Obstacle = Container.expand(function () {
var self = Container.call(this);
var sprite = self.attachAsset('obstacle', {
anchorX: 0.5,
anchorY: 1
});
// Logical position in corridor space
self.lane = 1; // 0=left, 1=center, 2=right
self.depth = 0; // Distance from player (z)
self.type = 'obstacle';
self.active = true;
// For collision detection
self.getHitbox = function () {
// Returns {x, y, w, h} in screen space
return {
x: self.x - sprite.width / 2,
y: self.y - sprite.height,
w: sprite.width,
h: sprite.height
};
};
return self;
});
// Represents a projectile shot by the player
var Projectile = Container.expand(function () {
var self = Container.call(this);
var sprite = self.attachAsset('btnShoot', {
anchorX: 0.5,
anchorY: 0.5
});
sprite.tint = 0xff2222;
self.type = 'projectile';
self.active = true;
self.lane = 1;
self.row = 1;
self.depth = 0;
self.speed = 0.7; // how fast the projectile moves forward (z axis)
self.lastDepth = self.depth;
self.update = function () {
self.lastDepth = self.depth;
self.depth += self.speed;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x000000
});
/****
* Game Code
****/
// --- Scrolling background setup ---
var bg1 = LK.getAsset('ceilingSlice', {
anchorX: 0.5,
anchorY: 0
});
var bg2 = LK.getAsset('ceilingSlice', {
anchorX: 0.5,
anchorY: 0
});
bg1.width = 2048;
bg1.height = 2732;
bg2.width = 2048;
bg2.height = 2732;
bg1.x = 2048 / 2;
bg2.x = 2048 / 2;
bg1.y = 0;
bg2.y = -2732;
bg1.alpha = 0.18;
bg2.alpha = 0.18;
game.addChild(bg1);
game.addChild(bg2);
// Collectible (ellipse)
// Obstacle (box)
// Ceiling slice (horizontal rectangle, will be stretched/scaled for perspective)
// Floor slice (horizontal rectangle, will be stretched/scaled for perspective)
// Corridor wall slice (vertical rectangle, will be stretched/scaled for perspective)
// --- Raycasting & Corridor Parameters ---
var corridor = {
width: 3,
height: 2,
// 2 rows: 0=top, 1=bottom
// 6 lanes: (0,0)=top-left, (1,0)=top-center, (2,0)=top-right, (0,1)=bottom-left, (1,1)=bottom-center, (2,1)=bottom-right
laneWidth: 700,
rowHeight: 900,
// vertical distance between rows (was 600, now lower lane is much lower)
// vertical distance between rows
depthSlices: 16,
maxDepth: 16,
fov: Math.PI / 3,
wallColor: 0x4444aa,
floorColor: 0x222222,
ceilingColor: 0x111133
};
// Player state
var player = {
lane: 1,
// 0=left, 1=center, 2=right
row: 1,
// 0=top, 1=bottom
moving: false,
moveTarget: 1,
moveRowTarget: 1,
moveTween: null,
moveRowTween: null,
alive: true
};
// Input delay state
var inputDelayActive = false;
var inputDelayTimer = null;
var inputDelayMs = 180; // ms, matches tween duration
// Game state
var speed = 0.18; // units per frame (z axis)
var speedIncrease = 0.00004; // per frame
var maxSpeed = 0.38;
var objects = []; // all obstacles and collectibles
var spawnTimer = 0;
var spawnInterval = 38; // frames between spawns
var score = 0;
var distance = 0; // how far player has run (for difficulty)
var lastTouchX = null;
// --- GUI ---
var scoreTxt = new Text2('0', {
size: 220,
fill: "#fff"
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
var distTxt = new Text2('0m', {
size: 120,
fill: "#aaa"
});
distTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(distTxt);
distTxt.y = 220;
// --- Corridor rendering nodes ---
var wallNodes = [];
var floorNodes = [];
var ceilingNodes = [];
for (var i = 0; i < corridor.depthSlices; i++) {
// Left wall
var wl = LK.getAsset('wallSlice', {
anchorX: 0.5,
anchorY: 1
});
game.addChild(wl);
wallNodes.push(wl);
// Right wall
var wr = LK.getAsset('wallSlice', {
anchorX: 0.5,
anchorY: 1
});
game.addChild(wr);
wallNodes.push(wr);
// Floor
var fl = LK.getAsset('floorSlice', {
anchorX: 0.5,
anchorY: 0
});
game.addChild(fl);
floorNodes.push(fl);
// Ceiling
var cl = LK.getAsset('ceilingSlice', {
anchorX: 0.5,
anchorY: 1
});
game.addChild(cl);
ceilingNodes.push(cl);
}
// --- Helper: Perspective projection for fake-3D ---
function projectZ(z) {
// Returns scale and y offset for a given depth (z)
// Camera is at y = horizonY, looking down corridor
var screenH = 2732;
var horizonY = screenH * 0.42;
var vanishY = horizonY;
var baseScale = 1.0;
// Perspective: scale = nearPlane / (z + nearPlane)
var near = 1.2;
var scale = near / (z + near);
var y = vanishY + screenH * 0.32 / (z + 0.7);
return {
scale: scale,
y: y
};
}
// --- Helper: X position for lane at given depth ---
function laneX(lane, z) {
var centerX = 2048 / 2;
var laneOffset = (lane - 1) * corridor.laneWidth;
var p = projectZ(z);
return centerX + laneOffset * p.scale;
}
// --- Helper: Y position for row at given depth ---
function laneY(row, z) {
// row: 0=top, 1=bottom
var baseY = 0;
var rowOffset = (row - 0.5) * corridor.rowHeight;
var p = projectZ(z);
return baseY + rowOffset * p.scale;
}
// --- Spawning obstacles and collectibles ---
function spawnObject() {
// Randomly choose obstacle or collectible
var isObstacle = Math.random() < 0.7;
var lane = Math.floor(Math.random() * corridor.width);
var row = Math.floor(Math.random() * corridor.height);
var depth = corridor.maxDepth + 2; // spawn just beyond farthest slice
var obj;
if (isObstacle) {
obj = new Obstacle();
} else {
obj = new Collectible();
}
obj.lane = lane;
obj.row = row;
obj.depth = depth;
obj.active = true;
objects.push(obj);
game.addChild(obj);
}
// --- On-screen button controls for movement ---
var btnSize = 340;
var btnMargin = 0; // Make buttons much closer together (no margin at all)
var btnAlpha = 0.92;
// Create button assets
var btnUp = LK.getAsset('btnUp', {
anchorX: 0.5,
anchorY: 0.5
});
var btnDown = LK.getAsset('btnDown', {
anchorX: 0.5,
anchorY: 0.5
});
var btnLeft = LK.getAsset('btnLeft', {
anchorX: 0.5,
anchorY: 0.5
});
var btnRight = LK.getAsset('btnRight', {
anchorX: 0.5,
anchorY: 0.5
});
// Set button sizes and alpha
btnUp.width = btnUp.height = btnSize;
btnDown.width = btnDown.height = btnSize;
btnLeft.width = btnLeft.height = btnSize;
btnRight.width = btnRight.height = btnSize;
btnUp.alpha = btnDown.alpha = btnLeft.alpha = btnRight.alpha = btnAlpha;
// Position buttons in a diamond at the bottom left of the screen, avoiding the top left 100x100 area
// Move the buttons further up and to the right
var btnYBase = 2732 - btnSize - btnMargin - 120; // move up by 120px (was 40)
var btnXBase = btnMargin + btnSize + 140; // move right by 140px (was 180)
btnUp.x = btnXBase;
btnUp.y = btnYBase - btnSize - btnMargin;
btnDown.x = btnXBase;
btnDown.y = btnYBase + btnSize + btnMargin;
btnLeft.x = btnXBase - btnSize - btnMargin;
btnLeft.y = btnYBase;
btnRight.x = btnXBase + btnSize + btnMargin;
btnRight.y = btnYBase;
// Add buttons to game
game.addChild(btnUp);
game.addChild(btnDown);
game.addChild(btnLeft);
game.addChild(btnRight);
// --- Shoot button setup ---
var btnShoot = LK.getAsset('btnShoot', {
anchorX: 0.5,
anchorY: 0.5
});
btnShoot.width = btnShoot.height = btnSize;
btnShoot.alpha = btnAlpha;
// Place shoot button even further to the right of the right button, with a slightly larger margin
btnShoot.x = btnRight.x + btnSize + btnMargin + 240;
btnShoot.y = btnRight.y;
game.addChild(btnShoot);
// Button event handlers
btnUp.down = function (x, y, obj) {
if (!player.moving && player.alive && !inputDelayActive && player.row > 0) {
movePlayerTo(player.lane, player.row - 1);
inputDelayActive = true;
if (inputDelayTimer !== null) LK.clearTimeout(inputDelayTimer);
inputDelayTimer = LK.setTimeout(function () {
inputDelayActive = false;
inputDelayTimer = null;
}, inputDelayMs);
}
};
btnDown.down = function (x, y, obj) {
if (!player.moving && player.alive && !inputDelayActive && player.row < 1) {
movePlayerTo(player.lane, player.row + 1);
inputDelayActive = true;
if (inputDelayTimer !== null) LK.clearTimeout(inputDelayTimer);
inputDelayTimer = LK.setTimeout(function () {
inputDelayActive = false;
inputDelayTimer = null;
}, inputDelayMs);
}
};
btnLeft.down = function (x, y, obj) {
if (!player.moving && player.alive && !inputDelayActive && player.lane > 0) {
movePlayerTo(player.lane - 1, player.row);
inputDelayActive = true;
if (inputDelayTimer !== null) LK.clearTimeout(inputDelayTimer);
inputDelayTimer = LK.setTimeout(function () {
inputDelayActive = false;
inputDelayTimer = null;
}, inputDelayMs);
}
};
btnRight.down = function (x, y, obj) {
if (!player.moving && player.alive && !inputDelayActive && player.lane < 2) {
movePlayerTo(player.lane + 1, player.row);
inputDelayActive = true;
if (inputDelayTimer !== null) LK.clearTimeout(inputDelayTimer);
inputDelayTimer = LK.setTimeout(function () {
inputDelayActive = false;
inputDelayTimer = null;
}, inputDelayMs);
}
};
// --- Projectiles array ---
var projectiles = [];
// --- Shoot button event handler ---
btnShoot.down = function (x, y, obj) {
if (!player.alive) return;
// Create a new projectile at the player's current lane/row, just in front of the player
var proj = new Projectile();
proj.lane = player.lane;
proj.row = player.row;
proj.depth = 0.7; // just in front of player
proj.lastDepth = proj.depth;
projectiles.push(proj);
game.addChild(proj);
};
// Disable tap-to-move and swipe
game.down = function (x, y, obj) {};
game.move = function (x, y, obj) {};
game.up = function (x, y, obj) {};
// --- Move player to lane with tween ---
function movePlayerTo(targetLane, targetRow) {
if (player.moving || !player.alive) return;
if (typeof targetRow === "undefined") targetRow = player.row;
player.moveTarget = targetLane;
player.moveRowTarget = targetRow;
player.moving = true;
// Activate input delay immediately on move
inputDelayActive = true;
if (inputDelayTimer !== null) LK.clearTimeout(inputDelayTimer);
inputDelayTimer = LK.setTimeout(function () {
inputDelayActive = false;
inputDelayTimer = null;
}, inputDelayMs);
var startLane = player.lane;
var endLane = targetLane;
var startRow = player.row;
var endRow = targetRow;
var t = {
v: startLane
};
var tr = {
v: startRow
};
player.moveTween = tween(t, {
v: endLane
}, {
duration: 180,
easing: tween.cubicOut,
onFinish: function onFinish() {
player.lane = endLane;
player.moving = false;
// If a move was queued during the tween, perform it now
if (typeof player.queuedMove !== "undefined" && player.queuedMove !== null && (player.queuedMove !== player.lane || player.queuedRowMove !== player.row)) {
var nextLane = player.queuedMove;
var nextRow = player.queuedRowMove;
player.queuedMove = null;
player.queuedRowMove = null;
movePlayerTo(nextLane, nextRow);
}
}
});
player.moveRowTween = tween(tr, {
v: endRow
}, {
duration: 180,
easing: tween.cubicOut,
onFinish: function onFinish() {
player.row = endRow;
}
});
// We'll interpolate player.lane and player.row visually in render
player.laneTweenObj = t;
player.rowTweenObj = tr;
}
// --- Main update loop ---
game.update = function () {
// --- Scroll background ---
var bgScrollSpeed = speed * 2732 * 0.18; // scale to match corridor speed visually
bg1.y += bgScrollSpeed;
bg2.y += bgScrollSpeed;
if (bg1.y >= 2732) bg1.y = bg2.y - 2732;
if (bg2.y >= 2732) bg2.y = bg1.y - 2732;
if (!player.alive) return;
// Increase speed over time, and also based on collectibles collected (score)
var speedFromCollectibles = 0.018 * score; // Each collectible increases speed by 0.018
speed += speedIncrease;
speed = 0.18 + speedFromCollectibles + (speed - 0.18); // base + collectibles + time
if (speed > maxSpeed) speed = maxSpeed;
distance += speed;
distTxt.setText(Math.floor(distance) + "m");
// Spawn new objects
spawnTimer--;
if (spawnTimer <= 0) {
spawnObject();
spawnTimer = spawnInterval - Math.floor(distance / 60);
if (spawnTimer < 18) spawnTimer = 18;
}
// Move objects closer (decrease depth)
for (var i = objects.length - 1; i >= 0; i--) {
var obj = objects[i];
obj.depth -= speed;
if (obj.depth < 0.2) {
// Passed player, remove
obj.destroy();
objects.splice(i, 1);
continue;
}
// Project to screen
var p = projectZ(obj.depth);
obj.scaleX = obj.scaleY = p.scale * 1.2;
if (typeof obj.row === "undefined") obj.row = 1;
obj.x = laneX(obj.lane, obj.depth);
obj.y = p.y + 320 * p.scale + laneY(obj.row, obj.depth);
obj.visible = obj.depth > 0.2 && obj.depth < corridor.maxDepth + 2;
}
// Player lane interpolation (for smooth movement)
var visLane = player.lane;
var visRow = player.row;
if (player.moving && player.laneTweenObj) {
visLane = player.laneTweenObj.v;
}
if (player.moving && player.rowTweenObj) {
visRow = player.rowTweenObj.v;
}
// --- Render corridor slices ---
for (var i = 0; i < corridor.depthSlices; i++) {
// Offset z by distance to make walls/floor/ceiling scroll opposite to objects
var z = i + 1.2 - distance % 1; // reverse scroll direction
var p = projectZ(z);
// Walls
var wallW = 80 * p.scale * 2.2;
var wallH = 800 * p.scale * 2.2;
// Left wall
var wl = wallNodes[i * 2];
wl.x = laneX(0, z) - corridor.laneWidth * p.scale / 2 - wallW / 2;
wl.y = p.y + wallH;
wl.width = wallW;
wl.height = wallH;
wl.visible = true;
// Right wall
var wr = wallNodes[i * 2 + 1];
wr.x = laneX(2, z) + corridor.laneWidth * p.scale / 2 + wallW / 2;
wr.y = p.y + wallH;
wr.width = wallW;
wr.height = wallH;
wr.visible = true;
// Floor
var fl = floorNodes[i];
fl.x = 2048 / 2;
fl.y = p.y + wallH;
fl.width = corridor.laneWidth * corridor.width * p.scale * 1.1;
fl.height = 80 * p.scale * 2.2;
fl.visible = true;
// Ceiling
var cl = ceilingNodes[i];
cl.x = 2048 / 2;
cl.y = p.y - 80 * p.scale * 1.2;
cl.width = corridor.laneWidth * corridor.width * p.scale * 1.1;
cl.height = 80 * p.scale * 2.2;
cl.visible = true;
}
// --- Collision detection ---
// Player is always at z=0, y=bottom of screen
var playerZ = 0.7;
var playerP = projectZ(playerZ);
var playerX = laneX(visLane, playerZ);
var playerY = playerP.y + 320 * playerP.scale + laneY(visRow, playerZ);
var playerW = 340 * playerP.scale * 1.2;
var playerH = 420 * playerP.scale * 1.2;
// --- Render player visual representation ---
if (typeof player.visual === "undefined") {
// Create the player visual using a new asset, anchor center
player.visual = LK.getAsset('player', {
anchorX: 0.5,
anchorY: 1
});
player.visual.width = 340;
player.visual.height = 420;
player.visual.alpha = 0.98;
game.addChild(player.visual);
}
player.visual.visible = player.alive;
player.visual.x = playerX;
player.visual.y = playerY;
player.visual.scaleX = playerW / player.visual.width;
player.visual.scaleY = playerH / player.visual.height;
// Move player visual behind the movement buttons in the display list
if (player.visual.parent && player.visual.parent.children) {
var parent = player.visual.parent;
// Remove player.visual if it exists
if (parent) {
parent.removeChild(player.visual);
// Find the lowest index of the button visuals
var btns = [btnUp, btnDown, btnLeft, btnRight];
var minBtnIdx = parent.children.length;
for (var i = 0; i < btns.length; i++) {
var idx = parent.children.indexOf(btns[i]);
if (idx !== -1 && idx < minBtnIdx) minBtnIdx = idx;
}
// Insert player.visual just before the first button, or at the end if not found
if (minBtnIdx < parent.children.length) {
parent.addChildAt(player.visual, minBtnIdx);
} else {
parent.addChild(player.visual);
}
}
}
;
// --- Projectiles update/render/collision ---
for (var pi = projectiles.length - 1; pi >= 0; pi--) {
var proj = projectiles[pi];
proj.update();
// Project to screen
var pz = projectZ(proj.depth);
proj.scaleX = proj.scaleY = pz.scale * 1.3;
proj.x = laneX(proj.lane, proj.depth);
proj.y = pz.y + 320 * pz.scale + laneY(proj.row, proj.depth);
proj.visible = proj.depth > 0.2 && proj.depth < corridor.maxDepth + 2;
// Remove projectile if out of range
if (proj.depth > corridor.maxDepth + 2) {
proj.destroy();
projectiles.splice(pi, 1);
continue;
}
// Check collision with obstacles
for (var oi = objects.length - 1; oi >= 0; oi--) {
var obj = objects[oi];
if (!obj.active) continue;
if (obj.type !== 'obstacle') continue;
// Only check collision if close enough in depth
if (Math.abs(obj.depth - proj.depth) < 0.7 && obj.lane === proj.lane && obj.row === proj.row) {
// Remove both projectile and obstacle
obj.active = false;
tween(obj, {
scaleX: 2.2,
scaleY: 2.2,
alpha: 0
}, {
duration: 180,
easing: tween.cubicOut,
onFinish: function onFinish() {
obj.destroy();
}
});
objects.splice(oi, 1);
proj.destroy();
projectiles.splice(pi, 1);
break;
}
}
}
// For debugging, could render a player shadow here
for (var i = objects.length - 1; i >= 0; i--) {
var obj = objects[i];
if (!obj.active) continue;
// Only check collision if close enough
if (obj.depth < 1.2 && obj.depth > 0.2) {
var dx = Math.abs(obj.x - playerX);
var dy = Math.abs(obj.y - playerY);
var hitW = (playerW + obj.width) / 2;
var hitH = (playerH + obj.height) / 2;
if (dx < hitW * 0.7 && dy < hitH * 0.7) {
if (obj.type === 'obstacle') {
// Hit obstacle: game over
player.alive = false;
LK.effects.flashScreen(0xff0000, 900);
LK.showGameOver();
return;
} else if (obj.type === 'collectible') {
// Collect item
obj.active = false;
score += 1;
LK.setScore(score);
scoreTxt.setText(score);
tween(obj, {
scaleX: 2.2,
scaleY: 2.2,
alpha: 0
}, {
duration: 220,
easing: tween.cubicOut,
onFinish: function onFinish() {
obj.destroy();
}
});
objects.splice(i, 1);
}
}
}
}
};
// --- Reset game state on new game ---
function resetGame() {
// Remove all objects
for (var i = 0; i < objects.length; i++) {
objects[i].destroy();
}
objects = [];
player.lane = 1;
player.row = 1;
player.moving = false;
player.moveTarget = 1;
player.moveRowTarget = 1;
player.moveTween = null;
player.moveRowTween = null;
player.queuedMove = null;
player.queuedRowMove = null;
player.alive = true;
speed = 0.18;
score = 0;
distance = 0;
spawnTimer = 18;
scoreTxt.setText('0');
distTxt.setText('0m');
// Reset input delay state
inputDelayActive = false;
if (inputDelayTimer !== null) {
LK.clearTimeout(inputDelayTimer);
inputDelayTimer = null;
}
}
resetGame();
// Play background music at game start
LK.playMusic('Music');
// --- LK handles game over and reset automatically, but listen for new game start ---
LK.on('gameStart', function () {
resetGame();
LK.playMusic('Music');
}); /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Represents a single collectible in the corridor
var Collectible = Container.expand(function () {
var self = Container.call(this);
var sprite = self.attachAsset('collectible', {
anchorX: 0.5,
anchorY: 1
});
self.lane = 1;
self.depth = 0;
self.type = 'collectible';
self.active = true;
self.getHitbox = function () {
return {
x: self.x - sprite.width / 2,
y: self.y - sprite.height,
w: sprite.width,
h: sprite.height
};
};
return self;
});
// Represents a single obstacle in the corridor
var Obstacle = Container.expand(function () {
var self = Container.call(this);
var sprite = self.attachAsset('obstacle', {
anchorX: 0.5,
anchorY: 1
});
// Logical position in corridor space
self.lane = 1; // 0=left, 1=center, 2=right
self.depth = 0; // Distance from player (z)
self.type = 'obstacle';
self.active = true;
// For collision detection
self.getHitbox = function () {
// Returns {x, y, w, h} in screen space
return {
x: self.x - sprite.width / 2,
y: self.y - sprite.height,
w: sprite.width,
h: sprite.height
};
};
return self;
});
// Represents a projectile shot by the player
var Projectile = Container.expand(function () {
var self = Container.call(this);
var sprite = self.attachAsset('btnShoot', {
anchorX: 0.5,
anchorY: 0.5
});
sprite.tint = 0xff2222;
self.type = 'projectile';
self.active = true;
self.lane = 1;
self.row = 1;
self.depth = 0;
self.speed = 0.7; // how fast the projectile moves forward (z axis)
self.lastDepth = self.depth;
self.update = function () {
self.lastDepth = self.depth;
self.depth += self.speed;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x000000
});
/****
* Game Code
****/
// --- Scrolling background setup ---
var bg1 = LK.getAsset('ceilingSlice', {
anchorX: 0.5,
anchorY: 0
});
var bg2 = LK.getAsset('ceilingSlice', {
anchorX: 0.5,
anchorY: 0
});
bg1.width = 2048;
bg1.height = 2732;
bg2.width = 2048;
bg2.height = 2732;
bg1.x = 2048 / 2;
bg2.x = 2048 / 2;
bg1.y = 0;
bg2.y = -2732;
bg1.alpha = 0.18;
bg2.alpha = 0.18;
game.addChild(bg1);
game.addChild(bg2);
// Collectible (ellipse)
// Obstacle (box)
// Ceiling slice (horizontal rectangle, will be stretched/scaled for perspective)
// Floor slice (horizontal rectangle, will be stretched/scaled for perspective)
// Corridor wall slice (vertical rectangle, will be stretched/scaled for perspective)
// --- Raycasting & Corridor Parameters ---
var corridor = {
width: 3,
height: 2,
// 2 rows: 0=top, 1=bottom
// 6 lanes: (0,0)=top-left, (1,0)=top-center, (2,0)=top-right, (0,1)=bottom-left, (1,1)=bottom-center, (2,1)=bottom-right
laneWidth: 700,
rowHeight: 900,
// vertical distance between rows (was 600, now lower lane is much lower)
// vertical distance between rows
depthSlices: 16,
maxDepth: 16,
fov: Math.PI / 3,
wallColor: 0x4444aa,
floorColor: 0x222222,
ceilingColor: 0x111133
};
// Player state
var player = {
lane: 1,
// 0=left, 1=center, 2=right
row: 1,
// 0=top, 1=bottom
moving: false,
moveTarget: 1,
moveRowTarget: 1,
moveTween: null,
moveRowTween: null,
alive: true
};
// Input delay state
var inputDelayActive = false;
var inputDelayTimer = null;
var inputDelayMs = 180; // ms, matches tween duration
// Game state
var speed = 0.18; // units per frame (z axis)
var speedIncrease = 0.00004; // per frame
var maxSpeed = 0.38;
var objects = []; // all obstacles and collectibles
var spawnTimer = 0;
var spawnInterval = 38; // frames between spawns
var score = 0;
var distance = 0; // how far player has run (for difficulty)
var lastTouchX = null;
// --- GUI ---
var scoreTxt = new Text2('0', {
size: 220,
fill: "#fff"
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
var distTxt = new Text2('0m', {
size: 120,
fill: "#aaa"
});
distTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(distTxt);
distTxt.y = 220;
// --- Corridor rendering nodes ---
var wallNodes = [];
var floorNodes = [];
var ceilingNodes = [];
for (var i = 0; i < corridor.depthSlices; i++) {
// Left wall
var wl = LK.getAsset('wallSlice', {
anchorX: 0.5,
anchorY: 1
});
game.addChild(wl);
wallNodes.push(wl);
// Right wall
var wr = LK.getAsset('wallSlice', {
anchorX: 0.5,
anchorY: 1
});
game.addChild(wr);
wallNodes.push(wr);
// Floor
var fl = LK.getAsset('floorSlice', {
anchorX: 0.5,
anchorY: 0
});
game.addChild(fl);
floorNodes.push(fl);
// Ceiling
var cl = LK.getAsset('ceilingSlice', {
anchorX: 0.5,
anchorY: 1
});
game.addChild(cl);
ceilingNodes.push(cl);
}
// --- Helper: Perspective projection for fake-3D ---
function projectZ(z) {
// Returns scale and y offset for a given depth (z)
// Camera is at y = horizonY, looking down corridor
var screenH = 2732;
var horizonY = screenH * 0.42;
var vanishY = horizonY;
var baseScale = 1.0;
// Perspective: scale = nearPlane / (z + nearPlane)
var near = 1.2;
var scale = near / (z + near);
var y = vanishY + screenH * 0.32 / (z + 0.7);
return {
scale: scale,
y: y
};
}
// --- Helper: X position for lane at given depth ---
function laneX(lane, z) {
var centerX = 2048 / 2;
var laneOffset = (lane - 1) * corridor.laneWidth;
var p = projectZ(z);
return centerX + laneOffset * p.scale;
}
// --- Helper: Y position for row at given depth ---
function laneY(row, z) {
// row: 0=top, 1=bottom
var baseY = 0;
var rowOffset = (row - 0.5) * corridor.rowHeight;
var p = projectZ(z);
return baseY + rowOffset * p.scale;
}
// --- Spawning obstacles and collectibles ---
function spawnObject() {
// Randomly choose obstacle or collectible
var isObstacle = Math.random() < 0.7;
var lane = Math.floor(Math.random() * corridor.width);
var row = Math.floor(Math.random() * corridor.height);
var depth = corridor.maxDepth + 2; // spawn just beyond farthest slice
var obj;
if (isObstacle) {
obj = new Obstacle();
} else {
obj = new Collectible();
}
obj.lane = lane;
obj.row = row;
obj.depth = depth;
obj.active = true;
objects.push(obj);
game.addChild(obj);
}
// --- On-screen button controls for movement ---
var btnSize = 340;
var btnMargin = 0; // Make buttons much closer together (no margin at all)
var btnAlpha = 0.92;
// Create button assets
var btnUp = LK.getAsset('btnUp', {
anchorX: 0.5,
anchorY: 0.5
});
var btnDown = LK.getAsset('btnDown', {
anchorX: 0.5,
anchorY: 0.5
});
var btnLeft = LK.getAsset('btnLeft', {
anchorX: 0.5,
anchorY: 0.5
});
var btnRight = LK.getAsset('btnRight', {
anchorX: 0.5,
anchorY: 0.5
});
// Set button sizes and alpha
btnUp.width = btnUp.height = btnSize;
btnDown.width = btnDown.height = btnSize;
btnLeft.width = btnLeft.height = btnSize;
btnRight.width = btnRight.height = btnSize;
btnUp.alpha = btnDown.alpha = btnLeft.alpha = btnRight.alpha = btnAlpha;
// Position buttons in a diamond at the bottom left of the screen, avoiding the top left 100x100 area
// Move the buttons further up and to the right
var btnYBase = 2732 - btnSize - btnMargin - 120; // move up by 120px (was 40)
var btnXBase = btnMargin + btnSize + 140; // move right by 140px (was 180)
btnUp.x = btnXBase;
btnUp.y = btnYBase - btnSize - btnMargin;
btnDown.x = btnXBase;
btnDown.y = btnYBase + btnSize + btnMargin;
btnLeft.x = btnXBase - btnSize - btnMargin;
btnLeft.y = btnYBase;
btnRight.x = btnXBase + btnSize + btnMargin;
btnRight.y = btnYBase;
// Add buttons to game
game.addChild(btnUp);
game.addChild(btnDown);
game.addChild(btnLeft);
game.addChild(btnRight);
// --- Shoot button setup ---
var btnShoot = LK.getAsset('btnShoot', {
anchorX: 0.5,
anchorY: 0.5
});
btnShoot.width = btnShoot.height = btnSize;
btnShoot.alpha = btnAlpha;
// Place shoot button even further to the right of the right button, with a slightly larger margin
btnShoot.x = btnRight.x + btnSize + btnMargin + 240;
btnShoot.y = btnRight.y;
game.addChild(btnShoot);
// Button event handlers
btnUp.down = function (x, y, obj) {
if (!player.moving && player.alive && !inputDelayActive && player.row > 0) {
movePlayerTo(player.lane, player.row - 1);
inputDelayActive = true;
if (inputDelayTimer !== null) LK.clearTimeout(inputDelayTimer);
inputDelayTimer = LK.setTimeout(function () {
inputDelayActive = false;
inputDelayTimer = null;
}, inputDelayMs);
}
};
btnDown.down = function (x, y, obj) {
if (!player.moving && player.alive && !inputDelayActive && player.row < 1) {
movePlayerTo(player.lane, player.row + 1);
inputDelayActive = true;
if (inputDelayTimer !== null) LK.clearTimeout(inputDelayTimer);
inputDelayTimer = LK.setTimeout(function () {
inputDelayActive = false;
inputDelayTimer = null;
}, inputDelayMs);
}
};
btnLeft.down = function (x, y, obj) {
if (!player.moving && player.alive && !inputDelayActive && player.lane > 0) {
movePlayerTo(player.lane - 1, player.row);
inputDelayActive = true;
if (inputDelayTimer !== null) LK.clearTimeout(inputDelayTimer);
inputDelayTimer = LK.setTimeout(function () {
inputDelayActive = false;
inputDelayTimer = null;
}, inputDelayMs);
}
};
btnRight.down = function (x, y, obj) {
if (!player.moving && player.alive && !inputDelayActive && player.lane < 2) {
movePlayerTo(player.lane + 1, player.row);
inputDelayActive = true;
if (inputDelayTimer !== null) LK.clearTimeout(inputDelayTimer);
inputDelayTimer = LK.setTimeout(function () {
inputDelayActive = false;
inputDelayTimer = null;
}, inputDelayMs);
}
};
// --- Projectiles array ---
var projectiles = [];
// --- Shoot button event handler ---
btnShoot.down = function (x, y, obj) {
if (!player.alive) return;
// Create a new projectile at the player's current lane/row, just in front of the player
var proj = new Projectile();
proj.lane = player.lane;
proj.row = player.row;
proj.depth = 0.7; // just in front of player
proj.lastDepth = proj.depth;
projectiles.push(proj);
game.addChild(proj);
};
// Disable tap-to-move and swipe
game.down = function (x, y, obj) {};
game.move = function (x, y, obj) {};
game.up = function (x, y, obj) {};
// --- Move player to lane with tween ---
function movePlayerTo(targetLane, targetRow) {
if (player.moving || !player.alive) return;
if (typeof targetRow === "undefined") targetRow = player.row;
player.moveTarget = targetLane;
player.moveRowTarget = targetRow;
player.moving = true;
// Activate input delay immediately on move
inputDelayActive = true;
if (inputDelayTimer !== null) LK.clearTimeout(inputDelayTimer);
inputDelayTimer = LK.setTimeout(function () {
inputDelayActive = false;
inputDelayTimer = null;
}, inputDelayMs);
var startLane = player.lane;
var endLane = targetLane;
var startRow = player.row;
var endRow = targetRow;
var t = {
v: startLane
};
var tr = {
v: startRow
};
player.moveTween = tween(t, {
v: endLane
}, {
duration: 180,
easing: tween.cubicOut,
onFinish: function onFinish() {
player.lane = endLane;
player.moving = false;
// If a move was queued during the tween, perform it now
if (typeof player.queuedMove !== "undefined" && player.queuedMove !== null && (player.queuedMove !== player.lane || player.queuedRowMove !== player.row)) {
var nextLane = player.queuedMove;
var nextRow = player.queuedRowMove;
player.queuedMove = null;
player.queuedRowMove = null;
movePlayerTo(nextLane, nextRow);
}
}
});
player.moveRowTween = tween(tr, {
v: endRow
}, {
duration: 180,
easing: tween.cubicOut,
onFinish: function onFinish() {
player.row = endRow;
}
});
// We'll interpolate player.lane and player.row visually in render
player.laneTweenObj = t;
player.rowTweenObj = tr;
}
// --- Main update loop ---
game.update = function () {
// --- Scroll background ---
var bgScrollSpeed = speed * 2732 * 0.18; // scale to match corridor speed visually
bg1.y += bgScrollSpeed;
bg2.y += bgScrollSpeed;
if (bg1.y >= 2732) bg1.y = bg2.y - 2732;
if (bg2.y >= 2732) bg2.y = bg1.y - 2732;
if (!player.alive) return;
// Increase speed over time, and also based on collectibles collected (score)
var speedFromCollectibles = 0.018 * score; // Each collectible increases speed by 0.018
speed += speedIncrease;
speed = 0.18 + speedFromCollectibles + (speed - 0.18); // base + collectibles + time
if (speed > maxSpeed) speed = maxSpeed;
distance += speed;
distTxt.setText(Math.floor(distance) + "m");
// Spawn new objects
spawnTimer--;
if (spawnTimer <= 0) {
spawnObject();
spawnTimer = spawnInterval - Math.floor(distance / 60);
if (spawnTimer < 18) spawnTimer = 18;
}
// Move objects closer (decrease depth)
for (var i = objects.length - 1; i >= 0; i--) {
var obj = objects[i];
obj.depth -= speed;
if (obj.depth < 0.2) {
// Passed player, remove
obj.destroy();
objects.splice(i, 1);
continue;
}
// Project to screen
var p = projectZ(obj.depth);
obj.scaleX = obj.scaleY = p.scale * 1.2;
if (typeof obj.row === "undefined") obj.row = 1;
obj.x = laneX(obj.lane, obj.depth);
obj.y = p.y + 320 * p.scale + laneY(obj.row, obj.depth);
obj.visible = obj.depth > 0.2 && obj.depth < corridor.maxDepth + 2;
}
// Player lane interpolation (for smooth movement)
var visLane = player.lane;
var visRow = player.row;
if (player.moving && player.laneTweenObj) {
visLane = player.laneTweenObj.v;
}
if (player.moving && player.rowTweenObj) {
visRow = player.rowTweenObj.v;
}
// --- Render corridor slices ---
for (var i = 0; i < corridor.depthSlices; i++) {
// Offset z by distance to make walls/floor/ceiling scroll opposite to objects
var z = i + 1.2 - distance % 1; // reverse scroll direction
var p = projectZ(z);
// Walls
var wallW = 80 * p.scale * 2.2;
var wallH = 800 * p.scale * 2.2;
// Left wall
var wl = wallNodes[i * 2];
wl.x = laneX(0, z) - corridor.laneWidth * p.scale / 2 - wallW / 2;
wl.y = p.y + wallH;
wl.width = wallW;
wl.height = wallH;
wl.visible = true;
// Right wall
var wr = wallNodes[i * 2 + 1];
wr.x = laneX(2, z) + corridor.laneWidth * p.scale / 2 + wallW / 2;
wr.y = p.y + wallH;
wr.width = wallW;
wr.height = wallH;
wr.visible = true;
// Floor
var fl = floorNodes[i];
fl.x = 2048 / 2;
fl.y = p.y + wallH;
fl.width = corridor.laneWidth * corridor.width * p.scale * 1.1;
fl.height = 80 * p.scale * 2.2;
fl.visible = true;
// Ceiling
var cl = ceilingNodes[i];
cl.x = 2048 / 2;
cl.y = p.y - 80 * p.scale * 1.2;
cl.width = corridor.laneWidth * corridor.width * p.scale * 1.1;
cl.height = 80 * p.scale * 2.2;
cl.visible = true;
}
// --- Collision detection ---
// Player is always at z=0, y=bottom of screen
var playerZ = 0.7;
var playerP = projectZ(playerZ);
var playerX = laneX(visLane, playerZ);
var playerY = playerP.y + 320 * playerP.scale + laneY(visRow, playerZ);
var playerW = 340 * playerP.scale * 1.2;
var playerH = 420 * playerP.scale * 1.2;
// --- Render player visual representation ---
if (typeof player.visual === "undefined") {
// Create the player visual using a new asset, anchor center
player.visual = LK.getAsset('player', {
anchorX: 0.5,
anchorY: 1
});
player.visual.width = 340;
player.visual.height = 420;
player.visual.alpha = 0.98;
game.addChild(player.visual);
}
player.visual.visible = player.alive;
player.visual.x = playerX;
player.visual.y = playerY;
player.visual.scaleX = playerW / player.visual.width;
player.visual.scaleY = playerH / player.visual.height;
// Move player visual behind the movement buttons in the display list
if (player.visual.parent && player.visual.parent.children) {
var parent = player.visual.parent;
// Remove player.visual if it exists
if (parent) {
parent.removeChild(player.visual);
// Find the lowest index of the button visuals
var btns = [btnUp, btnDown, btnLeft, btnRight];
var minBtnIdx = parent.children.length;
for (var i = 0; i < btns.length; i++) {
var idx = parent.children.indexOf(btns[i]);
if (idx !== -1 && idx < minBtnIdx) minBtnIdx = idx;
}
// Insert player.visual just before the first button, or at the end if not found
if (minBtnIdx < parent.children.length) {
parent.addChildAt(player.visual, minBtnIdx);
} else {
parent.addChild(player.visual);
}
}
}
;
// --- Projectiles update/render/collision ---
for (var pi = projectiles.length - 1; pi >= 0; pi--) {
var proj = projectiles[pi];
proj.update();
// Project to screen
var pz = projectZ(proj.depth);
proj.scaleX = proj.scaleY = pz.scale * 1.3;
proj.x = laneX(proj.lane, proj.depth);
proj.y = pz.y + 320 * pz.scale + laneY(proj.row, proj.depth);
proj.visible = proj.depth > 0.2 && proj.depth < corridor.maxDepth + 2;
// Remove projectile if out of range
if (proj.depth > corridor.maxDepth + 2) {
proj.destroy();
projectiles.splice(pi, 1);
continue;
}
// Check collision with obstacles
for (var oi = objects.length - 1; oi >= 0; oi--) {
var obj = objects[oi];
if (!obj.active) continue;
if (obj.type !== 'obstacle') continue;
// Only check collision if close enough in depth
if (Math.abs(obj.depth - proj.depth) < 0.7 && obj.lane === proj.lane && obj.row === proj.row) {
// Remove both projectile and obstacle
obj.active = false;
tween(obj, {
scaleX: 2.2,
scaleY: 2.2,
alpha: 0
}, {
duration: 180,
easing: tween.cubicOut,
onFinish: function onFinish() {
obj.destroy();
}
});
objects.splice(oi, 1);
proj.destroy();
projectiles.splice(pi, 1);
break;
}
}
}
// For debugging, could render a player shadow here
for (var i = objects.length - 1; i >= 0; i--) {
var obj = objects[i];
if (!obj.active) continue;
// Only check collision if close enough
if (obj.depth < 1.2 && obj.depth > 0.2) {
var dx = Math.abs(obj.x - playerX);
var dy = Math.abs(obj.y - playerY);
var hitW = (playerW + obj.width) / 2;
var hitH = (playerH + obj.height) / 2;
if (dx < hitW * 0.7 && dy < hitH * 0.7) {
if (obj.type === 'obstacle') {
// Hit obstacle: game over
player.alive = false;
LK.effects.flashScreen(0xff0000, 900);
LK.showGameOver();
return;
} else if (obj.type === 'collectible') {
// Collect item
obj.active = false;
score += 1;
LK.setScore(score);
scoreTxt.setText(score);
tween(obj, {
scaleX: 2.2,
scaleY: 2.2,
alpha: 0
}, {
duration: 220,
easing: tween.cubicOut,
onFinish: function onFinish() {
obj.destroy();
}
});
objects.splice(i, 1);
}
}
}
}
};
// --- Reset game state on new game ---
function resetGame() {
// Remove all objects
for (var i = 0; i < objects.length; i++) {
objects[i].destroy();
}
objects = [];
player.lane = 1;
player.row = 1;
player.moving = false;
player.moveTarget = 1;
player.moveRowTarget = 1;
player.moveTween = null;
player.moveRowTween = null;
player.queuedMove = null;
player.queuedRowMove = null;
player.alive = true;
speed = 0.18;
score = 0;
distance = 0;
spawnTimer = 18;
scoreTxt.setText('0');
distTxt.setText('0m');
// Reset input delay state
inputDelayActive = false;
if (inputDelayTimer !== null) {
LK.clearTimeout(inputDelayTimer);
inputDelayTimer = null;
}
}
resetGame();
// Play background music at game start
LK.playMusic('Music');
// --- LK handles game over and reset automatically, but listen for new game start ---
LK.on('gameStart', function () {
resetGame();
LK.playMusic('Music');
});