/**** * 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');
});