User prompt
Make them scroll the other way
User prompt
Make the walls and ground scroll with the enemy’s and the coins
User prompt
Make more lanes
User prompt
Make the stuff all bigger
User prompt
Add better controls
Code edit (1 edits merged)
Please save this source code
User prompt
Raycast Runner
Initial prompt
Make a fake 3d game using raycasting
/**** * 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');
});