Code edit (3 edits merged)
Please save this source code
User prompt
Please fix the bug: 'Graphics is not a constructor' in or related to this line: 'var hitZoneLine = new Graphics();' Line Number: 242
Code edit (1 edits merged)
Please save this source code
User prompt
Please fix the bug: 'TypeError: LK.getTime is not a function' in or related to this line: 'var now = LK.getTime();' Line Number: 535
User prompt
Please fix the bug: 'LK.getTime is not a function' in or related to this line: 'gameStartTime = LK.getTime();' Line Number: 549
Code edit (1 edits merged)
Please save this source code
User prompt
Beat Swipe: Rhythm Saber
Initial prompt
Create a 2D rhythm game framework inspired by Beat Saber. Game mechanics: - Notes (circles or shapes) appear far away and grow in size as they "approach" the player, simulating a forward motion toward the screen. - There are 3 types of notes: 1. Tap notes — the player must click exactly when the note reaches maximum size. 2. Swipe notes — the player must perform a quick swipe with the mouse through the note in a given direction (left, right, up, down). 3. Trap notes — these must be avoided entirely. Clicking or swiping them results in a combo break. - Notes appear at specific times and types defined in an array. Controls: - Mouse only. Recognize: - Clicks (onMouseDown + release timing). - Swipes (track onMouseDown → onMouseUp direction and distance). Scoring: - Perfect timing (±0.1 sec) gives max points. - Good timing (±0.3 sec) gives half points. - Missed or incorrect interaction resets combo. - Trap interaction resets combo too. Other features: - Notes scale up as they approach to simulate perspective. - Show combo counter and score. - Simple placeholder graphics and dark background are fine. - Music and visuals will be added manually later. Code structure must clearly separate: - Note spawn logic. - Input detection (click + swipe). - Timing window checks. - Scoring and combo logic. Use simple assets for now (colored shapes). Add comments to explain functions.
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Note Types: 'tap', 'swipe', 'trap'
// Each note spawns at a given time (ms), type, and swipeDir (for swipe notes)
var Note = Container.expand(function () {
var self = Container.call(this);
// Properties
self.noteType = 'tap'; // 'tap', 'swipe', 'trap'
self.swipeDir = null; // 'left', 'right', 'up', 'down' (for swipe notes)
self.spawnTime = 0; // ms
self.hit = false; // Whether this note has been hit
self.judged = false; // Whether this note has been judged (hit/miss)
self.scaleStart = 0.3; // Initial scale
self.scaleEnd = 1.2; // Final scale at hit time
self.centerX = 2048 / 2;
self.centerY = 1800; // Target area (bottom center)
self.startY = 600; // Where notes start scaling up from
// Attach asset based on note type
var noteAsset;
if (self.noteType === 'tap') {
noteAsset = self.attachAsset('tapNote', {
anchorX: 0.5,
anchorY: 0.5
});
} else if (self.noteType === 'swipe') {
noteAsset = self.attachAsset('swipeNote', {
anchorX: 0.5,
anchorY: 0.5
});
} else if (self.noteType === 'trap') {
noteAsset = self.attachAsset('trapNote', {
anchorX: 0.5,
anchorY: 0.5
});
}
self.noteAsset = noteAsset;
// For swipe notes, add a direction indicator (arrow)
if (self.noteType === 'swipe') {
var arrow = new Text2('', {
size: 80,
fill: 0xFFFFFF
});
arrow.anchor.set(0.5, 0.5);
if (self.swipeDir === 'left') arrow.setText('←');else if (self.swipeDir === 'right') arrow.setText('→');else if (self.swipeDir === 'up') arrow.setText('↑');else if (self.swipeDir === 'down') arrow.setText('↓');
self.addChild(arrow);
self.arrow = arrow;
}
// For hit feedback
self.showHitFeedback = function (result) {
var feedback = LK.getAsset('hitFeedback', {
anchorX: 0.5,
anchorY: 0.5,
x: 0,
y: 0,
scaleX: 0.7,
scaleY: 0.7,
alpha: 0.7
});
if (result === 'perfect') feedback.tint = 0xffff00;else if (result === 'good') feedback.tint = 0x00ff00;else feedback.tint = 0xff0000;
self.addChild(feedback);
tween(feedback, {
alpha: 0,
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 350,
easing: tween.easeOut,
onFinish: function onFinish() {
feedback.destroy();
}
});
};
// Called every tick
self.update = function () {
// Progress: 0 (spawn) to 1 (hit time)
var now = LK.getTime();
var progress = (now - self.spawnTime) / noteTravelTime;
if (progress < 0) progress = 0;
if (progress > 1) progress = 1;
// Scale up and move toward target
var scale = self.scaleStart + (self.scaleEnd - self.scaleStart) * progress;
self.scale.x = scale;
self.scale.y = scale;
// Y position: from startY to centerY
self.x = self.centerX;
self.y = self.startY + (self.centerY - self.startY) * progress;
// If not yet judged and past hit window, mark as miss
if (!self.judged && now > self.spawnTime + hitWindowGood) {
self.judged = true;
if (self.noteType !== 'trap') {
// Missed note
game.onNoteMiss(self);
}
}
};
// For hit detection
self.isInHitWindow = function () {
var now = LK.getTime();
var dt = Math.abs(now - self.spawnTime);
return dt <= hitWindowGood;
};
self.getHitAccuracy = function () {
var now = LK.getTime();
var dt = Math.abs(now - self.spawnTime);
if (dt <= hitWindowPerfect) return 'perfect';
if (dt <= hitWindowGood) return 'good';
return 'miss';
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x181828
});
/****
* Game Code
****/
// Hit Feedback: White circle
// Trap Note: Red triangle (simulate with a red ellipse for simplicity)
// Swipe Note: Green rectangle
// Tap Note: Blue circle
// --- Rhythm Map (ms, type, [swipeDir]) ---
// For MVP, a short hardcoded map
var rhythmMap = [{
time: 1000,
type: 'tap'
}, {
time: 1800,
type: 'tap'
}, {
time: 2600,
type: 'swipe',
swipeDir: 'left'
}, {
time: 3400,
type: 'tap'
}, {
time: 4200,
type: 'swipe',
swipeDir: 'right'
}, {
time: 5000,
type: 'trap'
}, {
time: 5800,
type: 'tap'
}, {
time: 6600,
type: 'swipe',
swipeDir: 'up'
}, {
time: 7400,
type: 'tap'
}, {
time: 8200,
type: 'trap'
}, {
time: 9000,
type: 'swipe',
swipeDir: 'down'
}, {
time: 9800,
type: 'tap'
}, {
time: 10600,
type: 'tap'
}, {
time: 11400,
type: 'swipe',
swipeDir: 'left'
}, {
time: 12200,
type: 'trap'
}, {
time: 13000,
type: 'tap'
}];
// --- Timing Windows (ms) ---
var noteTravelTime = 1200; // ms: time from spawn to hit area
var hitWindowPerfect = 120; // ms
var hitWindowGood = 260; // ms
// --- State ---
var notes = [];
var nextNoteIdx = 0;
var gameStartTime = 0;
var score = 0;
var combo = 0;
var maxCombo = 0;
var lastInput = null; // {x, y, time}
var swipeStart = null; // {x, y, time}
var inputLocked = false; // Prevent double hits
// --- GUI ---
var scoreTxt = new Text2('Score: 0', {
size: 100,
fill: 0xFFFFFF
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
var comboTxt = new Text2('Combo: 0', {
size: 80,
fill: 0xFFFF00
});
comboTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(comboTxt);
comboTxt.y = 120;
// --- Helper: Reset State ---
function resetGameState() {
notes = [];
nextNoteIdx = 0;
score = 0;
combo = 0;
maxCombo = 0;
lastInput = null;
swipeStart = null;
inputLocked = false;
scoreTxt.setText('Score: 0');
comboTxt.setText('Combo: 0');
}
// --- Helper: Spawn Notes ---
function spawnNotes() {
var now = LK.getTime();
while (nextNoteIdx < rhythmMap.length) {
var noteData = rhythmMap[nextNoteIdx];
if (noteData.time - noteTravelTime <= now - gameStartTime) {
// Spawn note
var n = new Note();
n.noteType = noteData.type;
n.spawnTime = gameStartTime + noteData.time;
if (n.noteType === 'swipe') n.swipeDir = noteData.swipeDir;
// Re-attach asset for correct type
n.removeChildren();
if (n.noteType === 'tap') {
n.noteAsset = n.attachAsset('tapNote', {
anchorX: 0.5,
anchorY: 0.5
});
} else if (n.noteType === 'swipe') {
n.noteAsset = n.attachAsset('swipeNote', {
anchorX: 0.5,
anchorY: 0.5
});
var arrow = new Text2('', {
size: 80,
fill: 0xFFFFFF
});
arrow.anchor.set(0.5, 0.5);
if (n.swipeDir === 'left') arrow.setText('←');else if (n.swipeDir === 'right') arrow.setText('→');else if (n.swipeDir === 'up') arrow.setText('↑');else if (n.swipeDir === 'down') arrow.setText('↓');
n.addChild(arrow);
n.arrow = arrow;
} else if (n.noteType === 'trap') {
n.noteAsset = n.attachAsset('trapNote', {
anchorX: 0.5,
anchorY: 0.5
});
}
n.scale.x = n.scaleStart;
n.scale.y = n.scaleStart;
n.x = n.centerX;
n.y = n.startY;
notes.push(n);
game.addChild(n);
nextNoteIdx++;
} else {
break;
}
}
}
// --- Helper: Remove Old Notes ---
function removeOldNotes() {
var now = LK.getTime();
for (var i = notes.length - 1; i >= 0; i--) {
var n = notes[i];
if (n.judged && now > n.spawnTime + hitWindowGood + 400) {
n.destroy();
notes.splice(i, 1);
}
}
}
// --- Helper: Find Closest Note in Hit Area ---
function findNoteAt(x, y, type) {
var now = LK.getTime();
var best = null;
var bestDt = 99999;
for (var i = 0; i < notes.length; i++) {
var n = notes[i];
if (n.judged) continue;
if (n.noteType !== type) continue;
// Only consider notes in hit window
var dt = Math.abs(now - n.spawnTime);
if (dt > hitWindowGood) continue;
// Check if input is within note's area (use scaled size)
var dx = x - n.x;
var dy = y - n.y;
var r = n.noteAsset.width * n.scale.x / 2;
if (type === 'swipe') {
// Rectangle: width/height
var w = n.noteAsset.width * n.scale.x / 2;
var h = n.noteAsset.height * n.scale.y / 2;
if (dx < -w || dx > w || dy < -h || dy > h) continue;
} else {
// Circle: distance
if (dx * dx + dy * dy > r * r) continue;
}
if (dt < bestDt) {
best = n;
bestDt = dt;
}
}
return best;
}
// --- Helper: Find Trap Note at Position ---
function findTrapNoteAt(x, y) {
var now = LK.getTime();
for (var i = 0; i < notes.length; i++) {
var n = notes[i];
if (n.judged) continue;
if (n.noteType !== 'trap') continue;
var dt = Math.abs(now - n.spawnTime);
if (dt > hitWindowGood) continue;
var dx = x - n.x;
var dy = y - n.y;
var r = n.noteAsset.width * n.scale.x / 2;
if (dx * dx + dy * dy <= r * r) return n;
}
return null;
}
// --- Helper: Score/Combo ---
function addScore(result) {
if (result === 'perfect') score += 100;else if (result === 'good') score += 50;
scoreTxt.setText('Score: ' + score);
}
function addCombo() {
combo += 1;
if (combo > maxCombo) maxCombo = combo;
comboTxt.setText('Combo: ' + combo);
}
function resetCombo() {
combo = 0;
comboTxt.setText('Combo: 0');
}
// --- Game Over/Win ---
function checkGameEnd() {
if (nextNoteIdx >= rhythmMap.length && notes.length === 0) {
// All notes finished
LK.showYouWin();
}
}
// --- Miss Handler ---
game.onNoteMiss = function (note) {
note.judged = true;
note.showHitFeedback('miss');
resetCombo();
LK.effects.flashObject(note, 0xff0000, 300);
};
// --- Input Handling ---
game.down = function (x, y, obj) {
if (inputLocked) return;
lastInput = {
x: x,
y: y,
time: LK.getTime()
};
swipeStart = {
x: x,
y: y,
time: LK.getTime()
};
// Check for trap note
var trap = findTrapNoteAt(x, y);
if (trap && !trap.judged) {
trap.judged = true;
trap.showHitFeedback('miss');
resetCombo();
LK.effects.flashScreen(0xff0000, 400);
inputLocked = true;
LK.setTimeout(function () {
inputLocked = false;
}, 200);
return;
}
// Check for tap note
var tap = findNoteAt(x, y, 'tap');
if (tap && !tap.judged) {
var result = tap.getHitAccuracy();
tap.judged = true;
tap.showHitFeedback(result);
if (result !== 'miss') {
addScore(result);
addCombo();
} else {
resetCombo();
}
inputLocked = true;
LK.setTimeout(function () {
inputLocked = false;
}, 120);
return;
}
// Check for swipe note (start of swipe)
var swipe = findNoteAt(x, y, 'swipe');
if (swipe && !swipe.judged) {
swipeStart.note = swipe;
}
};
game.up = function (x, y, obj) {
if (inputLocked) return;
if (!swipeStart) return;
var swipe = swipeStart.note;
if (!swipe || swipe.judged) {
swipeStart = null;
return;
}
var dx = x - swipeStart.x;
var dy = y - swipeStart.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 80) {
swipeStart = null;
return; // Not a swipe
}
// Determine direction
var dir = null;
if (Math.abs(dx) > Math.abs(dy)) {
dir = dx > 0 ? 'right' : 'left';
} else {
dir = dy > 0 ? 'down' : 'up';
}
if (dir === swipe.swipeDir) {
var result = swipe.getHitAccuracy();
swipe.judged = true;
swipe.showHitFeedback(result);
if (result !== 'miss') {
addScore(result);
addCombo();
} else {
resetCombo();
}
} else {
// Wrong direction
swipe.judged = true;
swipe.showHitFeedback('miss');
resetCombo();
LK.effects.flashObject(swipe, 0xff0000, 300);
}
inputLocked = true;
LK.setTimeout(function () {
inputLocked = false;
}, 120);
swipeStart = null;
};
game.move = function (x, y, obj) {
// No drag needed
};
// --- Main Update Loop ---
game.update = function () {
var now = LK.getTime();
// Spawn notes as needed
spawnNotes();
// Update all notes
for (var i = 0; i < notes.length; i++) {
notes[i].update();
}
// Remove old notes
removeOldNotes();
// End check
checkGameEnd();
};
// --- Start Game ---
resetGameState();
gameStartTime = LK.getTime(); ===================================================================
--- original.js
+++ change.js
@@ -1,6 +1,479 @@
-/****
+/****
+* Plugins
+****/
+var tween = LK.import("@upit/tween.v1");
+
+/****
+* Classes
+****/
+// Note Types: 'tap', 'swipe', 'trap'
+// Each note spawns at a given time (ms), type, and swipeDir (for swipe notes)
+var Note = Container.expand(function () {
+ var self = Container.call(this);
+ // Properties
+ self.noteType = 'tap'; // 'tap', 'swipe', 'trap'
+ self.swipeDir = null; // 'left', 'right', 'up', 'down' (for swipe notes)
+ self.spawnTime = 0; // ms
+ self.hit = false; // Whether this note has been hit
+ self.judged = false; // Whether this note has been judged (hit/miss)
+ self.scaleStart = 0.3; // Initial scale
+ self.scaleEnd = 1.2; // Final scale at hit time
+ self.centerX = 2048 / 2;
+ self.centerY = 1800; // Target area (bottom center)
+ self.startY = 600; // Where notes start scaling up from
+ // Attach asset based on note type
+ var noteAsset;
+ if (self.noteType === 'tap') {
+ noteAsset = self.attachAsset('tapNote', {
+ anchorX: 0.5,
+ anchorY: 0.5
+ });
+ } else if (self.noteType === 'swipe') {
+ noteAsset = self.attachAsset('swipeNote', {
+ anchorX: 0.5,
+ anchorY: 0.5
+ });
+ } else if (self.noteType === 'trap') {
+ noteAsset = self.attachAsset('trapNote', {
+ anchorX: 0.5,
+ anchorY: 0.5
+ });
+ }
+ self.noteAsset = noteAsset;
+ // For swipe notes, add a direction indicator (arrow)
+ if (self.noteType === 'swipe') {
+ var arrow = new Text2('', {
+ size: 80,
+ fill: 0xFFFFFF
+ });
+ arrow.anchor.set(0.5, 0.5);
+ if (self.swipeDir === 'left') arrow.setText('←');else if (self.swipeDir === 'right') arrow.setText('→');else if (self.swipeDir === 'up') arrow.setText('↑');else if (self.swipeDir === 'down') arrow.setText('↓');
+ self.addChild(arrow);
+ self.arrow = arrow;
+ }
+ // For hit feedback
+ self.showHitFeedback = function (result) {
+ var feedback = LK.getAsset('hitFeedback', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ x: 0,
+ y: 0,
+ scaleX: 0.7,
+ scaleY: 0.7,
+ alpha: 0.7
+ });
+ if (result === 'perfect') feedback.tint = 0xffff00;else if (result === 'good') feedback.tint = 0x00ff00;else feedback.tint = 0xff0000;
+ self.addChild(feedback);
+ tween(feedback, {
+ alpha: 0,
+ scaleX: 1.5,
+ scaleY: 1.5
+ }, {
+ duration: 350,
+ easing: tween.easeOut,
+ onFinish: function onFinish() {
+ feedback.destroy();
+ }
+ });
+ };
+ // Called every tick
+ self.update = function () {
+ // Progress: 0 (spawn) to 1 (hit time)
+ var now = LK.getTime();
+ var progress = (now - self.spawnTime) / noteTravelTime;
+ if (progress < 0) progress = 0;
+ if (progress > 1) progress = 1;
+ // Scale up and move toward target
+ var scale = self.scaleStart + (self.scaleEnd - self.scaleStart) * progress;
+ self.scale.x = scale;
+ self.scale.y = scale;
+ // Y position: from startY to centerY
+ self.x = self.centerX;
+ self.y = self.startY + (self.centerY - self.startY) * progress;
+ // If not yet judged and past hit window, mark as miss
+ if (!self.judged && now > self.spawnTime + hitWindowGood) {
+ self.judged = true;
+ if (self.noteType !== 'trap') {
+ // Missed note
+ game.onNoteMiss(self);
+ }
+ }
+ };
+ // For hit detection
+ self.isInHitWindow = function () {
+ var now = LK.getTime();
+ var dt = Math.abs(now - self.spawnTime);
+ return dt <= hitWindowGood;
+ };
+ self.getHitAccuracy = function () {
+ var now = LK.getTime();
+ var dt = Math.abs(now - self.spawnTime);
+ if (dt <= hitWindowPerfect) return 'perfect';
+ if (dt <= hitWindowGood) return 'good';
+ return 'miss';
+ };
+ return self;
+});
+
+/****
* Initialize Game
-****/
+****/
var game = new LK.Game({
- backgroundColor: 0x000000
-});
\ No newline at end of file
+ backgroundColor: 0x181828
+});
+
+/****
+* Game Code
+****/
+// Hit Feedback: White circle
+// Trap Note: Red triangle (simulate with a red ellipse for simplicity)
+// Swipe Note: Green rectangle
+// Tap Note: Blue circle
+// --- Rhythm Map (ms, type, [swipeDir]) ---
+// For MVP, a short hardcoded map
+var rhythmMap = [{
+ time: 1000,
+ type: 'tap'
+}, {
+ time: 1800,
+ type: 'tap'
+}, {
+ time: 2600,
+ type: 'swipe',
+ swipeDir: 'left'
+}, {
+ time: 3400,
+ type: 'tap'
+}, {
+ time: 4200,
+ type: 'swipe',
+ swipeDir: 'right'
+}, {
+ time: 5000,
+ type: 'trap'
+}, {
+ time: 5800,
+ type: 'tap'
+}, {
+ time: 6600,
+ type: 'swipe',
+ swipeDir: 'up'
+}, {
+ time: 7400,
+ type: 'tap'
+}, {
+ time: 8200,
+ type: 'trap'
+}, {
+ time: 9000,
+ type: 'swipe',
+ swipeDir: 'down'
+}, {
+ time: 9800,
+ type: 'tap'
+}, {
+ time: 10600,
+ type: 'tap'
+}, {
+ time: 11400,
+ type: 'swipe',
+ swipeDir: 'left'
+}, {
+ time: 12200,
+ type: 'trap'
+}, {
+ time: 13000,
+ type: 'tap'
+}];
+// --- Timing Windows (ms) ---
+var noteTravelTime = 1200; // ms: time from spawn to hit area
+var hitWindowPerfect = 120; // ms
+var hitWindowGood = 260; // ms
+// --- State ---
+var notes = [];
+var nextNoteIdx = 0;
+var gameStartTime = 0;
+var score = 0;
+var combo = 0;
+var maxCombo = 0;
+var lastInput = null; // {x, y, time}
+var swipeStart = null; // {x, y, time}
+var inputLocked = false; // Prevent double hits
+// --- GUI ---
+var scoreTxt = new Text2('Score: 0', {
+ size: 100,
+ fill: 0xFFFFFF
+});
+scoreTxt.anchor.set(0.5, 0);
+LK.gui.top.addChild(scoreTxt);
+var comboTxt = new Text2('Combo: 0', {
+ size: 80,
+ fill: 0xFFFF00
+});
+comboTxt.anchor.set(0.5, 0);
+LK.gui.top.addChild(comboTxt);
+comboTxt.y = 120;
+// --- Helper: Reset State ---
+function resetGameState() {
+ notes = [];
+ nextNoteIdx = 0;
+ score = 0;
+ combo = 0;
+ maxCombo = 0;
+ lastInput = null;
+ swipeStart = null;
+ inputLocked = false;
+ scoreTxt.setText('Score: 0');
+ comboTxt.setText('Combo: 0');
+}
+// --- Helper: Spawn Notes ---
+function spawnNotes() {
+ var now = LK.getTime();
+ while (nextNoteIdx < rhythmMap.length) {
+ var noteData = rhythmMap[nextNoteIdx];
+ if (noteData.time - noteTravelTime <= now - gameStartTime) {
+ // Spawn note
+ var n = new Note();
+ n.noteType = noteData.type;
+ n.spawnTime = gameStartTime + noteData.time;
+ if (n.noteType === 'swipe') n.swipeDir = noteData.swipeDir;
+ // Re-attach asset for correct type
+ n.removeChildren();
+ if (n.noteType === 'tap') {
+ n.noteAsset = n.attachAsset('tapNote', {
+ anchorX: 0.5,
+ anchorY: 0.5
+ });
+ } else if (n.noteType === 'swipe') {
+ n.noteAsset = n.attachAsset('swipeNote', {
+ anchorX: 0.5,
+ anchorY: 0.5
+ });
+ var arrow = new Text2('', {
+ size: 80,
+ fill: 0xFFFFFF
+ });
+ arrow.anchor.set(0.5, 0.5);
+ if (n.swipeDir === 'left') arrow.setText('←');else if (n.swipeDir === 'right') arrow.setText('→');else if (n.swipeDir === 'up') arrow.setText('↑');else if (n.swipeDir === 'down') arrow.setText('↓');
+ n.addChild(arrow);
+ n.arrow = arrow;
+ } else if (n.noteType === 'trap') {
+ n.noteAsset = n.attachAsset('trapNote', {
+ anchorX: 0.5,
+ anchorY: 0.5
+ });
+ }
+ n.scale.x = n.scaleStart;
+ n.scale.y = n.scaleStart;
+ n.x = n.centerX;
+ n.y = n.startY;
+ notes.push(n);
+ game.addChild(n);
+ nextNoteIdx++;
+ } else {
+ break;
+ }
+ }
+}
+// --- Helper: Remove Old Notes ---
+function removeOldNotes() {
+ var now = LK.getTime();
+ for (var i = notes.length - 1; i >= 0; i--) {
+ var n = notes[i];
+ if (n.judged && now > n.spawnTime + hitWindowGood + 400) {
+ n.destroy();
+ notes.splice(i, 1);
+ }
+ }
+}
+// --- Helper: Find Closest Note in Hit Area ---
+function findNoteAt(x, y, type) {
+ var now = LK.getTime();
+ var best = null;
+ var bestDt = 99999;
+ for (var i = 0; i < notes.length; i++) {
+ var n = notes[i];
+ if (n.judged) continue;
+ if (n.noteType !== type) continue;
+ // Only consider notes in hit window
+ var dt = Math.abs(now - n.spawnTime);
+ if (dt > hitWindowGood) continue;
+ // Check if input is within note's area (use scaled size)
+ var dx = x - n.x;
+ var dy = y - n.y;
+ var r = n.noteAsset.width * n.scale.x / 2;
+ if (type === 'swipe') {
+ // Rectangle: width/height
+ var w = n.noteAsset.width * n.scale.x / 2;
+ var h = n.noteAsset.height * n.scale.y / 2;
+ if (dx < -w || dx > w || dy < -h || dy > h) continue;
+ } else {
+ // Circle: distance
+ if (dx * dx + dy * dy > r * r) continue;
+ }
+ if (dt < bestDt) {
+ best = n;
+ bestDt = dt;
+ }
+ }
+ return best;
+}
+// --- Helper: Find Trap Note at Position ---
+function findTrapNoteAt(x, y) {
+ var now = LK.getTime();
+ for (var i = 0; i < notes.length; i++) {
+ var n = notes[i];
+ if (n.judged) continue;
+ if (n.noteType !== 'trap') continue;
+ var dt = Math.abs(now - n.spawnTime);
+ if (dt > hitWindowGood) continue;
+ var dx = x - n.x;
+ var dy = y - n.y;
+ var r = n.noteAsset.width * n.scale.x / 2;
+ if (dx * dx + dy * dy <= r * r) return n;
+ }
+ return null;
+}
+// --- Helper: Score/Combo ---
+function addScore(result) {
+ if (result === 'perfect') score += 100;else if (result === 'good') score += 50;
+ scoreTxt.setText('Score: ' + score);
+}
+function addCombo() {
+ combo += 1;
+ if (combo > maxCombo) maxCombo = combo;
+ comboTxt.setText('Combo: ' + combo);
+}
+function resetCombo() {
+ combo = 0;
+ comboTxt.setText('Combo: 0');
+}
+// --- Game Over/Win ---
+function checkGameEnd() {
+ if (nextNoteIdx >= rhythmMap.length && notes.length === 0) {
+ // All notes finished
+ LK.showYouWin();
+ }
+}
+// --- Miss Handler ---
+game.onNoteMiss = function (note) {
+ note.judged = true;
+ note.showHitFeedback('miss');
+ resetCombo();
+ LK.effects.flashObject(note, 0xff0000, 300);
+};
+// --- Input Handling ---
+game.down = function (x, y, obj) {
+ if (inputLocked) return;
+ lastInput = {
+ x: x,
+ y: y,
+ time: LK.getTime()
+ };
+ swipeStart = {
+ x: x,
+ y: y,
+ time: LK.getTime()
+ };
+ // Check for trap note
+ var trap = findTrapNoteAt(x, y);
+ if (trap && !trap.judged) {
+ trap.judged = true;
+ trap.showHitFeedback('miss');
+ resetCombo();
+ LK.effects.flashScreen(0xff0000, 400);
+ inputLocked = true;
+ LK.setTimeout(function () {
+ inputLocked = false;
+ }, 200);
+ return;
+ }
+ // Check for tap note
+ var tap = findNoteAt(x, y, 'tap');
+ if (tap && !tap.judged) {
+ var result = tap.getHitAccuracy();
+ tap.judged = true;
+ tap.showHitFeedback(result);
+ if (result !== 'miss') {
+ addScore(result);
+ addCombo();
+ } else {
+ resetCombo();
+ }
+ inputLocked = true;
+ LK.setTimeout(function () {
+ inputLocked = false;
+ }, 120);
+ return;
+ }
+ // Check for swipe note (start of swipe)
+ var swipe = findNoteAt(x, y, 'swipe');
+ if (swipe && !swipe.judged) {
+ swipeStart.note = swipe;
+ }
+};
+game.up = function (x, y, obj) {
+ if (inputLocked) return;
+ if (!swipeStart) return;
+ var swipe = swipeStart.note;
+ if (!swipe || swipe.judged) {
+ swipeStart = null;
+ return;
+ }
+ var dx = x - swipeStart.x;
+ var dy = y - swipeStart.y;
+ var dist = Math.sqrt(dx * dx + dy * dy);
+ if (dist < 80) {
+ swipeStart = null;
+ return; // Not a swipe
+ }
+ // Determine direction
+ var dir = null;
+ if (Math.abs(dx) > Math.abs(dy)) {
+ dir = dx > 0 ? 'right' : 'left';
+ } else {
+ dir = dy > 0 ? 'down' : 'up';
+ }
+ if (dir === swipe.swipeDir) {
+ var result = swipe.getHitAccuracy();
+ swipe.judged = true;
+ swipe.showHitFeedback(result);
+ if (result !== 'miss') {
+ addScore(result);
+ addCombo();
+ } else {
+ resetCombo();
+ }
+ } else {
+ // Wrong direction
+ swipe.judged = true;
+ swipe.showHitFeedback('miss');
+ resetCombo();
+ LK.effects.flashObject(swipe, 0xff0000, 300);
+ }
+ inputLocked = true;
+ LK.setTimeout(function () {
+ inputLocked = false;
+ }, 120);
+ swipeStart = null;
+};
+game.move = function (x, y, obj) {
+ // No drag needed
+};
+// --- Main Update Loop ---
+game.update = function () {
+ var now = LK.getTime();
+ // Spawn notes as needed
+ spawnNotes();
+ // Update all notes
+ for (var i = 0; i < notes.length; i++) {
+ notes[i].update();
+ }
+ // Remove old notes
+ removeOldNotes();
+ // End check
+ checkGameEnd();
+};
+// --- Start Game ---
+resetGameState();
+gameStartTime = LK.getTime();
\ No newline at end of file