/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1"); /**** * Classes ****/ // MissNote class: visually represents a miss as a falling note var MissNote = Container.expand(function () { var self = Container.call(this); self.lane = 0; self.speed = 5; self.noteAsset = self.attachAsset('note1', { anchorX: 0.5, anchorY: 0.5, x: 0, y: 0, alpha: 0.5 }); self.update = function () { self.y += self.speed; // Remove if off screen if (self.y > 2732 + 100) { self.destroy(); if (missNotes) { for (var i = missNotes.length - 1; i >= 0; i--) { if (missNotes[i] === self) { missNotes.splice(i, 1); break; } } } } }; return self; }); // Note class var Note = Container.expand(function () { var self = Container.call(this); // lane: 0-3 self.lane = 0; self.hit = false; self.missed = false; self.speed = 4; // Will be set dynamically // Attach note asset (set in .init) self.noteAsset = null; // Feedback effect self.effect = null; // Called every tick self.update = function () { self.y += self.speed; // If note goes past hit zone and not hit, mark as missed if (!self.hit && !self.missed && self.y > hitZoneY + hitZoneHeight) { self.missed = true; showMissEffect(self); } }; // Show hit feedback self.showHit = function () { if (self.effect) return; self.effect = self.attachAsset('hitEffect', { anchorX: 0.5, anchorY: 0.5, x: 0, y: 0, alpha: 0.7 }); tween(self.effect, { alpha: 0 }, { duration: 300, onFinish: function onFinish() { if (self.effect) { self.effect.destroy(); self.effect = null; } } }); }; // Show miss feedback self.showMiss = function () { if (self.effect) return; self.effect = self.attachAsset('missEffect', { anchorX: 0.5, anchorY: 0.5, x: 0, y: 0, alpha: 0.7 }); tween(self.effect, { alpha: 0 }, { duration: 400, onFinish: function onFinish() { if (self.effect) { self.effect.destroy(); self.effect = null; } } }); }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x181818 }); /**** * Game Code ****/ // Lane setup // Four note lanes (different colors for clarity) // Hit zone highlight // Notes (different colors for each lane) // Feedback shapes // Sound effects // Music (placeholder id) var laneCount = 4; var laneWidth = 400; var laneSpacing = 32; var totalLaneWidth = laneCount * laneWidth + (laneCount - 1) * laneSpacing; var leftMargin = Math.floor((2048 - totalLaneWidth) / 2); // Hit zone setup var hitZoneHeight = 160; // Increased from 80 to 160 for higher volume var hitZoneY = 2732 - 320; // 320px from bottom // Lane X positions var laneXs = []; for (var i = 0; i < laneCount; i++) { laneXs[i] = leftMargin + i * (laneWidth + laneSpacing) + laneWidth / 2; } // Draw lanes var lanes = []; for (var i = 0; i < laneCount; i++) { var laneAssetId = 'lane' + (i + 1); var lane = LK.getAsset(laneAssetId, { anchorX: 0.5, anchorY: 0, x: laneXs[i], y: 0, width: laneWidth, height: 2200 }); game.addChild(lane); lanes.push(lane); } // Draw hit zones var hitZones = []; for (var i = 0; i < laneCount; i++) { var hitZone = LK.getAsset('hitZone', { anchorX: 0.5, anchorY: 0, x: laneXs[i], y: hitZoneY, width: laneWidth, height: hitZoneHeight, alpha: 0.12 }); game.addChild(hitZone); hitZones.push(hitZone); } // Score and combo display var score = 0; var combo = 0; var maxCombo = 0; // Top score (persistent) var topScore = storage.topScore || 0; var topScoreTxt = new Text2('Top: ' + topScore, { size: 60, fill: 0x00ffcc }); topScoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(topScoreTxt); topScoreTxt.y = 90; var scoreTxt = new Text2('Score: 0', { size: 90, fill: 0xFFFFFF }); scoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(scoreTxt); var comboTxt = new Text2('', { size: 70, fill: 0xFFFF00 }); comboTxt.anchor.set(0.5, 0); LK.gui.top.addChild(comboTxt); comboTxt.y = 110; // Miss counter display (bottom right) var missTxt = new Text2('', { size: 80, fill: 0xFF4444 }); missTxt.anchor.set(1, 1); // bottom right LK.gui.bottomRight.addChild(missTxt); missTxt.x = 0; missTxt.y = 0; // Notes array var notes = []; // Miss notes array (visuals for misses) var missNotes = []; // Song data: array of {time, lane} // For MVP, a simple hardcoded pattern (in ms, relative to song start) var songNotes = [{ time: 800, lane: 0 }, { time: 1800, lane: 1 }, { time: 2800, lane: 2 }, { time: 3800, lane: 3 }, { time: 4800, lane: 0 }, { time: 5800, lane: 1 }, { time: 6800, lane: 2 }, { time: 7800, lane: 3 }]; // Ensure no two songNotes fall in the same lane at the same time for (var i = 1; i < songNotes.length; i++) { // Check for any previous note with the same time or overlapping time window in the same lane for (var j = 0; j < i; j++) { // If the notes overlap in time and lane, change lane if (songNotes[i].lane === songNotes[j].lane && Math.abs(songNotes[i].time - songNotes[j].time) < noteTravelTime) { // Pick a different lane not used by any overlapping note var forbiddenLanes = []; for (var k = 0; k < i; k++) { if (Math.abs(songNotes[i].time - songNotes[k].time) < noteTravelTime) { forbiddenLanes.push(songNotes[k].lane); } } var availableLanes = [0, 1, 2, 3].filter(function (l) { return forbiddenLanes.indexOf(l) === -1; }); if (availableLanes.length > 0) { songNotes[i].lane = availableLanes[Math.floor(Math.random() * availableLanes.length)]; } } } } // Song parameters var songDuration = 9000; // ms var noteSpeed = 4; // px per frame (reduced for slower fall) var noteTravelTime = 1800; // ms from spawn to hit zone (increased for slower fall) // Speed scaling: increase by 10% every 500 points var noteSpeedBase = 4; var noteSpeedScale = 1; var lastSpeedScoreStep = 0; // Timing var songStartTime = null; var lastTickTime = null; var songPlaying = false; var nextNoteIndex = 0; // Miss counter var misses = 0; var maxMisses = 5; // Feedback effect for misses function showMissEffect(note) { note.showMiss(); combo = 0; updateCombo(); LK.getSound('noteMiss').play(); // Flash lane LK.effects.flashObject(lanes[note.lane], 0xff0000, 200); // Spawn a MissNote visual at the missed note's lane var missNote = new MissNote(); missNote.lane = note.lane; missNote.x = laneXs[note.lane]; missNote.y = hitZoneY + hitZoneHeight / 2; missNotes.push(missNote); game.addChild(missNote); // Miss counter and score are now handled in game.update when a miss is detected } // Feedback effect for hits function showHitEffect(note) { note.showHit(); LK.getSound('noteHit').play(); // Flash lane LK.effects.flashObject(lanes[note.lane], 0x00ff00, 120); } // Update score and combo display function updateScore() { scoreTxt.setText('Score: ' + score); if (score > topScore) { topScore = score; topScoreTxt.setText('Top: ' + topScore); storage.topScore = topScore; } } function updateCombo() { if (combo > 1) { comboTxt.setText('Combo: ' + combo); } else { comboTxt.setText(''); } } // Start song and reset state function startSong() { score = 0; combo = 0; maxCombo = 0; misses = 0; missTxt.setText('Misses: 0 / ' + maxMisses); updateScore(); updateCombo(); noteSpeedScale = 1; noteSpeed = noteSpeedBase; lastSpeedScoreStep = 0; songStartTime = Date.now(); lastTickTime = songStartTime; songPlaying = true; nextNoteIndex = 0; // Remove old notes for (var i = notes.length - 1; i >= 0; i--) { notes[i].destroy(); notes.splice(i, 1); } // Play music LK.playMusic('song1'); } // End song (win) function endSong() { songPlaying = false; LK.showYouWin(); } // End song (fail) function failSong() { songPlaying = false; LK.showGameOver(); } // Spawn a note function spawnNote(lane, spawnTime) { var note = new Note(); note.lane = lane; note.speed = noteSpeed; var noteAssetId = 'note' + (lane + 1); note.noteAsset = note.attachAsset(noteAssetId, { anchorX: 0.5, anchorY: 0.5, x: 0, y: 0 }); // X position: center of lane note.x = laneXs[lane]; // Y position: spawn above screen so it reaches hit zone at correct time note.y = getNoteSpawnY(); notes.push(note); game.addChild(note); note.spawnTime = spawnTime; // Track lastWasIntersecting for hit zone lighting note.lastWasIntersecting = false; return note; } // Calculate note spawn Y so it reaches hit zone at the right time function getNoteSpawnY() { // Distance = speed * frames // frames = noteTravelTime / (1000/60) var frames = noteTravelTime / (1000 / 60); var distance = frames * noteSpeed; // Start notes at the very top of the screen (y = 0 - note height/2) return 0 - 40; // 40 is half the note height (80) } // Find the closest note in a lane to the hit zone (not yet hit or missed) function getHittableNote(lane) { var best = null; var bestDist = 99999; for (var i = 0; i < notes.length; i++) { var note = notes[i]; if (note.lane !== lane) continue; if (note.hit || note.missed) continue; var dist = Math.abs(note.y - (hitZoneY + hitZoneHeight / 2)); if (dist < bestDist) { best = note; bestDist = dist; } } return best; } // Handle tap input function handleTap(x, y, obj) { // Only if song is playing if (!songPlaying) return; // Which lane? for (var i = 0; i < laneCount; i++) { var laneLeft = laneXs[i] - laneWidth / 2; var laneRight = laneXs[i] + laneWidth / 2; if (x >= laneLeft && x <= laneRight) { // Check for hittable note in this lane var note = getHittableNote(i); if (note) { var noteCenterY = note.y; var hitCenterY = hitZoneY + hitZoneHeight / 2; var hitWindow = hitZoneHeight / 2; // Match hit window to half the new hit zone height if (Math.abs(noteCenterY - hitCenterY) <= hitWindow) { // Hit! note.hit = true; showHitEffect(note); score += 20; combo += 1; if (combo > maxCombo) maxCombo = combo; updateScore(); updateCombo(); // Remove note after feedback tween(note, { alpha: 0 }, { duration: 120, onFinish: function onFinish() { note.destroy(); } }); return; } } // Miss (tapped but no note in window) combo = 0; updateCombo(); LK.getSound('noteMiss').play(); LK.effects.flashObject(lanes[i], 0xff0000, 120); // No miss increment or score for empty tap return; } } } // Attach tap handler game.down = function (x, y, obj) { handleTap(x, y, obj); }; // No drag or move needed for this game game.move = function (x, y, obj) {}; game.up = function (x, y, obj) {}; // Main update loop game.update = function () { if (!songPlaying) return; var now = Date.now(); var songElapsed = now - songStartTime; // Update noteSpeed scaling every frame var speedScoreStep = Math.floor(score / 500); if (speedScoreStep > lastSpeedScoreStep) { // Increase speed by 20% for each 500 points noteSpeedScale = Math.pow(1.2, speedScoreStep); noteSpeed = noteSpeedBase * noteSpeedScale; lastSpeedScoreStep = speedScoreStep; // Also update speed for all active notes for (var i = 0; i < notes.length; i++) { notes[i].speed = noteSpeed; } // And for all active miss notes for (var i = 0; i < missNotes.length; i++) { missNotes[i].speed = noteSpeed; } } // Spawn notes as their time comes while (nextNoteIndex < songNotes.length && songNotes[nextNoteIndex].time <= songElapsed + noteTravelTime) { var noteData = songNotes[nextNoteIndex]; spawnNote(noteData.lane, noteData.time); nextNoteIndex++; } // After all songNotes are spawned, keep spawning random notes forever if (nextNoteIndex >= songNotes.length) { // Use a timer to control spawn rate if (!game._lastAutoNoteTime) game._lastAutoNoteTime = now; var autoNoteInterval = 1800; // ms between random notes (increased for much less frequent falling) // Limit the number of simultaneously falling random notes dynamically based on score // For example: start at 2, increase by 1 every 1000 points, up to a max of 6 var maxSimultaneousRandomNotes = Math.min(2 + Math.floor(score / 1000), 6); var activeRandomNotes = 0; for (var j = 0; j < notes.length; j++) { if (notes[j].spawnTime > songNotes[songNotes.length - 1].time) { if (!notes[j].hit && !notes[j].missed) { activeRandomNotes++; } } } if (now - game._lastAutoNoteTime > autoNoteInterval && activeRandomNotes < maxSimultaneousRandomNotes) { // Find lanes currently occupied by random notes (not hit/missed) var occupiedLanes = []; for (var j = 0; j < notes.length; j++) { if (notes[j].spawnTime > songNotes[songNotes.length - 1].time) { if (!notes[j].hit && !notes[j].missed) { occupiedLanes.push(notes[j].lane); } } } // Find available lanes var availableLanes = []; for (var l = 0; l < laneCount; l++) { if (occupiedLanes.indexOf(l) === -1) { availableLanes.push(l); } } if (availableLanes.length > 0) { // Further filter: ensure no note (song or random) is currently falling in this lane var trulyAvailableLanes = []; for (var l = 0; l < availableLanes.length; l++) { var lane = availableLanes[l]; var laneOccupied = false; for (var n = 0; n < notes.length; n++) { if (notes[n].lane === lane && !notes[n].hit && !notes[n].missed && notes[n].y < 2732 + 100 // still on screen ) { laneOccupied = true; break; } } if (!laneOccupied) { trulyAvailableLanes.push(lane); } } if (trulyAvailableLanes.length > 0) { var randLane = trulyAvailableLanes[Math.floor(Math.random() * trulyAvailableLanes.length)]; // Randomize fall speed for this note: between 80% and 120% of current noteSpeed var randomSpeedFactor = 0.8 + Math.random() * 0.4; var prevNoteSpeed = noteSpeed; noteSpeed = noteSpeed * randomSpeedFactor; // Randomize spawn Y: allow notes to start up to 200px above or below the default spawn Y var origGetNoteSpawnY = getNoteSpawnY; getNoteSpawnY = function getNoteSpawnY() { var baseY = origGetNoteSpawnY(); return baseY + Math.floor((Math.random() - 0.5) * 400); // ±200px }; spawnNote(randLane, songElapsed); getNoteSpawnY = origGetNoteSpawnY; // Restore noteSpeed = prevNoteSpeed; // Restore global noteSpeed for other notes game._lastAutoNoteTime = now; } } } } // Update notes for (var i = notes.length - 1; i >= 0; i--) { var note = notes[i]; note.update(); // Light up hitbar when note enters hit zone if (!note.lastWasIntersecting) { // Check intersection with hit zone for this note's lane var hitZone = hitZones[note.lane]; if (note.intersects(hitZone)) { // Animate hit zone alpha up, then back down, and tint yellow var originalTint = hitZone.tint !== undefined ? hitZone.tint : 0xffffff; hitZone.tint = 0xffff00; // yellow tween(hitZone, { alpha: 0.45 }, { duration: 60, onFinish: function onFinish() { tween(hitZone, { alpha: 0.12 }, { duration: 180, onFinish: function onFinish() { hitZone.tint = originalTint; } }); } }); } } note.lastWasIntersecting = note.intersects(hitZones[note.lane]); // Remove notes that are off screen or hit/missed and faded out if (note.y > 2732 + 100 || note.hit && note.alpha === 0 || note.missed && note.alpha === 0) { note.destroy(); notes.splice(i, 1); } // If note just missed if (note.missed && !note._missFeedbackShown) { note._missFeedbackShown = true; showMissEffect(note); // Only increment miss counter and score here, not in showMissEffect misses++; score += 1; updateScore(); missTxt.setText('Misses: ' + misses + ' / ' + maxMisses); if (misses >= maxMisses) { failSong(); } } } // End song if all notes are done and no notes left // (Removed endSong call so falling continues after song duration) // No speed ramp-up: notes stay slow for the whole game // Update and clean up missNotes for (var i = missNotes.length - 1; i >= 0; i--) { var mn = missNotes[i]; if (mn.update) mn.update(); // Removal is handled in MissNote class } }; // Start the game startSong(); // Play music (already started in startSong) LK.playMusic('song1');
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
// MissNote class: visually represents a miss as a falling note
var MissNote = Container.expand(function () {
var self = Container.call(this);
self.lane = 0;
self.speed = 5;
self.noteAsset = self.attachAsset('note1', {
anchorX: 0.5,
anchorY: 0.5,
x: 0,
y: 0,
alpha: 0.5
});
self.update = function () {
self.y += self.speed;
// Remove if off screen
if (self.y > 2732 + 100) {
self.destroy();
if (missNotes) {
for (var i = missNotes.length - 1; i >= 0; i--) {
if (missNotes[i] === self) {
missNotes.splice(i, 1);
break;
}
}
}
}
};
return self;
});
// Note class
var Note = Container.expand(function () {
var self = Container.call(this);
// lane: 0-3
self.lane = 0;
self.hit = false;
self.missed = false;
self.speed = 4; // Will be set dynamically
// Attach note asset (set in .init)
self.noteAsset = null;
// Feedback effect
self.effect = null;
// Called every tick
self.update = function () {
self.y += self.speed;
// If note goes past hit zone and not hit, mark as missed
if (!self.hit && !self.missed && self.y > hitZoneY + hitZoneHeight) {
self.missed = true;
showMissEffect(self);
}
};
// Show hit feedback
self.showHit = function () {
if (self.effect) return;
self.effect = self.attachAsset('hitEffect', {
anchorX: 0.5,
anchorY: 0.5,
x: 0,
y: 0,
alpha: 0.7
});
tween(self.effect, {
alpha: 0
}, {
duration: 300,
onFinish: function onFinish() {
if (self.effect) {
self.effect.destroy();
self.effect = null;
}
}
});
};
// Show miss feedback
self.showMiss = function () {
if (self.effect) return;
self.effect = self.attachAsset('missEffect', {
anchorX: 0.5,
anchorY: 0.5,
x: 0,
y: 0,
alpha: 0.7
});
tween(self.effect, {
alpha: 0
}, {
duration: 400,
onFinish: function onFinish() {
if (self.effect) {
self.effect.destroy();
self.effect = null;
}
}
});
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x181818
});
/****
* Game Code
****/
// Lane setup
// Four note lanes (different colors for clarity)
// Hit zone highlight
// Notes (different colors for each lane)
// Feedback shapes
// Sound effects
// Music (placeholder id)
var laneCount = 4;
var laneWidth = 400;
var laneSpacing = 32;
var totalLaneWidth = laneCount * laneWidth + (laneCount - 1) * laneSpacing;
var leftMargin = Math.floor((2048 - totalLaneWidth) / 2);
// Hit zone setup
var hitZoneHeight = 160; // Increased from 80 to 160 for higher volume
var hitZoneY = 2732 - 320; // 320px from bottom
// Lane X positions
var laneXs = [];
for (var i = 0; i < laneCount; i++) {
laneXs[i] = leftMargin + i * (laneWidth + laneSpacing) + laneWidth / 2;
}
// Draw lanes
var lanes = [];
for (var i = 0; i < laneCount; i++) {
var laneAssetId = 'lane' + (i + 1);
var lane = LK.getAsset(laneAssetId, {
anchorX: 0.5,
anchorY: 0,
x: laneXs[i],
y: 0,
width: laneWidth,
height: 2200
});
game.addChild(lane);
lanes.push(lane);
}
// Draw hit zones
var hitZones = [];
for (var i = 0; i < laneCount; i++) {
var hitZone = LK.getAsset('hitZone', {
anchorX: 0.5,
anchorY: 0,
x: laneXs[i],
y: hitZoneY,
width: laneWidth,
height: hitZoneHeight,
alpha: 0.12
});
game.addChild(hitZone);
hitZones.push(hitZone);
}
// Score and combo display
var score = 0;
var combo = 0;
var maxCombo = 0;
// Top score (persistent)
var topScore = storage.topScore || 0;
var topScoreTxt = new Text2('Top: ' + topScore, {
size: 60,
fill: 0x00ffcc
});
topScoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(topScoreTxt);
topScoreTxt.y = 90;
var scoreTxt = new Text2('Score: 0', {
size: 90,
fill: 0xFFFFFF
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
var comboTxt = new Text2('', {
size: 70,
fill: 0xFFFF00
});
comboTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(comboTxt);
comboTxt.y = 110;
// Miss counter display (bottom right)
var missTxt = new Text2('', {
size: 80,
fill: 0xFF4444
});
missTxt.anchor.set(1, 1); // bottom right
LK.gui.bottomRight.addChild(missTxt);
missTxt.x = 0;
missTxt.y = 0;
// Notes array
var notes = [];
// Miss notes array (visuals for misses)
var missNotes = [];
// Song data: array of {time, lane}
// For MVP, a simple hardcoded pattern (in ms, relative to song start)
var songNotes = [{
time: 800,
lane: 0
}, {
time: 1800,
lane: 1
}, {
time: 2800,
lane: 2
}, {
time: 3800,
lane: 3
}, {
time: 4800,
lane: 0
}, {
time: 5800,
lane: 1
}, {
time: 6800,
lane: 2
}, {
time: 7800,
lane: 3
}];
// Ensure no two songNotes fall in the same lane at the same time
for (var i = 1; i < songNotes.length; i++) {
// Check for any previous note with the same time or overlapping time window in the same lane
for (var j = 0; j < i; j++) {
// If the notes overlap in time and lane, change lane
if (songNotes[i].lane === songNotes[j].lane && Math.abs(songNotes[i].time - songNotes[j].time) < noteTravelTime) {
// Pick a different lane not used by any overlapping note
var forbiddenLanes = [];
for (var k = 0; k < i; k++) {
if (Math.abs(songNotes[i].time - songNotes[k].time) < noteTravelTime) {
forbiddenLanes.push(songNotes[k].lane);
}
}
var availableLanes = [0, 1, 2, 3].filter(function (l) {
return forbiddenLanes.indexOf(l) === -1;
});
if (availableLanes.length > 0) {
songNotes[i].lane = availableLanes[Math.floor(Math.random() * availableLanes.length)];
}
}
}
}
// Song parameters
var songDuration = 9000; // ms
var noteSpeed = 4; // px per frame (reduced for slower fall)
var noteTravelTime = 1800; // ms from spawn to hit zone (increased for slower fall)
// Speed scaling: increase by 10% every 500 points
var noteSpeedBase = 4;
var noteSpeedScale = 1;
var lastSpeedScoreStep = 0;
// Timing
var songStartTime = null;
var lastTickTime = null;
var songPlaying = false;
var nextNoteIndex = 0;
// Miss counter
var misses = 0;
var maxMisses = 5;
// Feedback effect for misses
function showMissEffect(note) {
note.showMiss();
combo = 0;
updateCombo();
LK.getSound('noteMiss').play();
// Flash lane
LK.effects.flashObject(lanes[note.lane], 0xff0000, 200);
// Spawn a MissNote visual at the missed note's lane
var missNote = new MissNote();
missNote.lane = note.lane;
missNote.x = laneXs[note.lane];
missNote.y = hitZoneY + hitZoneHeight / 2;
missNotes.push(missNote);
game.addChild(missNote);
// Miss counter and score are now handled in game.update when a miss is detected
}
// Feedback effect for hits
function showHitEffect(note) {
note.showHit();
LK.getSound('noteHit').play();
// Flash lane
LK.effects.flashObject(lanes[note.lane], 0x00ff00, 120);
}
// Update score and combo display
function updateScore() {
scoreTxt.setText('Score: ' + score);
if (score > topScore) {
topScore = score;
topScoreTxt.setText('Top: ' + topScore);
storage.topScore = topScore;
}
}
function updateCombo() {
if (combo > 1) {
comboTxt.setText('Combo: ' + combo);
} else {
comboTxt.setText('');
}
}
// Start song and reset state
function startSong() {
score = 0;
combo = 0;
maxCombo = 0;
misses = 0;
missTxt.setText('Misses: 0 / ' + maxMisses);
updateScore();
updateCombo();
noteSpeedScale = 1;
noteSpeed = noteSpeedBase;
lastSpeedScoreStep = 0;
songStartTime = Date.now();
lastTickTime = songStartTime;
songPlaying = true;
nextNoteIndex = 0;
// Remove old notes
for (var i = notes.length - 1; i >= 0; i--) {
notes[i].destroy();
notes.splice(i, 1);
}
// Play music
LK.playMusic('song1');
}
// End song (win)
function endSong() {
songPlaying = false;
LK.showYouWin();
}
// End song (fail)
function failSong() {
songPlaying = false;
LK.showGameOver();
}
// Spawn a note
function spawnNote(lane, spawnTime) {
var note = new Note();
note.lane = lane;
note.speed = noteSpeed;
var noteAssetId = 'note' + (lane + 1);
note.noteAsset = note.attachAsset(noteAssetId, {
anchorX: 0.5,
anchorY: 0.5,
x: 0,
y: 0
});
// X position: center of lane
note.x = laneXs[lane];
// Y position: spawn above screen so it reaches hit zone at correct time
note.y = getNoteSpawnY();
notes.push(note);
game.addChild(note);
note.spawnTime = spawnTime;
// Track lastWasIntersecting for hit zone lighting
note.lastWasIntersecting = false;
return note;
}
// Calculate note spawn Y so it reaches hit zone at the right time
function getNoteSpawnY() {
// Distance = speed * frames
// frames = noteTravelTime / (1000/60)
var frames = noteTravelTime / (1000 / 60);
var distance = frames * noteSpeed;
// Start notes at the very top of the screen (y = 0 - note height/2)
return 0 - 40; // 40 is half the note height (80)
}
// Find the closest note in a lane to the hit zone (not yet hit or missed)
function getHittableNote(lane) {
var best = null;
var bestDist = 99999;
for (var i = 0; i < notes.length; i++) {
var note = notes[i];
if (note.lane !== lane) continue;
if (note.hit || note.missed) continue;
var dist = Math.abs(note.y - (hitZoneY + hitZoneHeight / 2));
if (dist < bestDist) {
best = note;
bestDist = dist;
}
}
return best;
}
// Handle tap input
function handleTap(x, y, obj) {
// Only if song is playing
if (!songPlaying) return;
// Which lane?
for (var i = 0; i < laneCount; i++) {
var laneLeft = laneXs[i] - laneWidth / 2;
var laneRight = laneXs[i] + laneWidth / 2;
if (x >= laneLeft && x <= laneRight) {
// Check for hittable note in this lane
var note = getHittableNote(i);
if (note) {
var noteCenterY = note.y;
var hitCenterY = hitZoneY + hitZoneHeight / 2;
var hitWindow = hitZoneHeight / 2; // Match hit window to half the new hit zone height
if (Math.abs(noteCenterY - hitCenterY) <= hitWindow) {
// Hit!
note.hit = true;
showHitEffect(note);
score += 20;
combo += 1;
if (combo > maxCombo) maxCombo = combo;
updateScore();
updateCombo();
// Remove note after feedback
tween(note, {
alpha: 0
}, {
duration: 120,
onFinish: function onFinish() {
note.destroy();
}
});
return;
}
}
// Miss (tapped but no note in window)
combo = 0;
updateCombo();
LK.getSound('noteMiss').play();
LK.effects.flashObject(lanes[i], 0xff0000, 120);
// No miss increment or score for empty tap
return;
}
}
}
// Attach tap handler
game.down = function (x, y, obj) {
handleTap(x, y, obj);
};
// No drag or move needed for this game
game.move = function (x, y, obj) {};
game.up = function (x, y, obj) {};
// Main update loop
game.update = function () {
if (!songPlaying) return;
var now = Date.now();
var songElapsed = now - songStartTime;
// Update noteSpeed scaling every frame
var speedScoreStep = Math.floor(score / 500);
if (speedScoreStep > lastSpeedScoreStep) {
// Increase speed by 20% for each 500 points
noteSpeedScale = Math.pow(1.2, speedScoreStep);
noteSpeed = noteSpeedBase * noteSpeedScale;
lastSpeedScoreStep = speedScoreStep;
// Also update speed for all active notes
for (var i = 0; i < notes.length; i++) {
notes[i].speed = noteSpeed;
}
// And for all active miss notes
for (var i = 0; i < missNotes.length; i++) {
missNotes[i].speed = noteSpeed;
}
}
// Spawn notes as their time comes
while (nextNoteIndex < songNotes.length && songNotes[nextNoteIndex].time <= songElapsed + noteTravelTime) {
var noteData = songNotes[nextNoteIndex];
spawnNote(noteData.lane, noteData.time);
nextNoteIndex++;
}
// After all songNotes are spawned, keep spawning random notes forever
if (nextNoteIndex >= songNotes.length) {
// Use a timer to control spawn rate
if (!game._lastAutoNoteTime) game._lastAutoNoteTime = now;
var autoNoteInterval = 1800; // ms between random notes (increased for much less frequent falling)
// Limit the number of simultaneously falling random notes dynamically based on score
// For example: start at 2, increase by 1 every 1000 points, up to a max of 6
var maxSimultaneousRandomNotes = Math.min(2 + Math.floor(score / 1000), 6);
var activeRandomNotes = 0;
for (var j = 0; j < notes.length; j++) {
if (notes[j].spawnTime > songNotes[songNotes.length - 1].time) {
if (!notes[j].hit && !notes[j].missed) {
activeRandomNotes++;
}
}
}
if (now - game._lastAutoNoteTime > autoNoteInterval && activeRandomNotes < maxSimultaneousRandomNotes) {
// Find lanes currently occupied by random notes (not hit/missed)
var occupiedLanes = [];
for (var j = 0; j < notes.length; j++) {
if (notes[j].spawnTime > songNotes[songNotes.length - 1].time) {
if (!notes[j].hit && !notes[j].missed) {
occupiedLanes.push(notes[j].lane);
}
}
}
// Find available lanes
var availableLanes = [];
for (var l = 0; l < laneCount; l++) {
if (occupiedLanes.indexOf(l) === -1) {
availableLanes.push(l);
}
}
if (availableLanes.length > 0) {
// Further filter: ensure no note (song or random) is currently falling in this lane
var trulyAvailableLanes = [];
for (var l = 0; l < availableLanes.length; l++) {
var lane = availableLanes[l];
var laneOccupied = false;
for (var n = 0; n < notes.length; n++) {
if (notes[n].lane === lane && !notes[n].hit && !notes[n].missed && notes[n].y < 2732 + 100 // still on screen
) {
laneOccupied = true;
break;
}
}
if (!laneOccupied) {
trulyAvailableLanes.push(lane);
}
}
if (trulyAvailableLanes.length > 0) {
var randLane = trulyAvailableLanes[Math.floor(Math.random() * trulyAvailableLanes.length)];
// Randomize fall speed for this note: between 80% and 120% of current noteSpeed
var randomSpeedFactor = 0.8 + Math.random() * 0.4;
var prevNoteSpeed = noteSpeed;
noteSpeed = noteSpeed * randomSpeedFactor;
// Randomize spawn Y: allow notes to start up to 200px above or below the default spawn Y
var origGetNoteSpawnY = getNoteSpawnY;
getNoteSpawnY = function getNoteSpawnY() {
var baseY = origGetNoteSpawnY();
return baseY + Math.floor((Math.random() - 0.5) * 400); // ±200px
};
spawnNote(randLane, songElapsed);
getNoteSpawnY = origGetNoteSpawnY; // Restore
noteSpeed = prevNoteSpeed; // Restore global noteSpeed for other notes
game._lastAutoNoteTime = now;
}
}
}
}
// Update notes
for (var i = notes.length - 1; i >= 0; i--) {
var note = notes[i];
note.update();
// Light up hitbar when note enters hit zone
if (!note.lastWasIntersecting) {
// Check intersection with hit zone for this note's lane
var hitZone = hitZones[note.lane];
if (note.intersects(hitZone)) {
// Animate hit zone alpha up, then back down, and tint yellow
var originalTint = hitZone.tint !== undefined ? hitZone.tint : 0xffffff;
hitZone.tint = 0xffff00; // yellow
tween(hitZone, {
alpha: 0.45
}, {
duration: 60,
onFinish: function onFinish() {
tween(hitZone, {
alpha: 0.12
}, {
duration: 180,
onFinish: function onFinish() {
hitZone.tint = originalTint;
}
});
}
});
}
}
note.lastWasIntersecting = note.intersects(hitZones[note.lane]);
// Remove notes that are off screen or hit/missed and faded out
if (note.y > 2732 + 100 || note.hit && note.alpha === 0 || note.missed && note.alpha === 0) {
note.destroy();
notes.splice(i, 1);
}
// If note just missed
if (note.missed && !note._missFeedbackShown) {
note._missFeedbackShown = true;
showMissEffect(note);
// Only increment miss counter and score here, not in showMissEffect
misses++;
score += 1;
updateScore();
missTxt.setText('Misses: ' + misses + ' / ' + maxMisses);
if (misses >= maxMisses) {
failSong();
}
}
}
// End song if all notes are done and no notes left
// (Removed endSong call so falling continues after song duration)
// No speed ramp-up: notes stay slow for the whole game
// Update and clean up missNotes
for (var i = missNotes.length - 1; i >= 0; i--) {
var mn = missNotes[i];
if (mn.update) mn.update();
// Removal is handled in MissNote class
}
};
// Start the game
startSong();
// Play music (already started in startSong)
LK.playMusic('song1');