Code edit (1 edits merged)
Please save this source code
Code edit (1 edits merged)
Please save this source code
User prompt
Please fix the bug: 'Uncaught TypeError: Cannot read properties of undefined (reading 'stringify')' in or related to this line: 'console.log("Swipe BBox:", JSON.stringify(swipeBoundingBox));' Line Number: 497
Code edit (7 edits merged)
Please save this source code
User prompt
Please fix the bug: 'Uncaught TypeError: Cannot read properties of undefined (reading 'stringify')' in or related to this line: 'console.log("Swipe BBox:", JSON.stringify(swipeBoundingBox));' Line Number: 497
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 ****/ var Note = Container.expand(function (noteType, swipeDir, targetHitTimeFull) { var self = Container.call(this); self.noteType = noteType || 'tap'; self.swipeDir = swipeDir || null; self.targetHitTime = targetHitTimeFull; self.visualSpawnTime = self.targetHitTime - noteTravelTime; self.hit = false; self.judged = false; self.scaleStart = 0.3; self.scaleEnd = 1.2; self.centerX = 2048 / 2; self.centerY = 1800; self.startY = 600; self.noteAsset = null; if (self.noteType === 'tap') { self.noteAsset = self.attachAsset('tapNote', { anchorX: 0.5, anchorY: 0.5 }); } else if (self.noteType === 'swipe') { self.noteAsset = self.attachAsset('swipeNote', { anchorX: 0.5, anchorY: 0.5 }); if (self.swipeDir) { 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; } } else if (self.noteType === 'trap') { self.noteAsset = self.attachAsset('trapNote', { anchorX: 0.5, anchorY: 0.5 }); } self.alpha = 0; // Initially invisible, will become visible when it's time 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() { if (feedback.parent) { feedback.destroy(); } } }); }; self.update = function () { var now = Date.now(); if (now < self.visualSpawnTime) { self.alpha = 0; // Ensure it's invisible before visual spawn time return; } if (self.alpha === 0) { // First time it becomes visible self.alpha = 1; } var elapsedTimeSinceSpawn = now - self.visualSpawnTime; var progress = elapsedTimeSinceSpawn / noteTravelTime; if (progress < 0) { progress = 0; } if (progress > 1) { progress = 1; } var scale = self.scaleStart + (self.scaleEnd - self.scaleStart) * progress; self.scale.x = scale; self.scale.y = scale; self.x = self.centerX; self.y = self.startY + (self.centerY - self.startY) * progress; if (!self.judged && now > self.targetHitTime + hitWindowGood) { self.judged = true; if (self.noteType !== 'trap') { game.onNoteMiss(self); } else { // Trap notes just disappear if not hit, no miss penalty unless clicked } } }; self.isInHitWindow = function () { var now = Date.now(); var dt = Math.abs(now - self.targetHitTime); return dt <= hitWindowGood; }; self.getHitAccuracy = function () { var now = Date.now(); var dt = Math.abs(now - self.targetHitTime); 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 ****/ 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' }]; var noteTravelTime = 1200; var hitWindowPerfect = 120; var hitWindowGood = 260; var notes = []; var nextNoteIdx = 0; var gameStartTime = 0; var score = 0; var combo = 0; var maxCombo = 0; var swipeStart = null; var inputLocked = false; var scoreTxt = new Text2('Score: 0', { size: 100, fill: 0xFFFFFF }); scoreTxt.anchor.set(0.5, 0); scoreTxt.x = 2048 / 2; scoreTxt.y = 20; LK.gui.top.addChild(scoreTxt); var comboTxt = new Text2('Combo: 0', { size: 80, fill: 0xFFFF00 }); comboTxt.anchor.set(0.5, 0); comboTxt.x = 2048 / 2; comboTxt.y = 130; LK.gui.top.addChild(comboTxt); function resetGameState() { notes.forEach(function (n) { if (n && n.parent) n.destroy(); }); notes = []; nextNoteIdx = 0; score = 0; combo = 0; maxCombo = 0; swipeStart = null; inputLocked = false; scoreTxt.setText('Score: 0'); comboTxt.setText('Combo: 0'); } function spawnNotes() { var now = Date.now(); while (nextNoteIdx < rhythmMap.length) { var noteData = rhythmMap[nextNoteIdx]; var noteTargetHitTime = gameStartTime + noteData.time; var noteVisualSpawnTime = noteTargetHitTime - noteTravelTime; if (noteVisualSpawnTime <= now) { var n = new Note(noteData.type, noteData.swipeDir, noteTargetHitTime); n.x = n.centerX; n.y = n.startY; n.scale.x = n.scaleStart; n.scale.y = n.scaleStart; n.alpha = 0; // Will be set to 1 in its own update when it's time notes.push(n); game.addChild(n); nextNoteIdx++; } else { break; } } } function removeOldNotes() { var now = Date.now(); for (var i = notes.length - 1; i >= 0; i--) { var n = notes[i]; if (n.judged && now > n.targetHitTime + hitWindowGood + 400) { if (n.parent) { n.destroy(); } notes.splice(i, 1); } else if (!n.judged && now > n.targetHitTime + noteTravelTime + 500) { // Failsafe for notes that somehow weren't judged if (n.parent) { n.destroy(); } notes.splice(i, 1); } } } function findNoteAt(x, y, typeToFind) { var now = Date.now(); var bestNote = null; var smallestTimeDiff = hitWindowGood + 1; for (var i = 0; i < notes.length; i++) { var n = notes[i]; if (n.judged || n.noteType !== typeToFind) { continue; } var timeDiff = Math.abs(now - n.targetHitTime); if (timeDiff > hitWindowGood) { continue; } var dx = x - n.x; var dy = y - n.y; var noteRadiusOrWidth = n.noteAsset.width * n.scale.x / 2; var noteHeight = n.noteAsset.height * n.scale.y / 2; if (typeToFind === 'swipe') { if (dx >= -noteRadiusOrWidth && dx <= noteRadiusOrWidth && dy >= -noteHeight && dy <= noteHeight) { if (timeDiff < smallestTimeDiff) { bestNote = n; smallestTimeDiff = timeDiff; } } } else { // tap or trap if (dx * dx + dy * dy <= noteRadiusOrWidth * noteRadiusOrWidth) { if (timeDiff < smallestTimeDiff) { bestNote = n; smallestTimeDiff = timeDiff; } } } } return bestNote; } 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'); } function checkGameEnd() { if (nextNoteIdx >= rhythmMap.length && notes.length === 0) { // A small delay before showing "You Win" to let last feedback animations play LK.setTimeout(function () { if (nextNoteIdx >= rhythmMap.length && notes.length === 0) { // Double check in case of quick restart LK.showYouWin(); } }, 1000); } } game.onNoteMiss = function (note) { if (note.judged) return; // Already handled note.judged = true; note.showHitFeedback('miss'); resetCombo(); LK.effects.flashObject(note, 0xff0000, 300); }; game.down = function (x, y, obj) { if (inputLocked) { return; } swipeStart = { x: x, y: y, time: Date.now(), note: null }; var trap = findNoteAt(x, y, 'trap'); if (trap && !trap.judged && trap.isInHitWindow()) { trap.judged = true; trap.showHitFeedback('miss'); resetCombo(); LK.effects.flashScreen(0xff0000, 400); inputLocked = true; LK.setTimeout(function () { inputLocked = false; }, 200); return; } var tap = findNoteAt(x, y, 'tap'); if (tap && !tap.judged && tap.isInHitWindow()) { 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; } // For swipe, we only record the start here if it's over a swipe note. // The actual swipe action is judged on game.up var swipeNoteUnderCursor = findNoteAt(x, y, 'swipe'); if (swipeNoteUnderCursor && !swipeNoteUnderCursor.judged && swipeNoteUnderCursor.isInHitWindow()) { swipeStart.note = swipeNoteUnderCursor; } }; game.up = function (x, y, obj) { if (inputLocked || !swipeStart) { swipeStart = null; return; } var noteToJudge = swipeStart.note; if (!noteToJudge || noteToJudge.judged || noteToJudge.noteType !== 'swipe') { swipeStart = null; return; } // Only judge if the swipe ended within a reasonable time window of the note's target time if (!noteToJudge.isInHitWindow()) { // swipeStart = null; // Optional: could also mark as miss if swipe was too late/early 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; } var detectedDir = null; if (Math.abs(dx) > Math.abs(dy)) { detectedDir = dx > 0 ? 'right' : 'left'; } else { detectedDir = dy > 0 ? 'down' : 'up'; } if (detectedDir === noteToJudge.swipeDir) { var result = noteToJudge.getHitAccuracy(); noteToJudge.judged = true; noteToJudge.showHitFeedback(result); if (result !== 'miss') { addScore(result); addCombo(); } else { resetCombo(); } } else { noteToJudge.judged = true; noteToJudge.showHitFeedback('miss'); resetCombo(); LK.effects.flashObject(noteToJudge, 0xff0000, 300); } inputLocked = true; LK.setTimeout(function () { inputLocked = false; }, 120); swipeStart = null; }; game.move = function (x, y, obj) { // Potential drag handling for future, or for a different type of note }; game.update = function () { var now = Date.now(); spawnNotes(); for (var i = 0; i < notes.length; i++) { if (notes[i]) { // Check if note still exists notes[i].update(); } } removeOldNotes(); checkGameEnd(); }; resetGameState(); gameStartTime = Date.now();
===================================================================
--- original.js
+++ change.js
@@ -5,54 +5,57 @@
/****
* 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 Note = Container.expand(function (noteType, swipeDir, targetHitTimeFull) {
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.noteType = noteType || 'tap';
+ self.swipeDir = swipeDir || null;
+ self.targetHitTime = targetHitTimeFull;
+ self.visualSpawnTime = self.targetHitTime - noteTravelTime;
+ self.hit = false;
+ self.judged = false;
+ self.scaleStart = 0.3;
+ self.scaleEnd = 1.2;
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;
+ self.centerY = 1800;
+ self.startY = 600;
+ self.noteAsset = null;
if (self.noteType === 'tap') {
- noteAsset = self.attachAsset('tapNote', {
+ self.noteAsset = self.attachAsset('tapNote', {
anchorX: 0.5,
anchorY: 0.5
});
} else if (self.noteType === 'swipe') {
- noteAsset = self.attachAsset('swipeNote', {
+ self.noteAsset = self.attachAsset('swipeNote', {
anchorX: 0.5,
anchorY: 0.5
});
+ if (self.swipeDir) {
+ 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;
+ }
} else if (self.noteType === 'trap') {
- noteAsset = self.attachAsset('trapNote', {
+ self.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.alpha = 0; // Initially invisible, will become visible when it's time
self.showHitFeedback = function (result) {
var feedback = LK.getAsset('hitFeedback', {
anchorX: 0.5,
anchorY: 0.5,
@@ -61,9 +64,15 @@
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;
+ 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,
@@ -71,46 +80,60 @@
}, {
duration: 350,
easing: tween.easeOut,
onFinish: function onFinish() {
- feedback.destroy();
+ if (feedback.parent) {
+ feedback.destroy();
+ }
}
});
};
- // Called every tick
self.update = function () {
- // Progress: 0 (spawn) to 1 (hit time)
var now = Date.now();
- var progress = (now - self.spawnTime) / noteTravelTime;
- if (progress < 0) progress = 0;
- if (progress > 1) progress = 1;
- // Scale up and move toward target
+ if (now < self.visualSpawnTime) {
+ self.alpha = 0; // Ensure it's invisible before visual spawn time
+ return;
+ }
+ if (self.alpha === 0) {
+ // First time it becomes visible
+ self.alpha = 1;
+ }
+ var elapsedTimeSinceSpawn = now - self.visualSpawnTime;
+ var progress = elapsedTimeSinceSpawn / noteTravelTime;
+ if (progress < 0) {
+ progress = 0;
+ }
+ if (progress > 1) {
+ progress = 1;
+ }
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) {
+ if (!self.judged && now > self.targetHitTime + hitWindowGood) {
self.judged = true;
if (self.noteType !== 'trap') {
- // Missed note
game.onNoteMiss(self);
+ } else {
+ // Trap notes just disappear if not hit, no miss penalty unless clicked
}
}
};
- // For hit detection
self.isInHitWindow = function () {
var now = Date.now();
- var dt = Math.abs(now - self.spawnTime);
+ var dt = Math.abs(now - self.targetHitTime);
return dt <= hitWindowGood;
};
self.getHitAccuracy = function () {
var now = Date.now();
- var dt = Math.abs(now - self.spawnTime);
- if (dt <= hitWindowPerfect) return 'perfect';
- if (dt <= hitWindowGood) return 'good';
+ var dt = Math.abs(now - self.targetHitTime);
+ if (dt <= hitWindowPerfect) {
+ return 'perfect';
+ }
+ if (dt <= hitWindowGood) {
+ return 'good';
+ }
return 'miss';
};
return self;
});
@@ -124,14 +147,8 @@
/****
* 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'
}, {
@@ -184,201 +201,173 @@
}, {
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 noteTravelTime = 1200;
+var hitWindowPerfect = 120;
+var hitWindowGood = 260;
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 swipeStart = null;
+var inputLocked = false;
var scoreTxt = new Text2('Score: 0', {
size: 100,
fill: 0xFFFFFF
});
scoreTxt.anchor.set(0.5, 0);
+scoreTxt.x = 2048 / 2;
+scoreTxt.y = 20;
LK.gui.top.addChild(scoreTxt);
var comboTxt = new Text2('Combo: 0', {
size: 80,
fill: 0xFFFF00
});
comboTxt.anchor.set(0.5, 0);
+comboTxt.x = 2048 / 2;
+comboTxt.y = 130;
LK.gui.top.addChild(comboTxt);
-comboTxt.y = 120;
-// --- Helper: Reset State ---
function resetGameState() {
+ notes.forEach(function (n) {
+ if (n && n.parent) n.destroy();
+ });
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 = Date.now();
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;
+ var noteTargetHitTime = gameStartTime + noteData.time;
+ var noteVisualSpawnTime = noteTargetHitTime - noteTravelTime;
+ if (noteVisualSpawnTime <= now) {
+ var n = new Note(noteData.type, noteData.swipeDir, noteTargetHitTime);
n.x = n.centerX;
n.y = n.startY;
+ n.scale.x = n.scaleStart;
+ n.scale.y = n.scaleStart;
+ n.alpha = 0; // Will be set to 1 in its own update when it's time
notes.push(n);
game.addChild(n);
nextNoteIdx++;
} else {
break;
}
}
}
-// --- Helper: Remove Old Notes ---
function removeOldNotes() {
var now = Date.now();
for (var i = notes.length - 1; i >= 0; i--) {
var n = notes[i];
- if (n.judged && now > n.spawnTime + hitWindowGood + 400) {
- n.destroy();
+ if (n.judged && now > n.targetHitTime + hitWindowGood + 400) {
+ if (n.parent) {
+ n.destroy();
+ }
notes.splice(i, 1);
+ } else if (!n.judged && now > n.targetHitTime + noteTravelTime + 500) {
+ // Failsafe for notes that somehow weren't judged
+ if (n.parent) {
+ n.destroy();
+ }
+ notes.splice(i, 1);
}
}
}
-// --- Helper: Find Closest Note in Hit Area ---
-function findNoteAt(x, y, type) {
+function findNoteAt(x, y, typeToFind) {
var now = Date.now();
- var best = null;
- var bestDt = 99999;
+ var bestNote = null;
+ var smallestTimeDiff = hitWindowGood + 1;
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)
+ if (n.judged || n.noteType !== typeToFind) {
+ continue;
+ }
+ var timeDiff = Math.abs(now - n.targetHitTime);
+ if (timeDiff > hitWindowGood) {
+ continue;
+ }
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;
+ var noteRadiusOrWidth = n.noteAsset.width * n.scale.x / 2;
+ var noteHeight = n.noteAsset.height * n.scale.y / 2;
+ if (typeToFind === 'swipe') {
+ if (dx >= -noteRadiusOrWidth && dx <= noteRadiusOrWidth && dy >= -noteHeight && dy <= noteHeight) {
+ if (timeDiff < smallestTimeDiff) {
+ bestNote = n;
+ smallestTimeDiff = timeDiff;
+ }
+ }
} else {
- // Circle: distance
- if (dx * dx + dy * dy > r * r) continue;
+ // tap or trap
+ if (dx * dx + dy * dy <= noteRadiusOrWidth * noteRadiusOrWidth) {
+ if (timeDiff < smallestTimeDiff) {
+ bestNote = n;
+ smallestTimeDiff = timeDiff;
+ }
+ }
}
- if (dt < bestDt) {
- best = n;
- bestDt = dt;
- }
}
- return best;
+ return bestNote;
}
-// --- Helper: Find Trap Note at Position ---
-function findTrapNoteAt(x, y) {
- var now = Date.now();
- 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;
+ if (result === 'perfect') {
+ score += 100;
+ } else if (result === 'good') {
+ score += 50;
+ }
scoreTxt.setText('Score: ' + score);
}
function addCombo() {
combo += 1;
- if (combo > maxCombo) maxCombo = combo;
+ 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();
+ // A small delay before showing "You Win" to let last feedback animations play
+ LK.setTimeout(function () {
+ if (nextNoteIdx >= rhythmMap.length && notes.length === 0) {
+ // Double check in case of quick restart
+ LK.showYouWin();
+ }
+ }, 1000);
}
}
-// --- Miss Handler ---
game.onNoteMiss = function (note) {
+ if (note.judged) return; // Already handled
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: Date.now()
- };
+ if (inputLocked) {
+ return;
+ }
swipeStart = {
x: x,
y: y,
- time: Date.now()
+ time: Date.now(),
+ note: null
};
- // Check for trap note
- var trap = findTrapNoteAt(x, y);
- if (trap && !trap.judged) {
+ var trap = findNoteAt(x, y, 'trap');
+ if (trap && !trap.judged && trap.isInHitWindow()) {
trap.judged = true;
trap.showHitFeedback('miss');
resetCombo();
LK.effects.flashScreen(0xff0000, 400);
@@ -387,11 +376,10 @@
inputLocked = false;
}, 200);
return;
}
- // Check for tap note
var tap = findNoteAt(x, y, 'tap');
- if (tap && !tap.judged) {
+ if (tap && !tap.judged && tap.isInHitWindow()) {
var result = tap.getHitAccuracy();
tap.judged = true;
tap.showHitFeedback(result);
if (result !== 'miss') {
@@ -405,75 +393,78 @@
inputLocked = false;
}, 120);
return;
}
- // Check for swipe note (start of swipe)
- var swipe = findNoteAt(x, y, 'swipe');
- if (swipe && !swipe.judged) {
- swipeStart.note = swipe;
+ // For swipe, we only record the start here if it's over a swipe note.
+ // The actual swipe action is judged on game.up
+ var swipeNoteUnderCursor = findNoteAt(x, y, 'swipe');
+ if (swipeNoteUnderCursor && !swipeNoteUnderCursor.judged && swipeNoteUnderCursor.isInHitWindow()) {
+ swipeStart.note = swipeNoteUnderCursor;
}
};
game.up = function (x, y, obj) {
- if (inputLocked) return;
- if (!swipeStart) return;
- var swipe = swipeStart.note;
- if (!swipe || swipe.judged) {
+ if (inputLocked || !swipeStart) {
swipeStart = null;
return;
}
+ var noteToJudge = swipeStart.note;
+ if (!noteToJudge || noteToJudge.judged || noteToJudge.noteType !== 'swipe') {
+ swipeStart = null;
+ return;
+ }
+ // Only judge if the swipe ended within a reasonable time window of the note's target time
+ if (!noteToJudge.isInHitWindow()) {
+ // swipeStart = null; // Optional: could also mark as miss if swipe was too late/early
+ 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
+ return;
}
- // Determine direction
- var dir = null;
+ var detectedDir = null;
if (Math.abs(dx) > Math.abs(dy)) {
- dir = dx > 0 ? 'right' : 'left';
+ detectedDir = dx > 0 ? 'right' : 'left';
} else {
- dir = dy > 0 ? 'down' : 'up';
+ detectedDir = dy > 0 ? 'down' : 'up';
}
- if (dir === swipe.swipeDir) {
- var result = swipe.getHitAccuracy();
- swipe.judged = true;
- swipe.showHitFeedback(result);
+ if (detectedDir === noteToJudge.swipeDir) {
+ var result = noteToJudge.getHitAccuracy();
+ noteToJudge.judged = true;
+ noteToJudge.showHitFeedback(result);
if (result !== 'miss') {
addScore(result);
addCombo();
} else {
resetCombo();
}
} else {
- // Wrong direction
- swipe.judged = true;
- swipe.showHitFeedback('miss');
+ noteToJudge.judged = true;
+ noteToJudge.showHitFeedback('miss');
resetCombo();
- LK.effects.flashObject(swipe, 0xff0000, 300);
+ LK.effects.flashObject(noteToJudge, 0xff0000, 300);
}
inputLocked = true;
LK.setTimeout(function () {
inputLocked = false;
}, 120);
swipeStart = null;
};
game.move = function (x, y, obj) {
- // No drag needed
+ // Potential drag handling for future, or for a different type of note
};
-// --- Main Update Loop ---
game.update = function () {
var now = Date.now();
- // Spawn notes as needed
spawnNotes();
- // Update all notes
for (var i = 0; i < notes.length; i++) {
- notes[i].update();
+ if (notes[i]) {
+ // Check if note still exists
+ notes[i].update();
+ }
}
- // Remove old notes
removeOldNotes();
- // End check
checkGameEnd();
};
-// --- Start Game ---
resetGameState();
gameStartTime = Date.now();
\ No newline at end of file