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