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