/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ // AimBar class (player's horizontal aim) var AimBar = Container.expand(function () { var self = Container.call(this); var aimGfx = self.attachAsset('gun', { anchorX: 0.5, anchorY: 0.5 }); self.gfx = aimGfx; return self; }); // Tile class (falling note) var Tile = Container.expand(function () { var self = Container.call(this); // Pick a random color for the tile var tileColors = [0xffe100, 0x00eaff, 0xff3b3b, 0x7cff00, 0xff00e1, 0x00ffd0, 0xffa600, 0x00aaff]; var colorIdx = Math.floor(Math.random() * tileColors.length); var tileGfx = self.attachAsset('tile', { anchorX: 0.5, anchorY: 0.5 }); tileGfx.tint = tileColors[colorIdx]; self.gfx = tileGfx; self.speed = 0; // Set per tile self.hit = false; self.update = function () { if (self.hit) return; // Store lastY for beat sync explosion if (self.lastY === undefined) self.lastY = self.y; self.y += self.speed; // --- Beat-synced explosion: if tile passes the hit line on the beat, explode even if not hit --- // (Assume beat is every FRAMES_PER_BEAT, and tickCount is global) if (!self.hit && typeof tickCount !== "undefined" && typeof FRAMES_PER_BEAT !== "undefined") { // Calculate the current beat number var beatNum = Math.floor(tickCount / FRAMES_PER_BEAT); var beatTick = beatNum * FRAMES_PER_BEAT; // If this tile just crossed the hit line and it's a beat frame, explode if (self.lastY < TILE_TARGET_Y && self.y >= TILE_TARGET_Y && tickCount === beatTick) { self.flashHit(); } } self.lastY = self.y; }; // Flash tile when hit with explosion effect self.flashHit = function () { self.hit = true; // Store original values var origScaleX = self.gfx.scaleX || 1; var origScaleY = self.gfx.scaleY || 1; var origRotation = self.gfx.rotation || 0; // Explosion effect: scale up and rotate while changing color tween(self.gfx, { tint: 0xffe100, scaleX: origScaleX * 1.5, scaleY: origScaleY * 1.5, rotation: origRotation + Math.PI * 0.25 }, { duration: 80, onFinish: function onFinish() { // Scale back down and continue rotation while changing to final color tween(self.gfx, { tint: 0x00eaff, scaleX: origScaleX * 0.8, scaleY: origScaleY * 0.8, rotation: origRotation + Math.PI * 0.5, alpha: 0.7 }, { duration: 120 }); } }); }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x0a0a1a }); /**** * Game Code ****/ // --- Add background image --- var GAME_W = 2048, GAME_H = 2732; var bg = LK.getAsset('bgimg', { anchorX: 0.5, anchorY: 0.5, scaleX: GAME_W / 2048, scaleY: GAME_H / 2732, x: GAME_W / 2, y: GAME_H / 2 }); game.addChild(bg); // --- Tiles now fall in sync with the music rhythm (BPM-based pattern) --- // --- Game constants --- // Tiles (falling notes) // Gun (player's shooter) // Bullet // Sound: gunfire // Sound: tile hit // Music: EDM track (looping) var GAME_W = 2048, GAME_H = 2732; var GUN_Y = GAME_H - 320; var TILE_ROWS = 4; // Number of tile lanes var TILE_LANE_W = GAME_W / TILE_ROWS; var TILE_SPAWN_Y = -60; var TILE_TARGET_Y = GAME_H - 520; // Where tiles should be hit var TILE_SPEED = 22; // px per frame (tuned for rhythm) var TILE_INTERVAL = 32; // frames between tiles (tuned for rhythm) var BULLET_SPEED = -60; var BULLET_FIRE_INTERVAL = 6; // frames between bullets when holding var HIT_WINDOW = 90; // px vertical window for hit var GUN_MOVE_LIMIT = 180; // px from screen edge // --- Game state --- var aimBar; var tiles = []; var score = 0; var dragAim = false; var dragOffsetX = 0; var tickCount = 0; var nextTileTick = 0; var tilePattern = []; // Array of {lane, tick} var patternIndex = 0; var gameOver = false; // --- UI --- var scoreTxt = new Text2('0', { size: 120, fill: 0xFFFFFF }); scoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(scoreTxt); // --- Music --- LK.playMusic('edm1'); // --- Helper: Generate a rhythm pattern based on music BPM --- function generatePattern() { // Example: EDM1 is 128 BPM, so 128 beats per 60 seconds = 2.133 beats/sec // 60 FPS, so 1 beat = ~28 frames var BPM = 128; var BEATS = 64; // Number of beats/tiles var FRAMES_PER_BEAT = Math.round(60 * 60 / BPM); // 60 seconds * 60 FPS / BPM tilePattern = []; // Add 1.5 second delay (90 frames at 60fps) before first tile var t = 90; // Example rhythm: every beat, random lane, but you can use a fixed pattern for more musicality for (var i = 0; i < BEATS; i++) { var lane = Math.floor(Math.random() * TILE_ROWS); tilePattern.push({ lane: lane, tick: t }); t += FRAMES_PER_BEAT; } } // --- Helper: Spawn a tile at a given lane --- function spawnTile(lane, speedup) { var tile = new Tile(); // Clamp tile X so it never gets close to the left/right edge (wider margin) // Also ensure tile never spawns outside the visible screen area, considering tile width var TILE_EDGE_MARGIN = 220; var tileHalfW = tile.gfx.width / 2; var minX = TILE_EDGE_MARGIN + tileHalfW; var maxX = GAME_W - TILE_EDGE_MARGIN - tileHalfW; var idealX = TILE_LANE_W * (lane + 0.5); tile.x = Math.max(minX, Math.min(maxX, idealX)); tile.y = TILE_SPAWN_Y; // Calculate speed so tile reaches the hit line exactly on the beat // Find the next pattern entry for this lane // We'll use the default: time from spawn to hit = FRAMES_TO_HIT var FRAMES_TO_HIT = 90; // 1.5 seconds to reach hit line (slower fall) var distance = TILE_TARGET_Y - TILE_SPAWN_Y; tile.speed = distance / FRAMES_TO_HIT * (speedup || 1); tile.lane = lane; tile.hit = false; tiles.push(tile); game.addChild(tile); } // --- Helper: Fire a bullet from gun towards a lane --- function fireBullet() { var barrel = gun.getBarrel(); var bullet = new Bullet(); bullet.x = barrel.x; bullet.y = barrel.y; bullet.speed = BULLET_SPEED; bullets.push(bullet); game.addChild(bullet); LK.getSound('gunfire').play(); } // --- Helper: Check if bullet hits a tile --- function checkBulletTileCollision(bullet, tile) { // Only check if tile not hit if (tile.hit) return false; // Check if bullet and tile are in same lane (x overlap) var dx = Math.abs(bullet.x - tile.x); if (dx > TILE_LANE_W / 2 - 30) return false; // Check if bullet is within hit window (y) var dy = Math.abs(bullet.y - tile.y); if (dy < HIT_WINDOW) return true; return false; } // --- Helper: End game --- function endGame() { if (gameOver) return; gameOver = true; LK.effects.flashScreen(0xff0033, 800); LK.showGameOver(); } // --- Helper: Win game --- function winGame() { if (gameOver) return; gameOver = true; LK.effects.flashScreen(0x00ff99, 800); LK.showYouWin(); } // --- Initialize game objects --- function resetGame() { // Remove all tiles for (var i = 0; i < tiles.length; i++) tiles[i].destroy(); tiles = []; score = 0; scoreTxt.setText('0'); tickCount = 0; nextTileTick = 0; patternIndex = 0; gameOver = false; dragAim = false; // AimBar if (aimBar) aimBar.destroy(); aimBar = new AimBar(); aimBar.x = GAME_W / 2; // Move the gun a bit higher (e.g. 200px higher than before) aimBar.y = GUN_Y - 200; game.addChild(aimBar); // Pattern generatePattern(); } resetGame(); // --- Input: Drag to move aim bar horizontally --- game.down = function (x, y, obj) { // Only allow drag if touch is on aim bar or below if (y >= aimBar.y - aimBar.gfx.height / 2 - 60) { dragAim = true; dragOffsetX = x - aimBar.x; } }; game.move = function (x, y, obj) { if (dragAim) { // Clamp aim bar movement to screen var nx = x - dragOffsetX; nx = Math.max(GUN_MOVE_LIMIT, Math.min(GAME_W - GUN_MOVE_LIMIT, nx)); aimBar.x = nx; } }; game.up = function (x, y, obj) { dragAim = false; }; // --- Main game loop --- game.update = function () { if (gameOver) return; tickCount++; // --- Tiles speed up by 0.1% per frame --- if (typeof tileSpeedup === "undefined") { var tileSpeedup = 1; } tileSpeedup *= 1.001; // 0.1% faster each frame var speedup = tileSpeedup; // --- Spawn tiles according to pattern --- // If we are close to the end of the pattern, generate more pattern for endless play if (patternIndex >= tilePattern.length - 8) { // Generate more pattern and append var oldLen = tilePattern.length; var BPM = 128; var BEATS = 32; // Add 32 more beats/tiles at a time var FRAMES_PER_BEAT = Math.round(60 * 60 / BPM); var t = tilePattern.length > 0 ? tilePattern[tilePattern.length - 1].tick + FRAMES_PER_BEAT : 90; for (var i = 0; i < BEATS; i++) { var lane = Math.floor(Math.random() * TILE_ROWS); tilePattern.push({ lane: lane, tick: t }); t += FRAMES_PER_BEAT; } } while (patternIndex < tilePattern.length && tilePattern[patternIndex].tick <= tickCount) { spawnTile(tilePattern[patternIndex].lane, speedup); patternIndex++; } // --- Move tiles and check for hits --- for (var i = tiles.length - 1; i >= 0; i--) { var tile = tiles[i]; tile.update(); // Missed tile? (passed target line) if (!tile.hit && tile.y > TILE_TARGET_Y + HIT_WINDOW) { endGame(); return; } // Remove tile if off screen if (tile.y > GAME_H + 100) { tile.destroy(); tiles.splice(i, 1); continue; } // Check for hit: if tile is within hit window and aimBar is aligned horizontally if (!tile.hit && Math.abs(tile.y - TILE_TARGET_Y) < HIT_WINDOW) { var dx = Math.abs(tile.x - aimBar.x); if (dx < TILE_LANE_W / 2 - 30) { tile.flashHit(); score += 1; scoreTxt.setText(score); // --- Rotate aimBar randomly left or right 90deg and return to original --- var origRot = aimBar.gfx.rotation || 0; var origScaleX = aimBar.gfx.scaleX || 1; var origScaleY = aimBar.gfx.scaleY || 1; var dir = Math.random() < 0.5 ? -1 : 1; tween(aimBar.gfx, { rotation: origRot + dir * (Math.PI / 2), scaleX: origScaleX * 1.1, scaleY: origScaleY * 1.1 }, { duration: 120, onFinish: function onFinish() { tween(aimBar.gfx, { rotation: origRot, scaleX: origScaleX, scaleY: origScaleY }, { duration: 120 }); } }); // Remove tile after short delay (function (tileObj) { LK.setTimeout(function () { tileObj.destroy(); var idx = tiles.indexOf(tileObj); if (idx !== -1) tiles.splice(idx, 1); }, 80); })(tile); // Endless: No win condition } } } }; // --- Reset game on game over/win --- LK.on('gameover', function () { resetGame(); }); LK.on('youwin', function () { resetGame(); });
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// AimBar class (player's horizontal aim)
var AimBar = Container.expand(function () {
var self = Container.call(this);
var aimGfx = self.attachAsset('gun', {
anchorX: 0.5,
anchorY: 0.5
});
self.gfx = aimGfx;
return self;
});
// Tile class (falling note)
var Tile = Container.expand(function () {
var self = Container.call(this);
// Pick a random color for the tile
var tileColors = [0xffe100, 0x00eaff, 0xff3b3b, 0x7cff00, 0xff00e1, 0x00ffd0, 0xffa600, 0x00aaff];
var colorIdx = Math.floor(Math.random() * tileColors.length);
var tileGfx = self.attachAsset('tile', {
anchorX: 0.5,
anchorY: 0.5
});
tileGfx.tint = tileColors[colorIdx];
self.gfx = tileGfx;
self.speed = 0; // Set per tile
self.hit = false;
self.update = function () {
if (self.hit) return;
// Store lastY for beat sync explosion
if (self.lastY === undefined) self.lastY = self.y;
self.y += self.speed;
// --- Beat-synced explosion: if tile passes the hit line on the beat, explode even if not hit ---
// (Assume beat is every FRAMES_PER_BEAT, and tickCount is global)
if (!self.hit && typeof tickCount !== "undefined" && typeof FRAMES_PER_BEAT !== "undefined") {
// Calculate the current beat number
var beatNum = Math.floor(tickCount / FRAMES_PER_BEAT);
var beatTick = beatNum * FRAMES_PER_BEAT;
// If this tile just crossed the hit line and it's a beat frame, explode
if (self.lastY < TILE_TARGET_Y && self.y >= TILE_TARGET_Y && tickCount === beatTick) {
self.flashHit();
}
}
self.lastY = self.y;
};
// Flash tile when hit with explosion effect
self.flashHit = function () {
self.hit = true;
// Store original values
var origScaleX = self.gfx.scaleX || 1;
var origScaleY = self.gfx.scaleY || 1;
var origRotation = self.gfx.rotation || 0;
// Explosion effect: scale up and rotate while changing color
tween(self.gfx, {
tint: 0xffe100,
scaleX: origScaleX * 1.5,
scaleY: origScaleY * 1.5,
rotation: origRotation + Math.PI * 0.25
}, {
duration: 80,
onFinish: function onFinish() {
// Scale back down and continue rotation while changing to final color
tween(self.gfx, {
tint: 0x00eaff,
scaleX: origScaleX * 0.8,
scaleY: origScaleY * 0.8,
rotation: origRotation + Math.PI * 0.5,
alpha: 0.7
}, {
duration: 120
});
}
});
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x0a0a1a
});
/****
* Game Code
****/
// --- Add background image ---
var GAME_W = 2048,
GAME_H = 2732;
var bg = LK.getAsset('bgimg', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: GAME_W / 2048,
scaleY: GAME_H / 2732,
x: GAME_W / 2,
y: GAME_H / 2
});
game.addChild(bg);
// --- Tiles now fall in sync with the music rhythm (BPM-based pattern) ---
// --- Game constants ---
// Tiles (falling notes)
// Gun (player's shooter)
// Bullet
// Sound: gunfire
// Sound: tile hit
// Music: EDM track (looping)
var GAME_W = 2048,
GAME_H = 2732;
var GUN_Y = GAME_H - 320;
var TILE_ROWS = 4; // Number of tile lanes
var TILE_LANE_W = GAME_W / TILE_ROWS;
var TILE_SPAWN_Y = -60;
var TILE_TARGET_Y = GAME_H - 520; // Where tiles should be hit
var TILE_SPEED = 22; // px per frame (tuned for rhythm)
var TILE_INTERVAL = 32; // frames between tiles (tuned for rhythm)
var BULLET_SPEED = -60;
var BULLET_FIRE_INTERVAL = 6; // frames between bullets when holding
var HIT_WINDOW = 90; // px vertical window for hit
var GUN_MOVE_LIMIT = 180; // px from screen edge
// --- Game state ---
var aimBar;
var tiles = [];
var score = 0;
var dragAim = false;
var dragOffsetX = 0;
var tickCount = 0;
var nextTileTick = 0;
var tilePattern = []; // Array of {lane, tick}
var patternIndex = 0;
var gameOver = false;
// --- UI ---
var scoreTxt = new Text2('0', {
size: 120,
fill: 0xFFFFFF
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// --- Music ---
LK.playMusic('edm1');
// --- Helper: Generate a rhythm pattern based on music BPM ---
function generatePattern() {
// Example: EDM1 is 128 BPM, so 128 beats per 60 seconds = 2.133 beats/sec
// 60 FPS, so 1 beat = ~28 frames
var BPM = 128;
var BEATS = 64; // Number of beats/tiles
var FRAMES_PER_BEAT = Math.round(60 * 60 / BPM); // 60 seconds * 60 FPS / BPM
tilePattern = [];
// Add 1.5 second delay (90 frames at 60fps) before first tile
var t = 90;
// Example rhythm: every beat, random lane, but you can use a fixed pattern for more musicality
for (var i = 0; i < BEATS; i++) {
var lane = Math.floor(Math.random() * TILE_ROWS);
tilePattern.push({
lane: lane,
tick: t
});
t += FRAMES_PER_BEAT;
}
}
// --- Helper: Spawn a tile at a given lane ---
function spawnTile(lane, speedup) {
var tile = new Tile();
// Clamp tile X so it never gets close to the left/right edge (wider margin)
// Also ensure tile never spawns outside the visible screen area, considering tile width
var TILE_EDGE_MARGIN = 220;
var tileHalfW = tile.gfx.width / 2;
var minX = TILE_EDGE_MARGIN + tileHalfW;
var maxX = GAME_W - TILE_EDGE_MARGIN - tileHalfW;
var idealX = TILE_LANE_W * (lane + 0.5);
tile.x = Math.max(minX, Math.min(maxX, idealX));
tile.y = TILE_SPAWN_Y;
// Calculate speed so tile reaches the hit line exactly on the beat
// Find the next pattern entry for this lane
// We'll use the default: time from spawn to hit = FRAMES_TO_HIT
var FRAMES_TO_HIT = 90; // 1.5 seconds to reach hit line (slower fall)
var distance = TILE_TARGET_Y - TILE_SPAWN_Y;
tile.speed = distance / FRAMES_TO_HIT * (speedup || 1);
tile.lane = lane;
tile.hit = false;
tiles.push(tile);
game.addChild(tile);
}
// --- Helper: Fire a bullet from gun towards a lane ---
function fireBullet() {
var barrel = gun.getBarrel();
var bullet = new Bullet();
bullet.x = barrel.x;
bullet.y = barrel.y;
bullet.speed = BULLET_SPEED;
bullets.push(bullet);
game.addChild(bullet);
LK.getSound('gunfire').play();
}
// --- Helper: Check if bullet hits a tile ---
function checkBulletTileCollision(bullet, tile) {
// Only check if tile not hit
if (tile.hit) return false;
// Check if bullet and tile are in same lane (x overlap)
var dx = Math.abs(bullet.x - tile.x);
if (dx > TILE_LANE_W / 2 - 30) return false;
// Check if bullet is within hit window (y)
var dy = Math.abs(bullet.y - tile.y);
if (dy < HIT_WINDOW) return true;
return false;
}
// --- Helper: End game ---
function endGame() {
if (gameOver) return;
gameOver = true;
LK.effects.flashScreen(0xff0033, 800);
LK.showGameOver();
}
// --- Helper: Win game ---
function winGame() {
if (gameOver) return;
gameOver = true;
LK.effects.flashScreen(0x00ff99, 800);
LK.showYouWin();
}
// --- Initialize game objects ---
function resetGame() {
// Remove all tiles
for (var i = 0; i < tiles.length; i++) tiles[i].destroy();
tiles = [];
score = 0;
scoreTxt.setText('0');
tickCount = 0;
nextTileTick = 0;
patternIndex = 0;
gameOver = false;
dragAim = false;
// AimBar
if (aimBar) aimBar.destroy();
aimBar = new AimBar();
aimBar.x = GAME_W / 2;
// Move the gun a bit higher (e.g. 200px higher than before)
aimBar.y = GUN_Y - 200;
game.addChild(aimBar);
// Pattern
generatePattern();
}
resetGame();
// --- Input: Drag to move aim bar horizontally ---
game.down = function (x, y, obj) {
// Only allow drag if touch is on aim bar or below
if (y >= aimBar.y - aimBar.gfx.height / 2 - 60) {
dragAim = true;
dragOffsetX = x - aimBar.x;
}
};
game.move = function (x, y, obj) {
if (dragAim) {
// Clamp aim bar movement to screen
var nx = x - dragOffsetX;
nx = Math.max(GUN_MOVE_LIMIT, Math.min(GAME_W - GUN_MOVE_LIMIT, nx));
aimBar.x = nx;
}
};
game.up = function (x, y, obj) {
dragAim = false;
};
// --- Main game loop ---
game.update = function () {
if (gameOver) return;
tickCount++;
// --- Tiles speed up by 0.1% per frame ---
if (typeof tileSpeedup === "undefined") {
var tileSpeedup = 1;
}
tileSpeedup *= 1.001; // 0.1% faster each frame
var speedup = tileSpeedup;
// --- Spawn tiles according to pattern ---
// If we are close to the end of the pattern, generate more pattern for endless play
if (patternIndex >= tilePattern.length - 8) {
// Generate more pattern and append
var oldLen = tilePattern.length;
var BPM = 128;
var BEATS = 32; // Add 32 more beats/tiles at a time
var FRAMES_PER_BEAT = Math.round(60 * 60 / BPM);
var t = tilePattern.length > 0 ? tilePattern[tilePattern.length - 1].tick + FRAMES_PER_BEAT : 90;
for (var i = 0; i < BEATS; i++) {
var lane = Math.floor(Math.random() * TILE_ROWS);
tilePattern.push({
lane: lane,
tick: t
});
t += FRAMES_PER_BEAT;
}
}
while (patternIndex < tilePattern.length && tilePattern[patternIndex].tick <= tickCount) {
spawnTile(tilePattern[patternIndex].lane, speedup);
patternIndex++;
}
// --- Move tiles and check for hits ---
for (var i = tiles.length - 1; i >= 0; i--) {
var tile = tiles[i];
tile.update();
// Missed tile? (passed target line)
if (!tile.hit && tile.y > TILE_TARGET_Y + HIT_WINDOW) {
endGame();
return;
}
// Remove tile if off screen
if (tile.y > GAME_H + 100) {
tile.destroy();
tiles.splice(i, 1);
continue;
}
// Check for hit: if tile is within hit window and aimBar is aligned horizontally
if (!tile.hit && Math.abs(tile.y - TILE_TARGET_Y) < HIT_WINDOW) {
var dx = Math.abs(tile.x - aimBar.x);
if (dx < TILE_LANE_W / 2 - 30) {
tile.flashHit();
score += 1;
scoreTxt.setText(score);
// --- Rotate aimBar randomly left or right 90deg and return to original ---
var origRot = aimBar.gfx.rotation || 0;
var origScaleX = aimBar.gfx.scaleX || 1;
var origScaleY = aimBar.gfx.scaleY || 1;
var dir = Math.random() < 0.5 ? -1 : 1;
tween(aimBar.gfx, {
rotation: origRot + dir * (Math.PI / 2),
scaleX: origScaleX * 1.1,
scaleY: origScaleY * 1.1
}, {
duration: 120,
onFinish: function onFinish() {
tween(aimBar.gfx, {
rotation: origRot,
scaleX: origScaleX,
scaleY: origScaleY
}, {
duration: 120
});
}
});
// Remove tile after short delay
(function (tileObj) {
LK.setTimeout(function () {
tileObj.destroy();
var idx = tiles.indexOf(tileObj);
if (idx !== -1) tiles.splice(idx, 1);
}, 80);
})(tile);
// Endless: No win condition
}
}
}
};
// --- Reset game on game over/win ---
LK.on('gameover', function () {
resetGame();
});
LK.on('youwin', function () {
resetGame();
});