/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ // Drag Note var DragNote = Container.expand(function () { var self = Container.call(this); var note = self.attachAsset('dragNote', { anchorX: 0.5, anchorY: 0.5 }); self.type = 'drag'; self.lane = 0; self.targetLane = 0; self.time = 0; // When the drag should start (in ms) self.active = true; self.update = function () { if (!self.active) return; self.y += noteSpeed; }; // Make drag notes tappable directly self.down = function (x, y, obj) { if (!self.active) return; // Only allow hit if note is within hit window of hit line and not already being dragged var dist = Math.abs(self.y + 60 - NOTE_HIT_Y); // 60 = half note height if (dist < HIT_WINDOW && !self.dragging) { // Play noteHit sound when dragNote is tapped LK.getSound('noteHit').play(); self.active = false; self.flash(); addScore(dist < 60 ? 300 : 100, dist < 60); tween(self, { alpha: 0 }, { duration: 200, onFinish: function onFinish() { self.destroy(); } }); } }; self.flash = function () { tween(note, { scaleX: 1.3, scaleY: 1.3 }, { duration: 80, onFinish: function onFinish() { tween(note, { scaleX: 1, scaleY: 1 }, { duration: 80 }); } }); }; return self; }); // Hold Note var HoldNote = Container.expand(function () { var self = Container.call(this); var note = self.attachAsset('holdNote', { anchorX: 0.5, anchorY: 0 }); self.type = 'hold'; self.lane = 0; self.time = 0; // When the hold starts (in ms) self.duration = 1000; // How long to hold (ms) self.held = false; self.active = true; self.update = function () { if (!self.active) return; self.y += noteSpeed; }; self.flash = function () { tween(note, { scaleX: 1.2 }, { duration: 80, onFinish: function onFinish() { tween(note, { scaleX: 1 }, { duration: 80 }); } }); }; // Make hold notes tappable directly self.down = function (x, y, obj) { if (!self.active) return; // Only allow hit if note is within hit window of hit line and not already being held var dist = Math.abs(self.y - NOTE_HIT_Y); // holdNote's anchorY is 0 if (dist < HIT_WINDOW && !self.held) { // Play noteHit sound when holdNote is tapped LK.getSound('noteHit').play(); self.held = true; self.active = false; self.flash(); addScore(dist < 60 ? 300 : 100, dist < 60); tween(self, { alpha: 0 }, { duration: 200, onFinish: function onFinish() { self.destroy(); } }); } }; return self; }); // Tap Note var TapNote = Container.expand(function () { var self = Container.call(this); var note = self.attachAsset('tapNote', { anchorX: 0.5, anchorY: 0.5 }); self.type = 'tap'; self.hit = false; self.lane = 0; self.time = 0; // When the note should be hit (in ms) self.active = true; self.update = function () { // Move down at constant speed (set externally) if (!self.active) return; self.y += noteSpeed; }; // Visual feedback for hit self.flash = function () { tween(note, { scaleX: 1.3, scaleY: 1.3 }, { duration: 80, easing: tween.easeOut, onFinish: function onFinish() { tween(note, { scaleX: 1, scaleY: 1 }, { duration: 80 }); } }); }; // Make tap notes tappable directly self.down = function (x, y, obj) { if (!self.active) return; // Only allow hit if note is within hit window of hit line var dist = Math.abs(self.y + 60 - NOTE_HIT_Y); // 60 = half note height if (dist < HIT_WINDOW) { self.active = false; self.flash(); addScore(dist < 60 ? 300 : 100, dist < 60); LK.getSound('noteHit').play(); tween(self, { alpha: 0 }, { duration: 200, onFinish: function onFinish() { self.destroy(); } }); } }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x181818 }); /**** * Game Code ****/ // We'll use simple shapes for notes and lanes, and a sound for note hit feedback. // Note: Assets are auto-created by LK based on usage below. // --- Game Constants --- var NUM_LANES = 4; var LANE_WIDTH = 2048 / NUM_LANES; var NOTE_HIT_Y = 2300; // Y position where notes should be hit var HIT_WINDOW = 180; // px window for perfect/good var HOLD_EXTRA_WINDOW = 250; // px window for hold end var noteSpeed = 18; // px per frame, will increase as song progresses // --- Game State --- var notes = []; // All notes in play var currentNoteIndex = 0; // For spawning notes var score = 0; var combo = 0; var maxCombo = 0; var streakTxt, scoreTxt, comboTxt; var lanes = []; var isPlaying = false; var songStartTime = 0; var lastTickTime = 0; var songDuration = 60000; // 60s for MVP var songNotes = []; // Array of note objects to spawn var activeTouches = {}; // Track touches by id // --- Lane Setup --- for (var i = 0; i < NUM_LANES; i++) { var lane = LK.getAsset('lane', { anchorX: 0.5, anchorY: 0, x: LANE_WIDTH * i + LANE_WIDTH / 2, y: 0, width: LANE_WIDTH - 10, height: 2732 }); game.addChild(lane); lanes.push(lane); } // --- Hit Line --- var hitLine = LK.getAsset('lane', { anchorX: 0, anchorY: 0.5, x: 0, y: NOTE_HIT_Y, width: 2048, height: 12 }); hitLine.alpha = 0.3; game.addChild(hitLine); // --- GUI --- scoreTxt = new Text2('Score: 0', { size: 90, fill: "#fff" }); scoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(scoreTxt); comboTxt = new Text2('', { size: 70, fill: 0xFFE066 }); comboTxt.anchor.set(0.5, 0); LK.gui.top.addChild(comboTxt); streakTxt = new Text2('', { size: 60, fill: 0x7ED321 }); streakTxt.anchor.set(0.5, 0); LK.gui.top.addChild(streakTxt); // --- Song Data (MVP: simple hardcoded pattern) --- /* Each note: {type: 'tap'|'hold'|'drag', lane: 0-3, time: ms, [duration], [targetLane]} */ songNotes = [{ type: 'tap', lane: 0, time: 800 }, { type: 'tap', lane: 1, time: 1200 }, { type: 'tap', lane: 2, time: 1600 }, { type: 'tap', lane: 3, time: 2000 }, { type: 'hold', lane: 1, time: 2600, duration: 1200 }, { type: 'tap', lane: 0, time: 3200 }, { type: 'drag', lane: 2, targetLane: 3, time: 4000 }, { type: 'tap', lane: 3, time: 4800 }, { type: 'tap', lane: 1, time: 5200 }, { type: 'hold', lane: 0, time: 6000, duration: 1500 }, { type: 'tap', lane: 2, time: 7000 }, { type: 'drag', lane: 1, targetLane: 0, time: 8000 }, { type: 'tap', lane: 3, time: 9000 }, { type: 'tap', lane: 2, time: 9400 }, { type: 'hold', lane: 3, time: 10000, duration: 1200 } // ... (repeat or add more for MVP) ]; // --- Helper Functions --- function getLaneX(laneIdx) { return LANE_WIDTH * laneIdx + LANE_WIDTH / 2; } // --- Note Spawning --- function spawnNotes() { // Spawn notes that should appear now (based on time) var now = Date.now() - songStartTime; while (currentNoteIndex < songNotes.length && songNotes[currentNoteIndex].time - now < 1800) { var n = songNotes[currentNoteIndex]; var noteObj; if (n.type === 'tap') { noteObj = new TapNote(); } else if (n.type === 'hold') { noteObj = new HoldNote(); noteObj.duration = n.duration; } else if (n.type === 'drag') { noteObj = new DragNote(); noteObj.targetLane = n.targetLane; } noteObj.lane = n.lane; noteObj.x = getLaneX(n.lane); // Calculate y so it reaches hit line at n.time var timeToHit = n.time - now; noteObj.y = NOTE_HIT_Y - noteSpeed * (timeToHit / (1000 / 60)); noteObj.time = n.time; notes.push(noteObj); game.addChild(noteObj); currentNoteIndex++; } } // --- Scoring --- function addScore(amount, isPerfect) { score += amount; LK.setScore(score); scoreTxt.setText('Score: ' + score); if (isPerfect) { combo++; if (combo > maxCombo) maxCombo = combo; comboTxt.setText('Combo: ' + combo); streakTxt.setText('Perfect!'); } else { combo = 0; comboTxt.setText(''); streakTxt.setText('Good'); } } // --- Miss Handling --- function missNote(note) { note.active = false; combo = 0; comboTxt.setText(''); streakTxt.setText('Miss'); tween(note, { alpha: 0 }, { duration: 200, onFinish: function onFinish() { note.destroy(); } }); } // --- Touch Handling --- game.down = function (x, y, obj) { // Find which lane was touched var laneIdx = Math.floor(x / LANE_WIDTH); var closest = null; var minDist = 99999; var now = Date.now() - songStartTime; // Find the closest active note in this lane within hit window for (var i = 0; i < notes.length; i++) { var n = notes[i]; if (!n.active) continue; if (n.lane !== laneIdx) continue; if (n.type === 'tap' || n.type === 'drag') { var dist = Math.abs(n.y + 60 - NOTE_HIT_Y); // 60 = half note height if (dist < HIT_WINDOW && dist < minDist) { minDist = dist; closest = n; } } else if (n.type === 'hold') { // For hold, check if touch is near start var dist = Math.abs(n.y - NOTE_HIT_Y); if (dist < HIT_WINDOW && dist < minDist) { minDist = dist; closest = n; } } } if (closest) { if (closest.type === 'tap') { closest.active = false; closest.flash(); addScore(minDist < 60 ? 300 : 100, minDist < 60); LK.getSound('noteHit').play(); tween(closest, { alpha: 0 }, { duration: 200, onFinish: function onFinish() { closest.destroy(); } }); } else if (closest.type === 'hold') { // Start hold tracking closest.held = true; closest.holdStartY = y; closest.holdStartTime = now; activeTouches['hold'] = { note: closest, id: obj && obj.event && typeof obj.event.identifier !== "undefined" ? obj.event.identifier : 0 }; closest.flash(); } else if (closest.type === 'drag') { // Start drag tracking closest.dragging = true; closest.dragStartX = x; closest.dragStartY = y; activeTouches['drag'] = { note: closest, id: obj && obj.event && typeof obj.event.identifier !== "undefined" ? obj.event.identifier : 0 }; closest.flash(); } } }; game.move = function (x, y, obj) { // Handle hold if (activeTouches['hold']) { var holdObj = activeTouches['hold'].note; if (!holdObj.active) return; // If finger moves off lane, cancel var laneIdx = Math.floor(x / LANE_WIDTH); if (laneIdx !== holdObj.lane) { missNote(holdObj); holdObj.active = false; delete activeTouches['hold']; return; } // If hold duration is satisfied and finger is still down, score var now = Date.now() - songStartTime; if (now > holdObj.time + holdObj.duration - HOLD_EXTRA_WINDOW) { holdObj.active = false; addScore(500, true); LK.getSound('noteHit').play(); tween(holdObj, { alpha: 0 }, { duration: 200, onFinish: function onFinish() { holdObj.destroy(); } }); delete activeTouches['hold']; } } // Handle drag if (activeTouches['drag']) { var dragObj = activeTouches['drag'].note; if (!dragObj.active) return; var laneIdx = Math.floor(x / LANE_WIDTH); // If drag reaches target lane and is near hit line, score var distY = Math.abs(dragObj.y + 60 - NOTE_HIT_Y); if (laneIdx === dragObj.targetLane && distY < HIT_WINDOW) { dragObj.active = false; addScore(400, true); LK.getSound('noteHit').play(); tween(dragObj, { alpha: 0 }, { duration: 200, onFinish: function onFinish() { dragObj.destroy(); } }); delete activeTouches['drag']; } } }; game.up = function (x, y, obj) { // End hold if (activeTouches['hold']) { var holdObj = activeTouches['hold'].note; if (holdObj.active) { // If released too early, miss var now = Date.now() - songStartTime; if (now < holdObj.time + holdObj.duration - HOLD_EXTRA_WINDOW) { missNote(holdObj); } } delete activeTouches['hold']; } // End drag if (activeTouches['drag']) { var dragObj = activeTouches['drag'].note; if (dragObj.active) { // If not at target lane, miss missNote(dragObj); } delete activeTouches['drag']; } }; // --- Game Update Loop --- game.update = function () { if (!isPlaying) return; var now = Date.now() - songStartTime; spawnNotes(); // Update notes for (var i = notes.length - 1; i >= 0; i--) { var n = notes[i]; n.update(); // Remove notes that have passed hit line and not hit if (n.active) { if (n.type === 'tap' || n.type === 'drag') { if (n.y > NOTE_HIT_Y + HIT_WINDOW) { missNote(n); notes.splice(i, 1); } } else if (n.type === 'hold') { if (now > n.time + n.duration + HOLD_EXTRA_WINDOW) { if (n.active) missNote(n); notes.splice(i, 1); } } } else { notes.splice(i, 1); } } // Increase note speed as song progresses (simple linear ramp) noteSpeed = 18 + Math.floor(now / 12000) * 2; // End of song if (now > songDuration) { isPlaying = false; streakTxt.setText('Song Complete!'); LK.showYouWin(); } }; // --- Start Game --- function startGame() { // Reset state notes = []; currentNoteIndex = 0; score = 0; combo = 0; maxCombo = 0; scoreTxt.setText('Score: 0'); comboTxt.setText(''); streakTxt.setText(''); noteSpeed = 18; isPlaying = true; songStartTime = Date.now(); LK.setScore(0); LK.playMusic('song1'); } startGame();
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Drag Note
var DragNote = Container.expand(function () {
var self = Container.call(this);
var note = self.attachAsset('dragNote', {
anchorX: 0.5,
anchorY: 0.5
});
self.type = 'drag';
self.lane = 0;
self.targetLane = 0;
self.time = 0; // When the drag should start (in ms)
self.active = true;
self.update = function () {
if (!self.active) return;
self.y += noteSpeed;
};
// Make drag notes tappable directly
self.down = function (x, y, obj) {
if (!self.active) return;
// Only allow hit if note is within hit window of hit line and not already being dragged
var dist = Math.abs(self.y + 60 - NOTE_HIT_Y); // 60 = half note height
if (dist < HIT_WINDOW && !self.dragging) {
// Play noteHit sound when dragNote is tapped
LK.getSound('noteHit').play();
self.active = false;
self.flash();
addScore(dist < 60 ? 300 : 100, dist < 60);
tween(self, {
alpha: 0
}, {
duration: 200,
onFinish: function onFinish() {
self.destroy();
}
});
}
};
self.flash = function () {
tween(note, {
scaleX: 1.3,
scaleY: 1.3
}, {
duration: 80,
onFinish: function onFinish() {
tween(note, {
scaleX: 1,
scaleY: 1
}, {
duration: 80
});
}
});
};
return self;
});
// Hold Note
var HoldNote = Container.expand(function () {
var self = Container.call(this);
var note = self.attachAsset('holdNote', {
anchorX: 0.5,
anchorY: 0
});
self.type = 'hold';
self.lane = 0;
self.time = 0; // When the hold starts (in ms)
self.duration = 1000; // How long to hold (ms)
self.held = false;
self.active = true;
self.update = function () {
if (!self.active) return;
self.y += noteSpeed;
};
self.flash = function () {
tween(note, {
scaleX: 1.2
}, {
duration: 80,
onFinish: function onFinish() {
tween(note, {
scaleX: 1
}, {
duration: 80
});
}
});
};
// Make hold notes tappable directly
self.down = function (x, y, obj) {
if (!self.active) return;
// Only allow hit if note is within hit window of hit line and not already being held
var dist = Math.abs(self.y - NOTE_HIT_Y); // holdNote's anchorY is 0
if (dist < HIT_WINDOW && !self.held) {
// Play noteHit sound when holdNote is tapped
LK.getSound('noteHit').play();
self.held = true;
self.active = false;
self.flash();
addScore(dist < 60 ? 300 : 100, dist < 60);
tween(self, {
alpha: 0
}, {
duration: 200,
onFinish: function onFinish() {
self.destroy();
}
});
}
};
return self;
});
// Tap Note
var TapNote = Container.expand(function () {
var self = Container.call(this);
var note = self.attachAsset('tapNote', {
anchorX: 0.5,
anchorY: 0.5
});
self.type = 'tap';
self.hit = false;
self.lane = 0;
self.time = 0; // When the note should be hit (in ms)
self.active = true;
self.update = function () {
// Move down at constant speed (set externally)
if (!self.active) return;
self.y += noteSpeed;
};
// Visual feedback for hit
self.flash = function () {
tween(note, {
scaleX: 1.3,
scaleY: 1.3
}, {
duration: 80,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(note, {
scaleX: 1,
scaleY: 1
}, {
duration: 80
});
}
});
};
// Make tap notes tappable directly
self.down = function (x, y, obj) {
if (!self.active) return;
// Only allow hit if note is within hit window of hit line
var dist = Math.abs(self.y + 60 - NOTE_HIT_Y); // 60 = half note height
if (dist < HIT_WINDOW) {
self.active = false;
self.flash();
addScore(dist < 60 ? 300 : 100, dist < 60);
LK.getSound('noteHit').play();
tween(self, {
alpha: 0
}, {
duration: 200,
onFinish: function onFinish() {
self.destroy();
}
});
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x181818
});
/****
* Game Code
****/
// We'll use simple shapes for notes and lanes, and a sound for note hit feedback.
// Note: Assets are auto-created by LK based on usage below.
// --- Game Constants ---
var NUM_LANES = 4;
var LANE_WIDTH = 2048 / NUM_LANES;
var NOTE_HIT_Y = 2300; // Y position where notes should be hit
var HIT_WINDOW = 180; // px window for perfect/good
var HOLD_EXTRA_WINDOW = 250; // px window for hold end
var noteSpeed = 18; // px per frame, will increase as song progresses
// --- Game State ---
var notes = []; // All notes in play
var currentNoteIndex = 0; // For spawning notes
var score = 0;
var combo = 0;
var maxCombo = 0;
var streakTxt, scoreTxt, comboTxt;
var lanes = [];
var isPlaying = false;
var songStartTime = 0;
var lastTickTime = 0;
var songDuration = 60000; // 60s for MVP
var songNotes = []; // Array of note objects to spawn
var activeTouches = {}; // Track touches by id
// --- Lane Setup ---
for (var i = 0; i < NUM_LANES; i++) {
var lane = LK.getAsset('lane', {
anchorX: 0.5,
anchorY: 0,
x: LANE_WIDTH * i + LANE_WIDTH / 2,
y: 0,
width: LANE_WIDTH - 10,
height: 2732
});
game.addChild(lane);
lanes.push(lane);
}
// --- Hit Line ---
var hitLine = LK.getAsset('lane', {
anchorX: 0,
anchorY: 0.5,
x: 0,
y: NOTE_HIT_Y,
width: 2048,
height: 12
});
hitLine.alpha = 0.3;
game.addChild(hitLine);
// --- GUI ---
scoreTxt = new Text2('Score: 0', {
size: 90,
fill: "#fff"
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
comboTxt = new Text2('', {
size: 70,
fill: 0xFFE066
});
comboTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(comboTxt);
streakTxt = new Text2('', {
size: 60,
fill: 0x7ED321
});
streakTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(streakTxt);
// --- Song Data (MVP: simple hardcoded pattern) ---
/*
Each note: {type: 'tap'|'hold'|'drag', lane: 0-3, time: ms, [duration], [targetLane]}
*/
songNotes = [{
type: 'tap',
lane: 0,
time: 800
}, {
type: 'tap',
lane: 1,
time: 1200
}, {
type: 'tap',
lane: 2,
time: 1600
}, {
type: 'tap',
lane: 3,
time: 2000
}, {
type: 'hold',
lane: 1,
time: 2600,
duration: 1200
}, {
type: 'tap',
lane: 0,
time: 3200
}, {
type: 'drag',
lane: 2,
targetLane: 3,
time: 4000
}, {
type: 'tap',
lane: 3,
time: 4800
}, {
type: 'tap',
lane: 1,
time: 5200
}, {
type: 'hold',
lane: 0,
time: 6000,
duration: 1500
}, {
type: 'tap',
lane: 2,
time: 7000
}, {
type: 'drag',
lane: 1,
targetLane: 0,
time: 8000
}, {
type: 'tap',
lane: 3,
time: 9000
}, {
type: 'tap',
lane: 2,
time: 9400
}, {
type: 'hold',
lane: 3,
time: 10000,
duration: 1200
}
// ... (repeat or add more for MVP)
];
// --- Helper Functions ---
function getLaneX(laneIdx) {
return LANE_WIDTH * laneIdx + LANE_WIDTH / 2;
}
// --- Note Spawning ---
function spawnNotes() {
// Spawn notes that should appear now (based on time)
var now = Date.now() - songStartTime;
while (currentNoteIndex < songNotes.length && songNotes[currentNoteIndex].time - now < 1800) {
var n = songNotes[currentNoteIndex];
var noteObj;
if (n.type === 'tap') {
noteObj = new TapNote();
} else if (n.type === 'hold') {
noteObj = new HoldNote();
noteObj.duration = n.duration;
} else if (n.type === 'drag') {
noteObj = new DragNote();
noteObj.targetLane = n.targetLane;
}
noteObj.lane = n.lane;
noteObj.x = getLaneX(n.lane);
// Calculate y so it reaches hit line at n.time
var timeToHit = n.time - now;
noteObj.y = NOTE_HIT_Y - noteSpeed * (timeToHit / (1000 / 60));
noteObj.time = n.time;
notes.push(noteObj);
game.addChild(noteObj);
currentNoteIndex++;
}
}
// --- Scoring ---
function addScore(amount, isPerfect) {
score += amount;
LK.setScore(score);
scoreTxt.setText('Score: ' + score);
if (isPerfect) {
combo++;
if (combo > maxCombo) maxCombo = combo;
comboTxt.setText('Combo: ' + combo);
streakTxt.setText('Perfect!');
} else {
combo = 0;
comboTxt.setText('');
streakTxt.setText('Good');
}
}
// --- Miss Handling ---
function missNote(note) {
note.active = false;
combo = 0;
comboTxt.setText('');
streakTxt.setText('Miss');
tween(note, {
alpha: 0
}, {
duration: 200,
onFinish: function onFinish() {
note.destroy();
}
});
}
// --- Touch Handling ---
game.down = function (x, y, obj) {
// Find which lane was touched
var laneIdx = Math.floor(x / LANE_WIDTH);
var closest = null;
var minDist = 99999;
var now = Date.now() - songStartTime;
// Find the closest active note in this lane within hit window
for (var i = 0; i < notes.length; i++) {
var n = notes[i];
if (!n.active) continue;
if (n.lane !== laneIdx) continue;
if (n.type === 'tap' || n.type === 'drag') {
var dist = Math.abs(n.y + 60 - NOTE_HIT_Y); // 60 = half note height
if (dist < HIT_WINDOW && dist < minDist) {
minDist = dist;
closest = n;
}
} else if (n.type === 'hold') {
// For hold, check if touch is near start
var dist = Math.abs(n.y - NOTE_HIT_Y);
if (dist < HIT_WINDOW && dist < minDist) {
minDist = dist;
closest = n;
}
}
}
if (closest) {
if (closest.type === 'tap') {
closest.active = false;
closest.flash();
addScore(minDist < 60 ? 300 : 100, minDist < 60);
LK.getSound('noteHit').play();
tween(closest, {
alpha: 0
}, {
duration: 200,
onFinish: function onFinish() {
closest.destroy();
}
});
} else if (closest.type === 'hold') {
// Start hold tracking
closest.held = true;
closest.holdStartY = y;
closest.holdStartTime = now;
activeTouches['hold'] = {
note: closest,
id: obj && obj.event && typeof obj.event.identifier !== "undefined" ? obj.event.identifier : 0
};
closest.flash();
} else if (closest.type === 'drag') {
// Start drag tracking
closest.dragging = true;
closest.dragStartX = x;
closest.dragStartY = y;
activeTouches['drag'] = {
note: closest,
id: obj && obj.event && typeof obj.event.identifier !== "undefined" ? obj.event.identifier : 0
};
closest.flash();
}
}
};
game.move = function (x, y, obj) {
// Handle hold
if (activeTouches['hold']) {
var holdObj = activeTouches['hold'].note;
if (!holdObj.active) return;
// If finger moves off lane, cancel
var laneIdx = Math.floor(x / LANE_WIDTH);
if (laneIdx !== holdObj.lane) {
missNote(holdObj);
holdObj.active = false;
delete activeTouches['hold'];
return;
}
// If hold duration is satisfied and finger is still down, score
var now = Date.now() - songStartTime;
if (now > holdObj.time + holdObj.duration - HOLD_EXTRA_WINDOW) {
holdObj.active = false;
addScore(500, true);
LK.getSound('noteHit').play();
tween(holdObj, {
alpha: 0
}, {
duration: 200,
onFinish: function onFinish() {
holdObj.destroy();
}
});
delete activeTouches['hold'];
}
}
// Handle drag
if (activeTouches['drag']) {
var dragObj = activeTouches['drag'].note;
if (!dragObj.active) return;
var laneIdx = Math.floor(x / LANE_WIDTH);
// If drag reaches target lane and is near hit line, score
var distY = Math.abs(dragObj.y + 60 - NOTE_HIT_Y);
if (laneIdx === dragObj.targetLane && distY < HIT_WINDOW) {
dragObj.active = false;
addScore(400, true);
LK.getSound('noteHit').play();
tween(dragObj, {
alpha: 0
}, {
duration: 200,
onFinish: function onFinish() {
dragObj.destroy();
}
});
delete activeTouches['drag'];
}
}
};
game.up = function (x, y, obj) {
// End hold
if (activeTouches['hold']) {
var holdObj = activeTouches['hold'].note;
if (holdObj.active) {
// If released too early, miss
var now = Date.now() - songStartTime;
if (now < holdObj.time + holdObj.duration - HOLD_EXTRA_WINDOW) {
missNote(holdObj);
}
}
delete activeTouches['hold'];
}
// End drag
if (activeTouches['drag']) {
var dragObj = activeTouches['drag'].note;
if (dragObj.active) {
// If not at target lane, miss
missNote(dragObj);
}
delete activeTouches['drag'];
}
};
// --- Game Update Loop ---
game.update = function () {
if (!isPlaying) return;
var now = Date.now() - songStartTime;
spawnNotes();
// Update notes
for (var i = notes.length - 1; i >= 0; i--) {
var n = notes[i];
n.update();
// Remove notes that have passed hit line and not hit
if (n.active) {
if (n.type === 'tap' || n.type === 'drag') {
if (n.y > NOTE_HIT_Y + HIT_WINDOW) {
missNote(n);
notes.splice(i, 1);
}
} else if (n.type === 'hold') {
if (now > n.time + n.duration + HOLD_EXTRA_WINDOW) {
if (n.active) missNote(n);
notes.splice(i, 1);
}
}
} else {
notes.splice(i, 1);
}
}
// Increase note speed as song progresses (simple linear ramp)
noteSpeed = 18 + Math.floor(now / 12000) * 2;
// End of song
if (now > songDuration) {
isPlaying = false;
streakTxt.setText('Song Complete!');
LK.showYouWin();
}
};
// --- Start Game ---
function startGame() {
// Reset state
notes = [];
currentNoteIndex = 0;
score = 0;
combo = 0;
maxCombo = 0;
scoreTxt.setText('Score: 0');
comboTxt.setText('');
streakTxt.setText('');
noteSpeed = 18;
isPlaying = true;
songStartTime = Date.now();
LK.setScore(0);
LK.playMusic('song1');
}
startGame();