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