User prompt
game ends in 5 misses
User prompt
falling continues until game over
User prompt
hit zones light up in yellow
User prompt
make hitzones eclipse
User prompt
light up hitbars when bars get in the hitbars
User prompt
increase volume of hit zones
User prompt
bars have fewer frequency
User prompt
bars start falling from the top
User prompt
bars fall from higher
User prompt
make fall down slower
Code edit (1 edits merged)
Please save this source code
User prompt
Rhythm Tap Hero
Initial prompt
ı want a game like guitar hero
/**** * 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');