/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ var Column = Container.expand(function () { var self = Container.call(this); var columnGraphics = self.attachAsset('column', { anchorX: 0.5, anchorY: 0, alpha: 0.3 }); var scoreZone = self.attachAsset('scoreZone', { anchorX: 0.5, anchorY: 0.5, y: 2732 - 200, alpha: 0.4 }); self.scoreZoneY = 2732 - 200; return self; }); var Tile = Container.expand(function () { var self = Container.call(this); var tileGraphics = self.attachAsset('tile', { anchorX: 0.5, anchorY: 0.5 }); self.speed = 6; self.columnIndex = 0; self.scored = false; self.missed = false; self.update = function () { self.y += self.speed; }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x1a1a2e }); /**** * Game Code ****/ var columns = []; var tiles = []; var score = 0; var gameStarted = false; var musicDuration = 22000; // 22 seconds var gameStartTime = 0; var perfectZone = 50; var goodZone = 100; var pianoTones = ['noteA3', 'noteC4', 'noteE4', 'noteG4']; var columnDelays = [0, 50, 100, 150]; // Stagger effects by 50ms per column var lastBeatTime = 0; var beatCount = 0; var baseAnimationIntensity = 1.0; var comboCount = 0; var comboCounter = 0; // Create background overlay for rhythmic effects var backgroundOverlay = LK.getAsset('backgroundOverlay', { anchorX: 0.5, anchorY: 0.5, alpha: 0.05, x: 2048 / 2, y: 2732 / 2 }); game.addChild(backgroundOverlay); // Create back gray overlay for beat animation var backgrayoverlay = LK.getAsset('backgrayoverlay', { anchorX: 0.5, anchorY: 0.5, alpha: 0, x: 2048 / 2, y: 2732 / 2 }); game.addChild(backgrayoverlay); // Create pulse flash overlay at center, behind falling tiles var pulseFlashFull = LK.getAsset('pulseFlash1', { anchorX: 0.5, anchorY: 0.5, alpha: 0, scaleX: 2048 / 100, // Scale to screen width (2048px / 100px asset) scaleY: 2732 / 100, // Scale to screen height (2732px / 100px asset) x: 2048 / 2, y: 2732 / 2 }); game.addChild(pulseFlashFull); // Create UI var scoreTxt = new Text2('Score: 0', { size: 120, fill: 0xFFFFFF }); scoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(scoreTxt); // Create combo display var comboDisplay = new Text2('', { size: 60, fill: 0xFFD700, fontWeight: 'bold' }); comboDisplay.anchor.set(1, 0); comboDisplay.visible = false; LK.gui.topRight.addChild(comboDisplay); // Position relative to topRight anchor comboDisplay.x = -80; comboDisplay.y = 40; // Create columns var columnWidth = 2048 / 4; for (var i = 0; i < 4; i++) { var column = new Column(); column.x = i * columnWidth + columnWidth / 2; column.y = 0; columns.push(column); game.addChild(column); } // Multiple predefined melody patterns for variety var melodyPatterns = [[1, 1, 2, 2, 3, 3, 2, 2, 1], // Pattern A: C4, C4, E4, E4, G4, G4, E4, E4, C4 [3, 2, 1, 1, 0, 0, 1, 2], // Pattern B: G4, E4, C4, C4, A3, A3, C4, E4 [0, 2, 1, 3, 0, 2, 1, 3], // Pattern C: A3, E4, C4, G4, A3, E4, C4, G4 [3, 1, 3, 1, 2, 2, 0, 0] // Pattern D: G4, C4, G4, C4, E4, E4, A3, A3 ]; var currentPatternIndex = 0; var melodyPattern = melodyPatterns[currentPatternIndex]; var spawnPattern = []; var beatInterval = 500; // 500ms per beat for steady rhythm var patternRepeatDuration = melodyPattern.length * beatInterval; // 9 beats = 4500ms per pattern // Generate spawn pattern by repeating the melody pattern throughout the game duration var currentTime = 500; // Start after 500ms var patternPosition = 0; while (currentTime < musicDuration) { spawnPattern.push({ time: currentTime, columns: [melodyPattern[patternPosition]] }); currentTime += beatInterval; patternPosition = (patternPosition + 1) % melodyPattern.length; } var patternIndex = 0; function spawnTile(columnIndex) { var tile = new Tile(); tile.columnIndex = columnIndex; tile.x = columns[columnIndex].x; tile.y = -60; tiles.push(tile); game.addChild(tile); } function calculateScore(distance) { if (distance <= perfectZone) { return 100; } else if (distance <= goodZone) { return 50; } return 0; } function updateScore(points) { score += points; scoreTxt.setText('Score: ' + score); } function triggerBeatAnimation() { beatCount++; // Calculate animation intensity based on score milestones var scoreMultiplier = 1.0; if (score >= 5000) { scoreMultiplier = 2.0; } else if (score >= 2500) { scoreMultiplier = 1.7; } else if (score >= 1000) { scoreMultiplier = 1.4; } else if (score >= 500) { scoreMultiplier = 1.2; } var animationIntensity = baseAnimationIntensity * scoreMultiplier; // Background pulse - scale and brightness var pulseScale = 1.0 + 0.02 * animationIntensity; // 2% scale increase max var pulseAlpha = 0.05 + 0.01 * animationIntensity; // 1% brightness increase max tween(backgroundOverlay, { scaleX: pulseScale, scaleY: pulseScale, alpha: pulseAlpha }, { duration: 100, easing: tween.easeOut, onFinish: function onFinish() { tween(backgroundOverlay, { scaleX: 1.0, scaleY: 1.0, alpha: 0.05 }, { duration: 200, easing: tween.easeIn }); } }); // Pulse flash animation - fade in then out on every beat tween(pulseFlashFull, { alpha: 0.35 }, { duration: 0, onFinish: function onFinish() { tween(pulseFlashFull, { alpha: 0 }, { duration: 300, easing: tween.easeOut }); } }); // Subtle rotation effect that increases with score var rotationAmount = beatCount % 8 * 0.01 * animationIntensity; // Very subtle rotation tween(backgroundOverlay, { rotation: rotationAmount }, { duration: 300, easing: tween.easeInOut }); // Back gray overlay beat animation - fade in to 0.35 then out over 300ms tween(backgrayoverlay, { alpha: 0.35 }, { duration: 0, onFinish: function onFinish() { tween(backgrayoverlay, { alpha: 0 }, { duration: 300, easing: tween.easeOut }); } }); } game.down = function (x, y, obj) { if (!gameStarted) { gameStarted = true; gameStartTime = LK.ticks * (1000 / 60); LK.playMusic('gamesound3_backing', { loop: true }); return; } // Determine which column was tapped var columnIndex = Math.floor(x / columnWidth); if (columnIndex < 0) { columnIndex = 0; } if (columnIndex > 3) { columnIndex = 3; } var hitTile = null; var bestDistance = Infinity; // Find the closest tile in the tapped column within scoring range for (var i = 0; i < tiles.length; i++) { var tile = tiles[i]; if (tile.columnIndex === columnIndex && !tile.scored && !tile.missed) { var scoreZoneY = columns[columnIndex].scoreZoneY; var distance = Math.abs(tile.y - scoreZoneY); if (distance <= goodZone && distance < bestDistance) { bestDistance = distance; hitTile = tile; } } } if (hitTile) { hitTile.scored = true; var points = calculateScore(bestDistance); updateScore(points); // Increment combo count comboCount++; comboCounter++; // Update combo display if (comboCounter < 2) { comboDisplay.visible = false; } else { comboDisplay.visible = true; comboDisplay.setText('Combo x' + comboCounter); // Check for color change and scaling on multiples of 10 if (comboCounter % 10 === 0 && comboCounter > 0) { // Change color to bright red temporarily comboDisplay.tint = 0xFF4444; // Scale animation - larger scale for more dramatic effect tween(comboDisplay, { scaleX: 1.5, scaleY: 1.5 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { tween(comboDisplay, { scaleX: 1.0, scaleY: 1.0 }, { duration: 400, easing: tween.easeInOut }); } }); // Reset color after 600ms LK.setTimeout(function () { comboDisplay.tint = 0xFFD700; }, 600); } } // Check for 10-hit combo streak if (comboCount % 10 === 0) { // Spawn 'comboGlow' behind tiles with opacity 0.5 and fade it out over 400ms var comboGlow = LK.getAsset('comboGlow', { anchorX: 0.5, anchorY: 0.5, alpha: 0.5, x: 2048 / 2, y: 2732 / 2 }); game.addChild(comboGlow); // Fade out comboGlow over 400ms tween(comboGlow, { alpha: 0 }, { duration: 400, easing: tween.easeOut, onFinish: function onFinish() { comboGlow.destroy(); } }); // Spawn 'comboText' at (screen center X, 100px from top) var comboText = new Text2('Combo x' + comboCount + '!', { size: 200, fill: 0xFFD700 }); comboText.anchor.set(0.5, 0.5); comboText.x = 2048 / 2; comboText.y = 280; // 100px from top comboText.alpha = 0; comboText.scaleX = 0.8; comboText.scaleY = 0.8; game.addChild(comboText); // Fade in over 150ms with scaling popping effect tween(comboText, { alpha: 1, scaleX: 1.2, scaleY: 1.2 }, { duration: 150, easing: tween.easeOut, onFinish: function onFinish() { // Hold for 400ms, then fade out over 300ms LK.setTimeout(function () { tween(comboText, { alpha: 0, scaleX: 1.0, scaleY: 1.0 }, { duration: 300, easing: tween.easeOut, onFinish: function onFinish() { comboText.destroy(); } }); }, 400); } }); } // Create color burst effect with bright, saturated colors for each column var burstColors = [0x9932CC, 0x0000FF, 0xFFFF00, 0xFF0000]; // Purple, Blue, Yellow, Red var burstColor = burstColors[columnIndex]; var columnDelay = columnDelays[columnIndex]; // Create column light-up effect LK.setTimeout(function () { var columnOverlay = LK.getAsset('columnOverlay', { anchorX: 0.5, anchorY: 0, tint: burstColor, alpha: 0.4, x: columns[columnIndex].x, y: 0 }); game.addChild(columnOverlay); // Animate the column overlay with pulse effect tween(columnOverlay, { alpha: 0 }, { duration: 350, easing: tween.easeOut, onFinish: function onFinish() { columnOverlay.destroy(); } }); }, columnDelay); // Spawn 'burst1' at the tile's position - limited to 1.5x tile size with vivid colors LK.setTimeout(function () { var burst1 = LK.getAsset('brust1', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.6, scaleY: 0.6, tint: burstColor, alpha: 0.9, x: hitTile.x, y: hitTile.y }); game.addChild(burst1); tween(burst1, { scaleX: 1.8, // Limited to 1.5x tile size (400px * 1.5 = 600px, so scale ~1.8 for 100px asset) scaleY: 1.8, alpha: 0 }, { duration: 350, easing: tween.easeOut, onFinish: function onFinish() { burst1.destroy(); } }); }, columnDelay); // Overlay 'glowring1' with matching column color and radial gradient effect LK.setTimeout(function () { var glowRing = LK.getAsset('glowring1', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.8, scaleY: 0.8, tint: burstColor, alpha: 0.8, x: hitTile.x, y: hitTile.y }); game.addChild(glowRing); tween(glowRing, { scaleX: 1.8, // Limited to 1.5x tile size scaleY: 1.8, alpha: 0 }, { duration: 380, easing: tween.easeInOut, onFinish: function onFinish() { glowRing.destroy(); } }); }, columnDelay + 25); // Create spark line animation near the tile LK.setTimeout(function () { for (var s = 0; s < 4; s++) { var sparkLine = LK.getAsset('sparkline', { anchorX: 0.5, anchorY: 0.5, tint: burstColor, alpha: 0.9, x: hitTile.x + (Math.random() - 0.5) * 100, y: hitTile.y + (Math.random() - 0.5) * 100, rotation: Math.random() * Math.PI * 2 }); game.addChild(sparkLine); tween(sparkLine, { scaleX: 2, scaleY: 0.2, alpha: 0, rotation: sparkLine.rotation + Math.PI }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { sparkLine.destroy(); } }); } }, columnDelay + 50); // Emit 5-6 small 'trail1' particles radiating outward with small movement radius LK.setTimeout(function () { var numTrails = 5 + Math.floor(Math.random() * 2); // 5-6 particles for (var t = 0; t < numTrails; t++) { var trail = LK.getAsset('trail1', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.3, scaleY: 0.3, tint: burstColor, alpha: 0.8, x: hitTile.x, y: hitTile.y }); game.addChild(trail); var angle = t / numTrails * Math.PI * 2 + Math.random() * 0.3; var distance = 40 + Math.random() * 40; // Small movement radius within 80px var targetX = hitTile.x + Math.cos(angle) * distance; var targetY = hitTile.y + Math.sin(angle) * distance; tween(trail, { x: targetX, y: targetY, alpha: 0, scaleX: 0.1, scaleY: 0.1 }, { duration: 300, easing: tween.easeOut, onFinish: function onFinish() { trail.destroy(); } }); } }, columnDelay + 75); // Spawn columnFlash overlay at tile position with upward then downward animation var columnFlashAssets = ['columnFlash1', 'columnFlash2', 'columnFlash3', 'columnFlash4']; var originalY = hitTile.y; var columnFlash = LK.getAsset(columnFlashAssets[columnIndex], { anchorX: 0.5, anchorY: 0.5, alpha: 1.0, x: hitTile.x, y: originalY }); game.addChild(columnFlash); // Animate upward movement over 150ms with ease-out tween(columnFlash, { y: originalY - 100 }, { duration: 150, easing: tween.easeOut, onFinish: function onFinish() { // Animate downward movement back to original position over 150ms with ease-in tween(columnFlash, { y: originalY }, { duration: 150, easing: tween.easeIn, onFinish: function onFinish() { columnFlash.destroy(); } }); } }); // Fade out overlay over full 300ms duration tween(columnFlash, { alpha: 0 }, { duration: 300, easing: tween.easeOut }); // Visual feedback for scoring if (points === 100) { LK.effects.flashObject(hitTile, 0x00ff00, 200); } else if (points === 50) { LK.effects.flashObject(hitTile, 0xffff00, 200); } LK.getSound(pianoTones[columnIndex]).play(); // Remove tile hitTile.destroy(); for (var j = tiles.length - 1; j >= 0; j--) { if (tiles[j] === hitTile) { tiles.splice(j, 1); break; } } } }; game.update = function () { if (!gameStarted) { return; } var currentTime = LK.ticks * (1000 / 60) - gameStartTime; // Beat detection for background animation var currentBeatTime = Math.floor(currentTime / beatInterval) * beatInterval; if (currentBeatTime !== lastBeatTime && currentTime >= 500) { lastBeatTime = currentBeatTime; triggerBeatAnimation(); } // Spawn tiles according to endless repeating pattern with pattern cycling and controlled randomness var patternTime = currentTime % patternRepeatDuration; // Loop the pattern timing var shouldSpawn = false; // Check if we need to switch to the next pattern var patternCycleTime = Math.floor(currentTime / patternRepeatDuration); var newPatternIndex = patternCycleTime % melodyPatterns.length; if (newPatternIndex !== currentPatternIndex) { currentPatternIndex = newPatternIndex; melodyPattern = melodyPatterns[currentPatternIndex]; } for (var p = 0; p < melodyPattern.length; p++) { var spawnTime = p * beatInterval + 500; // Add initial 500ms offset if (Math.abs(patternTime - spawnTime) < 16) { // 16ms tolerance for 60fps var targetColumn = melodyPattern[p]; var finalColumn = targetColumn; // Add controlled randomness - 20% chance to vary the column if (Math.random() < 0.2) { // Define harmonic variations for each base column var harmonicVariations = [[0, 1], // A3 can vary to C4 [1, 2], // C4 can vary to E4 [2, 3, 1], // E4 can vary to G4 or back to C4 [3, 2] // G4 can vary to E4 ]; var variations = harmonicVariations[targetColumn]; if (variations && variations.length > 0) { finalColumn = variations[Math.floor(Math.random() * variations.length)]; } } spawnTile(finalColumn); shouldSpawn = true; break; } } // Update tiles and check for misses for (var i = tiles.length - 1; i >= 0; i--) { var tile = tiles[i]; // Check if tile passed the scoring zone without being hit if (!tile.scored && !tile.missed && tile.y > columns[tile.columnIndex].scoreZoneY + goodZone) { tile.missed = true; comboCount = 0; // Reset combo on miss comboCounter = 0; // Reset combo counter on miss // Update combo display visibility and reset properties comboDisplay.visible = false; comboDisplay.tint = 0xFFD700; comboDisplay.scaleX = 1.0; comboDisplay.scaleY = 1.0; LK.effects.flashObject(tile, 0xff0000, 300); LK.getSound('miss').play(); } // Remove tiles that are off screen if (tile.y > 2732 + 100) { tile.destroy(); tiles.splice(i, 1); } } };
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var Column = Container.expand(function () {
var self = Container.call(this);
var columnGraphics = self.attachAsset('column', {
anchorX: 0.5,
anchorY: 0,
alpha: 0.3
});
var scoreZone = self.attachAsset('scoreZone', {
anchorX: 0.5,
anchorY: 0.5,
y: 2732 - 200,
alpha: 0.4
});
self.scoreZoneY = 2732 - 200;
return self;
});
var Tile = Container.expand(function () {
var self = Container.call(this);
var tileGraphics = self.attachAsset('tile', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 6;
self.columnIndex = 0;
self.scored = false;
self.missed = false;
self.update = function () {
self.y += self.speed;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x1a1a2e
});
/****
* Game Code
****/
var columns = [];
var tiles = [];
var score = 0;
var gameStarted = false;
var musicDuration = 22000; // 22 seconds
var gameStartTime = 0;
var perfectZone = 50;
var goodZone = 100;
var pianoTones = ['noteA3', 'noteC4', 'noteE4', 'noteG4'];
var columnDelays = [0, 50, 100, 150]; // Stagger effects by 50ms per column
var lastBeatTime = 0;
var beatCount = 0;
var baseAnimationIntensity = 1.0;
var comboCount = 0;
var comboCounter = 0;
// Create background overlay for rhythmic effects
var backgroundOverlay = LK.getAsset('backgroundOverlay', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.05,
x: 2048 / 2,
y: 2732 / 2
});
game.addChild(backgroundOverlay);
// Create back gray overlay for beat animation
var backgrayoverlay = LK.getAsset('backgrayoverlay', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0,
x: 2048 / 2,
y: 2732 / 2
});
game.addChild(backgrayoverlay);
// Create pulse flash overlay at center, behind falling tiles
var pulseFlashFull = LK.getAsset('pulseFlash1', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0,
scaleX: 2048 / 100,
// Scale to screen width (2048px / 100px asset)
scaleY: 2732 / 100,
// Scale to screen height (2732px / 100px asset)
x: 2048 / 2,
y: 2732 / 2
});
game.addChild(pulseFlashFull);
// Create UI
var scoreTxt = new Text2('Score: 0', {
size: 120,
fill: 0xFFFFFF
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// Create combo display
var comboDisplay = new Text2('', {
size: 60,
fill: 0xFFD700,
fontWeight: 'bold'
});
comboDisplay.anchor.set(1, 0);
comboDisplay.visible = false;
LK.gui.topRight.addChild(comboDisplay);
// Position relative to topRight anchor
comboDisplay.x = -80;
comboDisplay.y = 40;
// Create columns
var columnWidth = 2048 / 4;
for (var i = 0; i < 4; i++) {
var column = new Column();
column.x = i * columnWidth + columnWidth / 2;
column.y = 0;
columns.push(column);
game.addChild(column);
}
// Multiple predefined melody patterns for variety
var melodyPatterns = [[1, 1, 2, 2, 3, 3, 2, 2, 1],
// Pattern A: C4, C4, E4, E4, G4, G4, E4, E4, C4
[3, 2, 1, 1, 0, 0, 1, 2],
// Pattern B: G4, E4, C4, C4, A3, A3, C4, E4
[0, 2, 1, 3, 0, 2, 1, 3],
// Pattern C: A3, E4, C4, G4, A3, E4, C4, G4
[3, 1, 3, 1, 2, 2, 0, 0] // Pattern D: G4, C4, G4, C4, E4, E4, A3, A3
];
var currentPatternIndex = 0;
var melodyPattern = melodyPatterns[currentPatternIndex];
var spawnPattern = [];
var beatInterval = 500; // 500ms per beat for steady rhythm
var patternRepeatDuration = melodyPattern.length * beatInterval; // 9 beats = 4500ms per pattern
// Generate spawn pattern by repeating the melody pattern throughout the game duration
var currentTime = 500; // Start after 500ms
var patternPosition = 0;
while (currentTime < musicDuration) {
spawnPattern.push({
time: currentTime,
columns: [melodyPattern[patternPosition]]
});
currentTime += beatInterval;
patternPosition = (patternPosition + 1) % melodyPattern.length;
}
var patternIndex = 0;
function spawnTile(columnIndex) {
var tile = new Tile();
tile.columnIndex = columnIndex;
tile.x = columns[columnIndex].x;
tile.y = -60;
tiles.push(tile);
game.addChild(tile);
}
function calculateScore(distance) {
if (distance <= perfectZone) {
return 100;
} else if (distance <= goodZone) {
return 50;
}
return 0;
}
function updateScore(points) {
score += points;
scoreTxt.setText('Score: ' + score);
}
function triggerBeatAnimation() {
beatCount++;
// Calculate animation intensity based on score milestones
var scoreMultiplier = 1.0;
if (score >= 5000) {
scoreMultiplier = 2.0;
} else if (score >= 2500) {
scoreMultiplier = 1.7;
} else if (score >= 1000) {
scoreMultiplier = 1.4;
} else if (score >= 500) {
scoreMultiplier = 1.2;
}
var animationIntensity = baseAnimationIntensity * scoreMultiplier;
// Background pulse - scale and brightness
var pulseScale = 1.0 + 0.02 * animationIntensity; // 2% scale increase max
var pulseAlpha = 0.05 + 0.01 * animationIntensity; // 1% brightness increase max
tween(backgroundOverlay, {
scaleX: pulseScale,
scaleY: pulseScale,
alpha: pulseAlpha
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(backgroundOverlay, {
scaleX: 1.0,
scaleY: 1.0,
alpha: 0.05
}, {
duration: 200,
easing: tween.easeIn
});
}
});
// Pulse flash animation - fade in then out on every beat
tween(pulseFlashFull, {
alpha: 0.35
}, {
duration: 0,
onFinish: function onFinish() {
tween(pulseFlashFull, {
alpha: 0
}, {
duration: 300,
easing: tween.easeOut
});
}
});
// Subtle rotation effect that increases with score
var rotationAmount = beatCount % 8 * 0.01 * animationIntensity; // Very subtle rotation
tween(backgroundOverlay, {
rotation: rotationAmount
}, {
duration: 300,
easing: tween.easeInOut
});
// Back gray overlay beat animation - fade in to 0.35 then out over 300ms
tween(backgrayoverlay, {
alpha: 0.35
}, {
duration: 0,
onFinish: function onFinish() {
tween(backgrayoverlay, {
alpha: 0
}, {
duration: 300,
easing: tween.easeOut
});
}
});
}
game.down = function (x, y, obj) {
if (!gameStarted) {
gameStarted = true;
gameStartTime = LK.ticks * (1000 / 60);
LK.playMusic('gamesound3_backing', {
loop: true
});
return;
}
// Determine which column was tapped
var columnIndex = Math.floor(x / columnWidth);
if (columnIndex < 0) {
columnIndex = 0;
}
if (columnIndex > 3) {
columnIndex = 3;
}
var hitTile = null;
var bestDistance = Infinity;
// Find the closest tile in the tapped column within scoring range
for (var i = 0; i < tiles.length; i++) {
var tile = tiles[i];
if (tile.columnIndex === columnIndex && !tile.scored && !tile.missed) {
var scoreZoneY = columns[columnIndex].scoreZoneY;
var distance = Math.abs(tile.y - scoreZoneY);
if (distance <= goodZone && distance < bestDistance) {
bestDistance = distance;
hitTile = tile;
}
}
}
if (hitTile) {
hitTile.scored = true;
var points = calculateScore(bestDistance);
updateScore(points);
// Increment combo count
comboCount++;
comboCounter++;
// Update combo display
if (comboCounter < 2) {
comboDisplay.visible = false;
} else {
comboDisplay.visible = true;
comboDisplay.setText('Combo x' + comboCounter);
// Check for color change and scaling on multiples of 10
if (comboCounter % 10 === 0 && comboCounter > 0) {
// Change color to bright red temporarily
comboDisplay.tint = 0xFF4444;
// Scale animation - larger scale for more dramatic effect
tween(comboDisplay, {
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(comboDisplay, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 400,
easing: tween.easeInOut
});
}
});
// Reset color after 600ms
LK.setTimeout(function () {
comboDisplay.tint = 0xFFD700;
}, 600);
}
}
// Check for 10-hit combo streak
if (comboCount % 10 === 0) {
// Spawn 'comboGlow' behind tiles with opacity 0.5 and fade it out over 400ms
var comboGlow = LK.getAsset('comboGlow', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.5,
x: 2048 / 2,
y: 2732 / 2
});
game.addChild(comboGlow);
// Fade out comboGlow over 400ms
tween(comboGlow, {
alpha: 0
}, {
duration: 400,
easing: tween.easeOut,
onFinish: function onFinish() {
comboGlow.destroy();
}
});
// Spawn 'comboText' at (screen center X, 100px from top)
var comboText = new Text2('Combo x' + comboCount + '!', {
size: 200,
fill: 0xFFD700
});
comboText.anchor.set(0.5, 0.5);
comboText.x = 2048 / 2;
comboText.y = 280; // 100px from top
comboText.alpha = 0;
comboText.scaleX = 0.8;
comboText.scaleY = 0.8;
game.addChild(comboText);
// Fade in over 150ms with scaling popping effect
tween(comboText, {
alpha: 1,
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 150,
easing: tween.easeOut,
onFinish: function onFinish() {
// Hold for 400ms, then fade out over 300ms
LK.setTimeout(function () {
tween(comboText, {
alpha: 0,
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
comboText.destroy();
}
});
}, 400);
}
});
}
// Create color burst effect with bright, saturated colors for each column
var burstColors = [0x9932CC, 0x0000FF, 0xFFFF00, 0xFF0000]; // Purple, Blue, Yellow, Red
var burstColor = burstColors[columnIndex];
var columnDelay = columnDelays[columnIndex];
// Create column light-up effect
LK.setTimeout(function () {
var columnOverlay = LK.getAsset('columnOverlay', {
anchorX: 0.5,
anchorY: 0,
tint: burstColor,
alpha: 0.4,
x: columns[columnIndex].x,
y: 0
});
game.addChild(columnOverlay);
// Animate the column overlay with pulse effect
tween(columnOverlay, {
alpha: 0
}, {
duration: 350,
easing: tween.easeOut,
onFinish: function onFinish() {
columnOverlay.destroy();
}
});
}, columnDelay);
// Spawn 'burst1' at the tile's position - limited to 1.5x tile size with vivid colors
LK.setTimeout(function () {
var burst1 = LK.getAsset('brust1', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.6,
scaleY: 0.6,
tint: burstColor,
alpha: 0.9,
x: hitTile.x,
y: hitTile.y
});
game.addChild(burst1);
tween(burst1, {
scaleX: 1.8,
// Limited to 1.5x tile size (400px * 1.5 = 600px, so scale ~1.8 for 100px asset)
scaleY: 1.8,
alpha: 0
}, {
duration: 350,
easing: tween.easeOut,
onFinish: function onFinish() {
burst1.destroy();
}
});
}, columnDelay);
// Overlay 'glowring1' with matching column color and radial gradient effect
LK.setTimeout(function () {
var glowRing = LK.getAsset('glowring1', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.8,
scaleY: 0.8,
tint: burstColor,
alpha: 0.8,
x: hitTile.x,
y: hitTile.y
});
game.addChild(glowRing);
tween(glowRing, {
scaleX: 1.8,
// Limited to 1.5x tile size
scaleY: 1.8,
alpha: 0
}, {
duration: 380,
easing: tween.easeInOut,
onFinish: function onFinish() {
glowRing.destroy();
}
});
}, columnDelay + 25);
// Create spark line animation near the tile
LK.setTimeout(function () {
for (var s = 0; s < 4; s++) {
var sparkLine = LK.getAsset('sparkline', {
anchorX: 0.5,
anchorY: 0.5,
tint: burstColor,
alpha: 0.9,
x: hitTile.x + (Math.random() - 0.5) * 100,
y: hitTile.y + (Math.random() - 0.5) * 100,
rotation: Math.random() * Math.PI * 2
});
game.addChild(sparkLine);
tween(sparkLine, {
scaleX: 2,
scaleY: 0.2,
alpha: 0,
rotation: sparkLine.rotation + Math.PI
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
sparkLine.destroy();
}
});
}
}, columnDelay + 50);
// Emit 5-6 small 'trail1' particles radiating outward with small movement radius
LK.setTimeout(function () {
var numTrails = 5 + Math.floor(Math.random() * 2); // 5-6 particles
for (var t = 0; t < numTrails; t++) {
var trail = LK.getAsset('trail1', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.3,
scaleY: 0.3,
tint: burstColor,
alpha: 0.8,
x: hitTile.x,
y: hitTile.y
});
game.addChild(trail);
var angle = t / numTrails * Math.PI * 2 + Math.random() * 0.3;
var distance = 40 + Math.random() * 40; // Small movement radius within 80px
var targetX = hitTile.x + Math.cos(angle) * distance;
var targetY = hitTile.y + Math.sin(angle) * distance;
tween(trail, {
x: targetX,
y: targetY,
alpha: 0,
scaleX: 0.1,
scaleY: 0.1
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
trail.destroy();
}
});
}
}, columnDelay + 75);
// Spawn columnFlash overlay at tile position with upward then downward animation
var columnFlashAssets = ['columnFlash1', 'columnFlash2', 'columnFlash3', 'columnFlash4'];
var originalY = hitTile.y;
var columnFlash = LK.getAsset(columnFlashAssets[columnIndex], {
anchorX: 0.5,
anchorY: 0.5,
alpha: 1.0,
x: hitTile.x,
y: originalY
});
game.addChild(columnFlash);
// Animate upward movement over 150ms with ease-out
tween(columnFlash, {
y: originalY - 100
}, {
duration: 150,
easing: tween.easeOut,
onFinish: function onFinish() {
// Animate downward movement back to original position over 150ms with ease-in
tween(columnFlash, {
y: originalY
}, {
duration: 150,
easing: tween.easeIn,
onFinish: function onFinish() {
columnFlash.destroy();
}
});
}
});
// Fade out overlay over full 300ms duration
tween(columnFlash, {
alpha: 0
}, {
duration: 300,
easing: tween.easeOut
});
// Visual feedback for scoring
if (points === 100) {
LK.effects.flashObject(hitTile, 0x00ff00, 200);
} else if (points === 50) {
LK.effects.flashObject(hitTile, 0xffff00, 200);
}
LK.getSound(pianoTones[columnIndex]).play();
// Remove tile
hitTile.destroy();
for (var j = tiles.length - 1; j >= 0; j--) {
if (tiles[j] === hitTile) {
tiles.splice(j, 1);
break;
}
}
}
};
game.update = function () {
if (!gameStarted) {
return;
}
var currentTime = LK.ticks * (1000 / 60) - gameStartTime;
// Beat detection for background animation
var currentBeatTime = Math.floor(currentTime / beatInterval) * beatInterval;
if (currentBeatTime !== lastBeatTime && currentTime >= 500) {
lastBeatTime = currentBeatTime;
triggerBeatAnimation();
}
// Spawn tiles according to endless repeating pattern with pattern cycling and controlled randomness
var patternTime = currentTime % patternRepeatDuration; // Loop the pattern timing
var shouldSpawn = false;
// Check if we need to switch to the next pattern
var patternCycleTime = Math.floor(currentTime / patternRepeatDuration);
var newPatternIndex = patternCycleTime % melodyPatterns.length;
if (newPatternIndex !== currentPatternIndex) {
currentPatternIndex = newPatternIndex;
melodyPattern = melodyPatterns[currentPatternIndex];
}
for (var p = 0; p < melodyPattern.length; p++) {
var spawnTime = p * beatInterval + 500; // Add initial 500ms offset
if (Math.abs(patternTime - spawnTime) < 16) {
// 16ms tolerance for 60fps
var targetColumn = melodyPattern[p];
var finalColumn = targetColumn;
// Add controlled randomness - 20% chance to vary the column
if (Math.random() < 0.2) {
// Define harmonic variations for each base column
var harmonicVariations = [[0, 1],
// A3 can vary to C4
[1, 2],
// C4 can vary to E4
[2, 3, 1],
// E4 can vary to G4 or back to C4
[3, 2] // G4 can vary to E4
];
var variations = harmonicVariations[targetColumn];
if (variations && variations.length > 0) {
finalColumn = variations[Math.floor(Math.random() * variations.length)];
}
}
spawnTile(finalColumn);
shouldSpawn = true;
break;
}
}
// Update tiles and check for misses
for (var i = tiles.length - 1; i >= 0; i--) {
var tile = tiles[i];
// Check if tile passed the scoring zone without being hit
if (!tile.scored && !tile.missed && tile.y > columns[tile.columnIndex].scoreZoneY + goodZone) {
tile.missed = true;
comboCount = 0; // Reset combo on miss
comboCounter = 0; // Reset combo counter on miss
// Update combo display visibility and reset properties
comboDisplay.visible = false;
comboDisplay.tint = 0xFFD700;
comboDisplay.scaleX = 1.0;
comboDisplay.scaleY = 1.0;
LK.effects.flashObject(tile, 0xff0000, 300);
LK.getSound('miss').play();
}
// Remove tiles that are off screen
if (tile.y > 2732 + 100) {
tile.destroy();
tiles.splice(i, 1);
}
}
};