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