/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ // Block class: falling piano key var Block = Container.expand(function () { var self = Container.call(this); // Attach block asset var blockAsset = self.attachAsset('block', { anchorX: 0.5, anchorY: 0.5 }); // Track if block has been hit or missed self.hit = false; self.missed = false; // For hit effect self.showHitEffect = function () { var effect = LK.getAsset('hitEffect', { anchorX: 0.5, anchorY: 0.5, x: 0, y: 0 }); self.addChild(effect); tween(effect, { alpha: 0 }, { duration: 300, easing: tween.linear, onFinish: function onFinish() { effect.destroy(); } }); }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x181818 }); /**** * Game Code ****/ // Background asset covering the entire game area // --- FULLSCREEN BACKGROUND --- // Create a background image that covers the entire game area var background = LK.getAsset('background', { anchorX: 0, anchorY: 0, x: 0, y: 0, scaleX: 1, scaleY: 1 }); game.addChild(background); // Piano note sounds for each lane (C, D, E, G as example) // Music (looping, but we will play it once per game) // Sound for miss // Sound for block hit // Block hit effect // Target line // Falling block (piano key) // --- GAME CONSTANTS --- // Piano note sounds for each lane // Piano note sounds for each lane (C, D, E, G as example) var NUM_LANES = 4; var LANE_WIDTH = 400; // 2048/4 = 512, but leave some margin var BLOCK_WIDTH = 300; var BLOCK_HEIGHT = 120; var BLOCK_SPEED_START = 12; // px per frame (60fps) var BLOCK_SPEED_MAX = 32; var BLOCK_SPEED_INCREMENT = 0.5; // per level var BLOCK_SPAWN_INTERVAL = 48; // frames between blocks (will decrease as tempo increases) var BLOCK_SPAWN_INTERVAL_MIN = 18; var TARGET_LINE_Y = 2732 - 320; // 320px from bottom var HIT_WINDOW = 80; // px window for perfect hit var MISS_WINDOW = 120; // px window for miss // --- GAME STATE --- var blocks = []; var score = 0; var combo = 0; var bestCombo = 0; var blockSpeed = BLOCK_SPEED_START; var blockSpawnInterval = BLOCK_SPAWN_INTERVAL; var ticksSinceLastBlock = 0; var isPlaying = false; var songTicks = 0; var songLengthTicks = 60 * 60; // 60 seconds at 60fps (will end game if song ends) var nextTempoIncrease = 600; // every 10 seconds // --- LANE POSITIONS --- var laneXs = []; for (var i = 0; i < NUM_LANES; i++) { // Center blocks in each lane laneXs[i] = LANE_WIDTH / 2 + i * LANE_WIDTH + (2048 - NUM_LANES * LANE_WIDTH) / 2; } // --- GUI ELEMENTS --- // Score text var scoreTxt = new Text2('0', { size: 120, fill: 0xFFFFFF }); scoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(scoreTxt); // Combo text var comboTxt = new Text2('', { size: 80, fill: 0xFFE066 }); comboTxt.anchor.set(0.5, 0); LK.gui.top.addChild(comboTxt); comboTxt.y = 130; // Best combo text var bestComboTxt = new Text2('', { size: 60, fill: 0xCCCCCC }); bestComboTxt.anchor.set(0.5, 0); LK.gui.top.addChild(bestComboTxt); bestComboTxt.y = 210; // --- TARGET LINE --- var targetLine = LK.getAsset('targetLine', { anchorX: 0.5, // center horizontally anchorY: 0.5, // center vertically x: 2048 / 2, // center of screen y: 2732 / 2, // exactly center vertically scaleX: 1, scaleY: 1 }); game.addChild(targetLine); // Make it a long, thin horizontal blue bar (yanlamasına uzatılmış mavi çubuk) at the center of the screen targetLine.width = 2000; targetLine.height = 80; // Kalınlaştırıldı targetLine.rotation = 0; // 0 degrees, horizontal (yan) targetLine.x = 2048 / 2; targetLine.y = TARGET_LINE_Y; // targetLine.tint = 0x2196F3; // --- GAME START --- function startGame() { // Reset state for (var i = blocks.length - 1; i >= 0; i--) { blocks[i].destroy(); blocks.splice(i, 1); } score = 0; combo = 0; bestCombo = 0; blockSpeed = BLOCK_SPEED_START; blockSpawnInterval = BLOCK_SPAWN_INTERVAL; ticksSinceLastBlock = 0; isPlaying = true; songTicks = 0; nextTempoIncrease = 600; scoreTxt.setText('0'); comboTxt.setText(''); bestComboTxt.setText(''); // Start music LK.playMusic('pianoTrack', { loop: false }); } startGame(); // --- BLOCK SCHEDULING --- // For MVP, blocks are spawned randomly in lanes, but now in sync with the music beat using LK.music.getBeat API function spawnBlock() { var lane = Math.floor(Math.random() * NUM_LANES); var block = new Block(); block.x = laneXs[lane]; block.y = -BLOCK_HEIGHT / 2; block.lane = lane; block.hit = false; block.missed = false; blocks.push(block); game.addChild(block); } // --- BEAT SYNC SCHEDULING --- var lastBeat = -1; // --- GAME UPDATE --- game.update = function () { if (!isPlaying) return; songTicks++; ticksSinceLastBlock++; // Increase tempo every 10 seconds, but do it smoothly every frame for gradual acceleration if (songTicks >= nextTempoIncrease) { if (blockSpeed < BLOCK_SPEED_MAX) blockSpeed += BLOCK_SPEED_INCREMENT; if (blockSpawnInterval > BLOCK_SPAWN_INTERVAL_MIN) blockSpawnInterval--; nextTempoIncrease += 600; } // Gradually increase block speed and decrease spawn interval every frame for smooth acceleration if (blockSpeed < BLOCK_SPEED_MAX) { blockSpeed += 0.01; // very slow, smooth increase if (blockSpeed > BLOCK_SPEED_MAX) blockSpeed = BLOCK_SPEED_MAX; } if (blockSpawnInterval > BLOCK_SPAWN_INTERVAL_MIN) { blockSpawnInterval -= 0.01; if (blockSpawnInterval < BLOCK_SPAWN_INTERVAL_MIN) blockSpawnInterval = BLOCK_SPAWN_INTERVAL_MIN; } // Spawn new block exactly on music beat using LK.music.getBeat API var currentBeat = LK.music && LK.music.getBeat ? LK.music.getBeat('pianoTrack') : undefined; if (typeof currentBeat === 'number' && currentBeat !== lastBeat) { spawnBlock(); lastBeat = currentBeat; ticksSinceLastBlock = 0; } else if (ticksSinceLastBlock >= blockSpawnInterval && (!(LK.music && LK.music.getBeat) || typeof currentBeat !== 'number')) { // fallback: spawn at interval if beat API not available spawnBlock(); ticksSinceLastBlock = 0; } // Move blocks for (var i = blocks.length - 1; i >= 0; i--) { var block = blocks[i]; if (block.hit || block.missed) continue; block.y += blockSpeed; // Missed block (passed target line + window) if (block.y - TARGET_LINE_Y > MISS_WINDOW) { block.missed = true; combo = 0; comboTxt.setText(''); LK.getSound('miss').play(); // Flash screen red LK.effects.flashScreen(0xff0000, 600); // End game isPlaying = false; LK.stopMusic(); LK.showGameOver(); break; } } // End game if song ends if (songTicks > songLengthTicks) { isPlaying = false; LK.stopMusic(); LK.showYouWin(); } }; // --- INPUT HANDLING --- // Tap/click on falling block at the right time game.down = function (x, y, obj) { if (!isPlaying) return; // Only allow taps below the target line (to avoid accidental taps) if (y < TARGET_LINE_Y - BLOCK_HEIGHT) return; // Find the block closest to the target line in each lane var tapped = false; for (var i = 0; i < NUM_LANES; i++) { // Check if tap is in this lane var laneLeft = laneXs[i] - LANE_WIDTH / 2; var laneRight = laneXs[i] + LANE_WIDTH / 2; if (x >= laneLeft && x < laneRight) { // Find the first block in this lane within the hit window var bestBlock = null; var bestDist = 9999; for (var j = 0; j < blocks.length; j++) { var block = blocks[j]; if (block.lane !== i || block.hit || block.missed) continue; var dist = Math.abs(block.y - TARGET_LINE_Y); if (dist < HIT_WINDOW && dist < bestDist) { bestBlock = block; bestDist = dist; } } if (bestBlock) { // Hit! bestBlock.hit = true; tapped = true; score += 1; combo += 1; if (combo > bestCombo) bestCombo = combo; scoreTxt.setText(score + ''); comboTxt.setText(combo > 1 ? combo + ' combo!' : ''); bestComboTxt.setText('Best: ' + bestCombo); bestBlock.showHitEffect(); // Play a different piano note sound for each lane, randomize note for each tap var noteLane = bestBlock.lane; var noteOptions = ['piano_note_0', 'piano_note_1', 'piano_note_2', 'piano_note_3']; // Pick a random note different from the lane's default var noteId = noteOptions[Math.floor(Math.random() * noteOptions.length)]; LK.getSound(noteId).play(); // Animate block fade out tween(bestBlock, { alpha: 0 }, { duration: 180, onFinish: function onFinish() { bestBlock.destroy(); } }); } break; } } if (!tapped) { // Missed tap (tapped empty lane or wrong time) combo = 0; comboTxt.setText(''); LK.getSound('miss').play(); LK.effects.flashScreen(0xff0000, 400); isPlaying = false; LK.stopMusic(); LK.showGameOver(); } }; // --- GAME OVER / WIN HANDLING --- // (Handled by LK engine, game will be reset automatically) // --- MUSIC RESTART ON GAME RESET --- LK.on('gameReset', function () { startGame(); });
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Block class: falling piano key
var Block = Container.expand(function () {
var self = Container.call(this);
// Attach block asset
var blockAsset = self.attachAsset('block', {
anchorX: 0.5,
anchorY: 0.5
});
// Track if block has been hit or missed
self.hit = false;
self.missed = false;
// For hit effect
self.showHitEffect = function () {
var effect = LK.getAsset('hitEffect', {
anchorX: 0.5,
anchorY: 0.5,
x: 0,
y: 0
});
self.addChild(effect);
tween(effect, {
alpha: 0
}, {
duration: 300,
easing: tween.linear,
onFinish: function onFinish() {
effect.destroy();
}
});
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x181818
});
/****
* Game Code
****/
// Background asset covering the entire game area
// --- FULLSCREEN BACKGROUND ---
// Create a background image that covers the entire game area
var background = LK.getAsset('background', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
scaleX: 1,
scaleY: 1
});
game.addChild(background);
// Piano note sounds for each lane (C, D, E, G as example)
// Music (looping, but we will play it once per game)
// Sound for miss
// Sound for block hit
// Block hit effect
// Target line
// Falling block (piano key)
// --- GAME CONSTANTS ---
// Piano note sounds for each lane
// Piano note sounds for each lane (C, D, E, G as example)
var NUM_LANES = 4;
var LANE_WIDTH = 400; // 2048/4 = 512, but leave some margin
var BLOCK_WIDTH = 300;
var BLOCK_HEIGHT = 120;
var BLOCK_SPEED_START = 12; // px per frame (60fps)
var BLOCK_SPEED_MAX = 32;
var BLOCK_SPEED_INCREMENT = 0.5; // per level
var BLOCK_SPAWN_INTERVAL = 48; // frames between blocks (will decrease as tempo increases)
var BLOCK_SPAWN_INTERVAL_MIN = 18;
var TARGET_LINE_Y = 2732 - 320; // 320px from bottom
var HIT_WINDOW = 80; // px window for perfect hit
var MISS_WINDOW = 120; // px window for miss
// --- GAME STATE ---
var blocks = [];
var score = 0;
var combo = 0;
var bestCombo = 0;
var blockSpeed = BLOCK_SPEED_START;
var blockSpawnInterval = BLOCK_SPAWN_INTERVAL;
var ticksSinceLastBlock = 0;
var isPlaying = false;
var songTicks = 0;
var songLengthTicks = 60 * 60; // 60 seconds at 60fps (will end game if song ends)
var nextTempoIncrease = 600; // every 10 seconds
// --- LANE POSITIONS ---
var laneXs = [];
for (var i = 0; i < NUM_LANES; i++) {
// Center blocks in each lane
laneXs[i] = LANE_WIDTH / 2 + i * LANE_WIDTH + (2048 - NUM_LANES * LANE_WIDTH) / 2;
}
// --- GUI ELEMENTS ---
// Score text
var scoreTxt = new Text2('0', {
size: 120,
fill: 0xFFFFFF
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// Combo text
var comboTxt = new Text2('', {
size: 80,
fill: 0xFFE066
});
comboTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(comboTxt);
comboTxt.y = 130;
// Best combo text
var bestComboTxt = new Text2('', {
size: 60,
fill: 0xCCCCCC
});
bestComboTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(bestComboTxt);
bestComboTxt.y = 210;
// --- TARGET LINE ---
var targetLine = LK.getAsset('targetLine', {
anchorX: 0.5,
// center horizontally
anchorY: 0.5,
// center vertically
x: 2048 / 2,
// center of screen
y: 2732 / 2,
// exactly center vertically
scaleX: 1,
scaleY: 1
});
game.addChild(targetLine);
// Make it a long, thin horizontal blue bar (yanlamasına uzatılmış mavi çubuk) at the center of the screen
targetLine.width = 2000;
targetLine.height = 80; // Kalınlaştırıldı
targetLine.rotation = 0; // 0 degrees, horizontal (yan)
targetLine.x = 2048 / 2;
targetLine.y = TARGET_LINE_Y;
// targetLine.tint = 0x2196F3;
// --- GAME START ---
function startGame() {
// Reset state
for (var i = blocks.length - 1; i >= 0; i--) {
blocks[i].destroy();
blocks.splice(i, 1);
}
score = 0;
combo = 0;
bestCombo = 0;
blockSpeed = BLOCK_SPEED_START;
blockSpawnInterval = BLOCK_SPAWN_INTERVAL;
ticksSinceLastBlock = 0;
isPlaying = true;
songTicks = 0;
nextTempoIncrease = 600;
scoreTxt.setText('0');
comboTxt.setText('');
bestComboTxt.setText('');
// Start music
LK.playMusic('pianoTrack', {
loop: false
});
}
startGame();
// --- BLOCK SCHEDULING ---
// For MVP, blocks are spawned randomly in lanes, but now in sync with the music beat using LK.music.getBeat API
function spawnBlock() {
var lane = Math.floor(Math.random() * NUM_LANES);
var block = new Block();
block.x = laneXs[lane];
block.y = -BLOCK_HEIGHT / 2;
block.lane = lane;
block.hit = false;
block.missed = false;
blocks.push(block);
game.addChild(block);
}
// --- BEAT SYNC SCHEDULING ---
var lastBeat = -1;
// --- GAME UPDATE ---
game.update = function () {
if (!isPlaying) return;
songTicks++;
ticksSinceLastBlock++;
// Increase tempo every 10 seconds, but do it smoothly every frame for gradual acceleration
if (songTicks >= nextTempoIncrease) {
if (blockSpeed < BLOCK_SPEED_MAX) blockSpeed += BLOCK_SPEED_INCREMENT;
if (blockSpawnInterval > BLOCK_SPAWN_INTERVAL_MIN) blockSpawnInterval--;
nextTempoIncrease += 600;
}
// Gradually increase block speed and decrease spawn interval every frame for smooth acceleration
if (blockSpeed < BLOCK_SPEED_MAX) {
blockSpeed += 0.01; // very slow, smooth increase
if (blockSpeed > BLOCK_SPEED_MAX) blockSpeed = BLOCK_SPEED_MAX;
}
if (blockSpawnInterval > BLOCK_SPAWN_INTERVAL_MIN) {
blockSpawnInterval -= 0.01;
if (blockSpawnInterval < BLOCK_SPAWN_INTERVAL_MIN) blockSpawnInterval = BLOCK_SPAWN_INTERVAL_MIN;
}
// Spawn new block exactly on music beat using LK.music.getBeat API
var currentBeat = LK.music && LK.music.getBeat ? LK.music.getBeat('pianoTrack') : undefined;
if (typeof currentBeat === 'number' && currentBeat !== lastBeat) {
spawnBlock();
lastBeat = currentBeat;
ticksSinceLastBlock = 0;
} else if (ticksSinceLastBlock >= blockSpawnInterval && (!(LK.music && LK.music.getBeat) || typeof currentBeat !== 'number')) {
// fallback: spawn at interval if beat API not available
spawnBlock();
ticksSinceLastBlock = 0;
}
// Move blocks
for (var i = blocks.length - 1; i >= 0; i--) {
var block = blocks[i];
if (block.hit || block.missed) continue;
block.y += blockSpeed;
// Missed block (passed target line + window)
if (block.y - TARGET_LINE_Y > MISS_WINDOW) {
block.missed = true;
combo = 0;
comboTxt.setText('');
LK.getSound('miss').play();
// Flash screen red
LK.effects.flashScreen(0xff0000, 600);
// End game
isPlaying = false;
LK.stopMusic();
LK.showGameOver();
break;
}
}
// End game if song ends
if (songTicks > songLengthTicks) {
isPlaying = false;
LK.stopMusic();
LK.showYouWin();
}
};
// --- INPUT HANDLING ---
// Tap/click on falling block at the right time
game.down = function (x, y, obj) {
if (!isPlaying) return;
// Only allow taps below the target line (to avoid accidental taps)
if (y < TARGET_LINE_Y - BLOCK_HEIGHT) return;
// Find the block closest to the target line in each lane
var tapped = false;
for (var i = 0; i < NUM_LANES; i++) {
// Check if tap is in this lane
var laneLeft = laneXs[i] - LANE_WIDTH / 2;
var laneRight = laneXs[i] + LANE_WIDTH / 2;
if (x >= laneLeft && x < laneRight) {
// Find the first block in this lane within the hit window
var bestBlock = null;
var bestDist = 9999;
for (var j = 0; j < blocks.length; j++) {
var block = blocks[j];
if (block.lane !== i || block.hit || block.missed) continue;
var dist = Math.abs(block.y - TARGET_LINE_Y);
if (dist < HIT_WINDOW && dist < bestDist) {
bestBlock = block;
bestDist = dist;
}
}
if (bestBlock) {
// Hit!
bestBlock.hit = true;
tapped = true;
score += 1;
combo += 1;
if (combo > bestCombo) bestCombo = combo;
scoreTxt.setText(score + '');
comboTxt.setText(combo > 1 ? combo + ' combo!' : '');
bestComboTxt.setText('Best: ' + bestCombo);
bestBlock.showHitEffect();
// Play a different piano note sound for each lane, randomize note for each tap
var noteLane = bestBlock.lane;
var noteOptions = ['piano_note_0', 'piano_note_1', 'piano_note_2', 'piano_note_3'];
// Pick a random note different from the lane's default
var noteId = noteOptions[Math.floor(Math.random() * noteOptions.length)];
LK.getSound(noteId).play();
// Animate block fade out
tween(bestBlock, {
alpha: 0
}, {
duration: 180,
onFinish: function onFinish() {
bestBlock.destroy();
}
});
}
break;
}
}
if (!tapped) {
// Missed tap (tapped empty lane or wrong time)
combo = 0;
comboTxt.setText('');
LK.getSound('miss').play();
LK.effects.flashScreen(0xff0000, 400);
isPlaying = false;
LK.stopMusic();
LK.showGameOver();
}
};
// --- GAME OVER / WIN HANDLING ---
// (Handled by LK engine, game will be reset automatically)
// --- MUSIC RESTART ON GAME RESET ---
LK.on('gameReset', function () {
startGame();
});