Code edit (3 edits merged)
Please save this source code
User prompt
when combo, animate comboTxt scale from 1 to 1.5 then return to 1
User prompt
if a note passes the hitLine without beeing tapped, make it flash red ↪💡 Consider importing and using the following plugins: @upit/tween.v1
Code edit (5 edits merged)
Please save this source code
User prompt
in showTapFeedback, animate noteSign scale increase to 4 ↪💡 Consider importing and using the following plugins: @upit/tween.v1
Code edit (4 edits merged)
Please save this source code
User prompt
Instantiate BgManager and set initial background
User prompt
Add BackgroundManager class that adds the 'background00' asset as a background to the game; don't instanciate yet
User prompt
Instantiate BgManager and set initial background
User prompt
Add BgManager class to manage backgrounds
User prompt
let dots continue their move until passing screen bottom instead of hitline
User prompt
add a blue tint to targets
User prompt
Instantiate 3 targets, one in each column at hitLineY
User prompt
Add Target class with target asset
Code edit (1 edits merged)
Please save this source code
User prompt
Move key sound play logic from game.down to Note.down()
Code edit (1 edits merged)
Please save this source code
User prompt
✅ Add NoteManager class to handle note spawning and management
Code edit (2 edits merged)
Please save this source code
User prompt
move the note tap logic from game.update() to Note.update()
Code edit (2 edits merged)
Please save this source code
User prompt
add a global speed multiplier to control absolute speed but respect relative notes speed
User prompt
Increase vertical space between falling dots
User prompt
Increase vertical space between falling dots by increasing noteTravelTime
User prompt
augment vertical space between dots
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ // Note class: a falling dot in a lane, with timing and tap state var Note = Container.expand(function () { var self = Container.call(this); // Attach the note dot asset, centered var dot = self.attachAsset('noteDot', { anchorX: 0.5, anchorY: 0.5 }); // Lane index (0,1,2) self.lane = 0; // Time (in ms) when this note should be hit self.hitTime = 0; // Whether this note has been tapped self.tapped = false; // Whether this note has been missed self.missed = false; // For tap feedback self.showTapFeedback = function () { var feedback = self.attachAsset('tapFeedback', { anchorX: 0.5, anchorY: 0.5, alpha: 0.5 }); tween(feedback, { alpha: 0 }, { duration: 250, onFinish: function onFinish() { feedback.destroy(); } }); }; // Called every tick self.update = function () { // Calculate songElapsed and t for this note if (!gameActive) return; var now = Date.now(); var songElapsed = now - songStartTime; // Calculate progress: 0 (spawn) to 1 (at hit line) var t = (songElapsed - (self.hitTime - noteTravelTime)) / noteTravelTime; if (t < 0) t = 0; if (t > 1) t = 1; self.y = noteStartY + (hitLineY - noteStartY) * t; // Miss detection: if note passes hit line and not tapped if (!self.tapped && !self.missed && songElapsed > self.hitTime + 220) { self.missed = true; self.alpha = 0.3; combo = 0; comboTxt.setText(''); /* LK.getSound('tapMiss').play(); // Flash lane red LK.effects.flashObject(laneHighlights[self.lane], 0xff0000, 300); // Game over on first miss LK.effects.flashScreen(0xff0000, 600); gameActive = false; LK.setTimeout(function () { LK.showGameOver(); }, 600); */ } }; return self; }); // NoteManager class: handles spawning and management of notes var NoteManager = Container.expand(function () { var self = Container.call(this); // Notes in play self.notes = []; // Index of next note to spawn self.nextNoteIdx = 0; // Reset all state self.reset = function () { for (var i = 0; i < self.notes.length; i++) { self.notes[i].destroy(); } self.notes = []; self.nextNoteIdx = 0; }; // Spawn notes as their time approaches self.spawnNotes = function (songNotes, songElapsed, noteTravelTime, laneX, noteStartY) { while (self.nextNoteIdx < songNotes.length && songNotes[self.nextNoteIdx].time - noteTravelTime <= songElapsed) { var noteData = songNotes[self.nextNoteIdx]; var note = new Note(); note.lane = noteData.lane; note.hitTime = noteData.time; note.x = laneX[note.lane]; note.y = noteStartY; self.notes.push(note); game.addChild(note); self.nextNoteIdx++; } }; // Remove notes that are far past the hit line self.cleanupNotes = function (songElapsed) { for (var i = self.notes.length - 1; i >= 0; i--) { var note = self.notes[i]; if (songElapsed > note.hitTime + 1200) { note.destroy(); self.notes.splice(i, 1); } } }; // Remove a specific note from the manager self.removeNote = function (note) { for (var i = 0; i < self.notes.length; i++) { if (self.notes[i] === note) { self.notes.splice(i, 1); break; } } }; // Get all notes in play self.getNotes = function () { return self.notes; }; // Get the index of the next note to spawn self.getNextNoteIdx = function () { return self.nextNoteIdx; }; // Set the index of the next note to spawn self.setNextNoteIdx = function (idx) { self.nextNoteIdx = idx; }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x222222 }); /**** * Game Code ****/ // Music track (Ode to Joy, assumed loaded as 'odeToJoy') // Sound for miss // Sound for correct tap // Lane highlight (subtle gray) // Tap feedback (blue highlight) // Falling note (white dot) // Sound assets for key0 to key14 var SONGS = [{ "name": "Ode to Joy", "bpm": 220, "pitchLevel": 0, "bitsPerPage": 16, "isComposed": false, "songNotes": [{ "time": 1432, "key": "Key6" }, { "time": 1855, "key": "Key6" }, { "time": 2305, "key": "Key7" }, { "time": 2788, "key": "Key8" }, { "time": 3216, "key": "Key8" }, { "time": 3666, "key": "Key7" }, { "time": 4122, "key": "Key6" }, { "time": 4567, "key": "Key5" }, { "time": 5027, "key": "Key4" }, { "time": 5479, "key": "Key4" }, { "time": 5937, "key": "Key5" }, { "time": 6397, "key": "Key6" }, { "time": 6864, "key": "Key6" }, { "time": 7583, "key": "Key5" }, { "time": 7820, "key": "Key5" }, { "time": 8816, "key": "Key6" }, { "time": 9289, "key": "Key6" }, { "time": 9778, "key": "Key7" }, { "time": 10205, "key": "Key8" }, { "time": 10672, "key": "Key8" }, { "time": 11108, "key": "Key7" }, { "time": 11564, "key": "Key6" }, { "time": 12000, "key": "Key5" }, { "time": 12455, "key": "Key4" }, { "time": 12911, "key": "Key4" }, { "time": 13339, "key": "Key5" }, { "time": 13785, "key": "Key6" }, { "time": 14370, "key": "Key5" }, { "time": 15131, "key": "Key4" }, { "time": 15341, "key": "Key4" }, { "time": 16318, "key": "Key5" }, { "time": 16760, "key": "Key5" }, { "time": 17243, "key": "Key6" }, { "time": 17711, "key": "Key4" }, { "time": 18164, "key": "Key5" }, { "time": 18607, "key": "Key6" }, { "time": 18840, "key": "Key7" }, { "time": 19107, "key": "Key6" }, { "time": 19556, "key": "Key4" }, { "time": 20007, "key": "Key5" }, { "time": 20428, "key": "Key6" }, { "time": 20634, "key": "Key7" }, { "time": 20915, "key": "Key6" }, { "time": 21375, "key": "Key5" }, { "time": 21859, "key": "Key4" }, { "time": 22325, "key": "Key5" }, { "time": 22818, "key": "Key1" }, { "time": 23809, "key": "Key6" }, { "time": 24259, "key": "Key6" }, { "time": 24725, "key": "Key7" }, { "time": 25156, "key": "Key8" }, { "time": 25597, "key": "Key8" }, { "time": 26039, "key": "Key7" }, { "time": 26496, "key": "Key6" }, { "time": 26950, "key": "Key5" }, { "time": 27413, "key": "Key4" }, { "time": 27882, "key": "Key4" }, { "time": 28309, "key": "Key5" }, { "time": 28830, "key": "Key6" }, { "time": 29319, "key": "Key5" }, { "time": 30092, "key": "Key4" }, { "time": 30343, "key": "Key4" }], "fromLibrary": true }]; ; // --- Song Data: Use SONGS[0] --- // Each note: {lane: 0|1|2, time: ms from song start} // Lane 0: left, 1: center, 2: right // We'll map keys to lanes below. var songNotesRaw = SONGS[0].songNotes; var keyToLane = { "Key0": 0, "Key1": 1, "Key2": 2, "Key3": 0, "Key4": 1, "Key5": 2, "Key6": 0, "Key7": 1, "Key8": 2, "Key9": 0, "Key10": 1, "Key11": 2, "Key12": 0, "Key13": 1, "Key14": 2 }; // If the song uses only Key6/Key7/Key8, map them to 0/1/2, else fallback to a round-robin var songNotes = []; for (var i = 0; i < songNotesRaw.length; i++) { var note = songNotesRaw[i]; var lane = 0; if (keyToLane.hasOwnProperty(note.key)) { lane = keyToLane[note.key]; } else { lane = i % 3; } songNotes.push({ lane: lane, time: note.time }); } // Song duration (ms) var songDuration = songNotes.length > 0 ? songNotes[songNotes.length - 1].time + 1000 : 9000; // --- Lane positions (3 columns) --- var laneCount = 3; var laneWidth = 480; var laneSpacing = 2048 / laneCount; var laneX = [laneSpacing * 0.5, // left laneSpacing * 1.5, // center laneSpacing * 2.5 // right ]; // --- Note fall parameters --- var speedMultiplier = 1.0; // Global speed multiplier (1.0 = normal, >1 = faster, <1 = slower) var baseNoteTravelTime = 8000; // ms: base time from spawn (top) to hit line (bottom) var noteTravelTime = baseNoteTravelTime / speedMultiplier; // effective travel time, updated if speedMultiplier changes var hitLineY = 2300; // y position where notes should be tapped var noteStartY = -100; // spawn just above the screen // --- State --- var noteManager = new NoteManager(); // Handles all notes and spawning var songStartTime = 0; // Date.now() when song started var gameActive = false; // True if game is running var score = 0; var combo = 0; var maxCombo = 0; var lastTapTime = 0; // --- GUI Elements --- var scoreTxt = new Text2('0', { size: 120, fill: 0xFFFFFF }); scoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(scoreTxt); var comboTxt = new Text2('', { size: 80, fill: 0x3A8EE6 }); comboTxt.anchor.set(0.5, 0); LK.gui.top.addChild(comboTxt); comboTxt.y = 130; // --- Lane highlights (for visual feedback) --- var laneHighlights = []; for (var i = 0; i < laneCount; i++) { var laneHL = LK.getAsset('laneHighlight', { anchorX: 0.5, anchorY: 0, alpha: 0.07, x: laneX[i], y: 0, height: 2732 }); game.addChild(laneHL); laneHighlights.push(laneHL); } // --- Hit line (invisible, for reference) --- var hitLineYDisplay = hitLineY; // --- Start the game --- function startGame() { // Reset state noteManager.reset(); score = 0; combo = 0; maxCombo = 0; scoreTxt.setText('0'); comboTxt.setText(''); lastTapTime = 0; gameActive = true; songStartTime = Date.now(); // Play music LK.playMusic('odeToJoy', { fade: { start: 0, end: 1, duration: 800 } }); } startGame(); // --- Main game update loop --- game.update = function () { // Update noteTravelTime in case speedMultiplier has changed noteTravelTime = baseNoteTravelTime / speedMultiplier; if (!gameActive) { return; } var now = Date.now(); var songElapsed = now - songStartTime; // --- Spawn notes as their time approaches --- noteManager.spawnNotes(songNotes, songElapsed, noteTravelTime, laneX, noteStartY); // --- Update notes: remove notes that are far past the hit line --- noteManager.cleanupNotes(songElapsed); // --- Win condition: song finished and all notes handled --- if (songElapsed > songDuration + 1000 && noteManager.getNotes().length === 0 && gameActive) { gameActive = false; LK.setTimeout(function () { LK.showYouWin(); }, 600); } }; // --- Tap input handling --- // Convert x to lane index (0,1,2) function getLaneFromX(x) { // Each lane is laneSpacing wide, centered at laneX[i] for (var i = 0; i < laneCount; i++) { var left = laneX[i] - laneWidth / 2; var right = laneX[i] + laneWidth / 2; if (x >= left && x <= right) { return i; } } // Out of bounds return -1; } // On tap (down) anywhere in game game.down = function (x, y, obj) { if (!gameActive) { return; } // Only accept taps near the hit line (±220px) if (y < hitLineY - 220 || y > hitLineY + 220) { return; } var lane = getLaneFromX(x); if (lane < 0 || lane >= laneCount) { return; } // Find the earliest untapped note in this lane within hit window var now = Date.now(); var songElapsed = now - songStartTime; var bestNote = null; var bestDelta = 9999; var notesInPlay = noteManager.getNotes(); for (var i = 0; i < notesInPlay.length; i++) { var note = notesInPlay[i]; if (note.lane !== lane) { continue; } if (note.tapped || note.missed) { continue; } var delta = Math.abs(songElapsed - note.hitTime); if (delta < 320 && delta < bestDelta) { // 320ms window bestNote = note; bestDelta = delta; } } if (bestNote) { // Correct tap! bestNote.tapped = true; bestNote.showTapFeedback(); LK.getSound('tapGood').play(); // Play the corresponding key sound for this note // Find the original song note for this bestNote var noteIdx = noteManager.getNextNoteIdx() - (notesInPlay.length - notesInPlay.indexOf(bestNote)); var songNote = songNotesRaw[noteIdx]; if (songNote && typeof songNote.key === "string") { var keySoundName = songNote.key.toLowerCase(); var keySound = LK.getSound(keySoundName); if (keySound) { keySound.play(); } } score += 1; combo += 1; if (combo > maxCombo) { maxCombo = combo; } scoreTxt.setText(score); if (combo > 1) { comboTxt.setText(combo + ' Combo!'); } else { comboTxt.setText(''); } // Flash lane blue LK.effects.flashObject(laneHighlights[lane], 0x3a8ee6, 180); // Remove note visually tween(bestNote, { alpha: 0 }, { duration: 180, onFinish: function onFinish() { bestNote.destroy(); } }); // Remove from notes array after fade noteManager.removeNote(bestNote); } else { // Miss! (Tapped with no note in window) combo = 0; comboTxt.setText(''); LK.getSound('tapMiss').play(); LK.effects.flashObject(laneHighlights[lane], 0xff0000, 300); LK.effects.flashScreen(0xff0000, 600); /* gameActive = false; LK.setTimeout(function () { LK.showGameOver(); }, 600); */ } }; // --- Visual: show hit line (subtle, not interactive) --- var hitLine = LK.getAsset('laneHighlight', { anchorX: 0.5, anchorY: 0.5, width: 2048, height: 8, color: 0x3a8ee6, alpha: 0.18, x: 2048 / 2, y: hitLineY }); game.addChild(hitLine); // --- Play music on start --- LK.playMusic('odeToJoy', { fade: { start: 0, end: 1, duration: 800 } }); // --- Reset game on game over or win --- LK.on('gameover', function () { LK.stopMusic(); startGame(); }); LK.on('youwin', function () { LK.stopMusic(); startGame(); });
===================================================================
--- original.js
+++ change.js
@@ -40,21 +40,15 @@
};
// Called every tick
self.update = function () {
// Calculate songElapsed and t for this note
- if (!gameActive) {
- return;
- }
+ if (!gameActive) return;
var now = Date.now();
var songElapsed = now - songStartTime;
// Calculate progress: 0 (spawn) to 1 (at hit line)
var t = (songElapsed - (self.hitTime - noteTravelTime)) / noteTravelTime;
- if (t < 0) {
- t = 0;
- }
- if (t > 1) {
- t = 1;
- }
+ if (t < 0) t = 0;
+ if (t > 1) t = 1;
self.y = noteStartY + (hitLineY - noteStartY) * t;
// Miss detection: if note passes hit line and not tapped
if (!self.tapped && !self.missed && songElapsed > self.hitTime + 220) {
self.missed = true;
@@ -70,13 +64,75 @@
gameActive = false;
LK.setTimeout(function () {
LK.showGameOver();
}, 600);
- */
+ */
}
};
return self;
});
+// NoteManager class: handles spawning and management of notes
+var NoteManager = Container.expand(function () {
+ var self = Container.call(this);
+ // Notes in play
+ self.notes = [];
+ // Index of next note to spawn
+ self.nextNoteIdx = 0;
+ // Reset all state
+ self.reset = function () {
+ for (var i = 0; i < self.notes.length; i++) {
+ self.notes[i].destroy();
+ }
+ self.notes = [];
+ self.nextNoteIdx = 0;
+ };
+ // Spawn notes as their time approaches
+ self.spawnNotes = function (songNotes, songElapsed, noteTravelTime, laneX, noteStartY) {
+ while (self.nextNoteIdx < songNotes.length && songNotes[self.nextNoteIdx].time - noteTravelTime <= songElapsed) {
+ var noteData = songNotes[self.nextNoteIdx];
+ var note = new Note();
+ note.lane = noteData.lane;
+ note.hitTime = noteData.time;
+ note.x = laneX[note.lane];
+ note.y = noteStartY;
+ self.notes.push(note);
+ game.addChild(note);
+ self.nextNoteIdx++;
+ }
+ };
+ // Remove notes that are far past the hit line
+ self.cleanupNotes = function (songElapsed) {
+ for (var i = self.notes.length - 1; i >= 0; i--) {
+ var note = self.notes[i];
+ if (songElapsed > note.hitTime + 1200) {
+ note.destroy();
+ self.notes.splice(i, 1);
+ }
+ }
+ };
+ // Remove a specific note from the manager
+ self.removeNote = function (note) {
+ for (var i = 0; i < self.notes.length; i++) {
+ if (self.notes[i] === note) {
+ self.notes.splice(i, 1);
+ break;
+ }
+ }
+ };
+ // Get all notes in play
+ self.getNotes = function () {
+ return self.notes;
+ };
+ // Get the index of the next note to spawn
+ self.getNextNoteIdx = function () {
+ return self.nextNoteIdx;
+ };
+ // Set the index of the next note to spawn
+ self.setNextNoteIdx = function (idx) {
+ self.nextNoteIdx = idx;
+ };
+ return self;
+});
/****
* Initialize Game
****/
@@ -86,15 +142,15 @@
/****
* Game Code
****/
-// Sound assets for key0 to key14
-// Falling note (white dot)
-// Tap feedback (blue highlight)
-// Lane highlight (subtle gray)
-// Sound for correct tap
-// Sound for miss
// Music track (Ode to Joy, assumed loaded as 'odeToJoy')
+// Sound for miss
+// Sound for correct tap
+// Lane highlight (subtle gray)
+// Tap feedback (blue highlight)
+// Falling note (white dot)
+// Sound assets for key0 to key14
var SONGS = [{
"name": "Ode to Joy",
"bpm": 220,
"pitchLevel": 0,
@@ -345,10 +401,9 @@
var noteTravelTime = baseNoteTravelTime / speedMultiplier; // effective travel time, updated if speedMultiplier changes
var hitLineY = 2300; // y position where notes should be tapped
var noteStartY = -100; // spawn just above the screen
// --- State ---
-var notes = []; // All Note objects in play
-var nextNoteIdx = 0; // Index of next note to spawn
+var noteManager = new NoteManager(); // Handles all notes and spawning
var songStartTime = 0; // Date.now() when song started
var gameActive = false; // True if game is running
var score = 0;
var combo = 0;
@@ -386,13 +441,9 @@
var hitLineYDisplay = hitLineY;
// --- Start the game ---
function startGame() {
// Reset state
- for (var i = 0; i < notes.length; i++) {
- notes[i].destroy();
- }
- notes = [];
- nextNoteIdx = 0;
+ noteManager.reset();
score = 0;
combo = 0;
maxCombo = 0;
scoreTxt.setText('0');
@@ -419,37 +470,13 @@
}
var now = Date.now();
var songElapsed = now - songStartTime;
// --- Spawn notes as their time approaches ---
- while (nextNoteIdx < songNotes.length && songNotes[nextNoteIdx].time - noteTravelTime <= songElapsed) {
- var noteData = songNotes[nextNoteIdx];
- var note = new Note();
- note.lane = noteData.lane;
- note.hitTime = noteData.time;
- note.x = laneX[note.lane];
- note.y = noteStartY;
- notes.push(note);
- game.addChild(note);
- nextNoteIdx++;
- }
+ noteManager.spawnNotes(songNotes, songElapsed, noteTravelTime, laneX, noteStartY);
// --- Update notes: remove notes that are far past the hit line ---
- for (var i = notes.length - 1; i >= 0; i--) {
- var note = notes[i];
- var t = (songElapsed - (note.hitTime - noteTravelTime)) / noteTravelTime;
- if (t < 0) {
- t = 0;
- }
- if (t > 1) {
- t = 1;
- }
- // Remove notes that are far past the hit line
- if (songElapsed > note.hitTime + 1200) {
- note.destroy();
- notes.splice(i, 1);
- }
- }
+ noteManager.cleanupNotes(songElapsed);
// --- Win condition: song finished and all notes handled ---
- if (songElapsed > songDuration + 1000 && notes.length === 0 && gameActive) {
+ if (songElapsed > songDuration + 1000 && noteManager.getNotes().length === 0 && gameActive) {
gameActive = false;
LK.setTimeout(function () {
LK.showYouWin();
}, 600);
@@ -469,100 +496,96 @@
// Out of bounds
return -1;
}
// On tap (down) anywhere in game
-// game.down = function (x, y, obj) {
-// if (!gameActive) {
-// return;
-// }
-// // Only accept taps near the hit line (±220px)
-// if (y < hitLineY - 220 || y > hitLineY + 220) {
-// return;
-// }
-// var lane = getLaneFromX(x);
-// if (lane < 0 || lane >= laneCount) {
-// return;
-// }
-// // Find the earliest untapped note in this lane within hit window
-// var now = Date.now();
-// var songElapsed = now - songStartTime;
-// var bestNote = null;
-// var bestDelta = 9999;
-// for (var i = 0; i < notes.length; i++) {
-// var note = notes[i];
-// if (note.lane !== lane) {
-// continue;
-// }
-// if (note.tapped || note.missed) {
-// continue;
-// }
-// var delta = Math.abs(songElapsed - note.hitTime);
-// if (delta < 320 && delta < bestDelta) {
-// // 320ms window
-// bestNote = note;
-// bestDelta = delta;
-// }
-// }
-// if (bestNote) {
-// // Correct tap!
-// bestNote.tapped = true;
-// bestNote.showTapFeedback();
-// LK.getSound('tapGood').play();
-// // Play the corresponding key sound for this note
-// // Find the original song note for this bestNote
-// var noteIdx = nextNoteIdx - (notes.length - notes.indexOf(bestNote));
-// var songNote = songNotesRaw[noteIdx];
-// if (songNote && typeof songNote.key === "string") {
-// var keySoundName = songNote.key.toLowerCase();
-// var keySound = LK.getSound(keySoundName);
-// if (keySound) {
-// keySound.play();
-// }
-// }
-// score += 1;
-// combo += 1;
-// if (combo > maxCombo) {
-// maxCombo = combo;
-// }
-// scoreTxt.setText(score);
-// if (combo > 1) {
-// comboTxt.setText(combo + ' Combo!');
-// } else {
-// comboTxt.setText('');
-// }
-// // Flash lane blue
-// LK.effects.flashObject(laneHighlights[lane], 0x3a8ee6, 180);
-// // Remove note visually
-// tween(bestNote, {
-// alpha: 0
-// }, {
-// duration: 180,
-// onFinish: function onFinish() {
-// bestNote.destroy();
-// }
-// });
-// // Remove from notes array after fade
-// for (var j = 0; j < notes.length; j++) {
-// if (notes[j] === bestNote) {
-// notes.splice(j, 1);
-// break;
-// }
-// }
-// } else {
-// // Miss! (Tapped with no note in window)
-// combo = 0;
-// comboTxt.setText('');
-// LK.getSound('tapMiss').play();
-// LK.effects.flashObject(laneHighlights[lane], 0xff0000, 300);
-// LK.effects.flashScreen(0xff0000, 600);
-// /*
-// gameActive = false;
-// LK.setTimeout(function () {
-// LK.showGameOver();
-// }, 600);
-// */
-// }
-// };
+game.down = function (x, y, obj) {
+ if (!gameActive) {
+ return;
+ }
+ // Only accept taps near the hit line (±220px)
+ if (y < hitLineY - 220 || y > hitLineY + 220) {
+ return;
+ }
+ var lane = getLaneFromX(x);
+ if (lane < 0 || lane >= laneCount) {
+ return;
+ }
+ // Find the earliest untapped note in this lane within hit window
+ var now = Date.now();
+ var songElapsed = now - songStartTime;
+ var bestNote = null;
+ var bestDelta = 9999;
+ var notesInPlay = noteManager.getNotes();
+ for (var i = 0; i < notesInPlay.length; i++) {
+ var note = notesInPlay[i];
+ if (note.lane !== lane) {
+ continue;
+ }
+ if (note.tapped || note.missed) {
+ continue;
+ }
+ var delta = Math.abs(songElapsed - note.hitTime);
+ if (delta < 320 && delta < bestDelta) {
+ // 320ms window
+ bestNote = note;
+ bestDelta = delta;
+ }
+ }
+ if (bestNote) {
+ // Correct tap!
+ bestNote.tapped = true;
+ bestNote.showTapFeedback();
+ LK.getSound('tapGood').play();
+ // Play the corresponding key sound for this note
+ // Find the original song note for this bestNote
+ var noteIdx = noteManager.getNextNoteIdx() - (notesInPlay.length - notesInPlay.indexOf(bestNote));
+ var songNote = songNotesRaw[noteIdx];
+ if (songNote && typeof songNote.key === "string") {
+ var keySoundName = songNote.key.toLowerCase();
+ var keySound = LK.getSound(keySoundName);
+ if (keySound) {
+ keySound.play();
+ }
+ }
+ score += 1;
+ combo += 1;
+ if (combo > maxCombo) {
+ maxCombo = combo;
+ }
+ scoreTxt.setText(score);
+ if (combo > 1) {
+ comboTxt.setText(combo + ' Combo!');
+ } else {
+ comboTxt.setText('');
+ }
+ // Flash lane blue
+ LK.effects.flashObject(laneHighlights[lane], 0x3a8ee6, 180);
+ // Remove note visually
+ tween(bestNote, {
+ alpha: 0
+ }, {
+ duration: 180,
+ onFinish: function onFinish() {
+ bestNote.destroy();
+ }
+ });
+ // Remove from notes array after fade
+ noteManager.removeNote(bestNote);
+ } else {
+ // Miss! (Tapped with no note in window)
+ combo = 0;
+ comboTxt.setText('');
+ LK.getSound('tapMiss').play();
+ LK.effects.flashObject(laneHighlights[lane], 0xff0000, 300);
+ LK.effects.flashScreen(0xff0000, 600);
+ /*
+ gameActive = false;
+ LK.setTimeout(function () {
+ LK.showGameOver();
+ }, 600);
+ */
+ }
+};
// --- Visual: show hit line (subtle, not interactive) ---
var hitLine = LK.getAsset('laneHighlight', {
anchorX: 0.5,
anchorY: 0.5,
key0
Sound effect
key1
Sound effect
key2
Sound effect
key3
Sound effect
key4
Sound effect
key5
Sound effect
key6
Sound effect
key7
Sound effect
key8
Sound effect
key10
Sound effect
key11
Sound effect
key12
Sound effect
key13
Sound effect
key14
Sound effect
key9
Sound effect
tapMiss
Sound effect
cheers
Sound effect
startSound
Sound effect
click
Sound effect
menuSpawn
Sound effect
jeers
Sound effect