Code edit (4 edits merged)
Please save this source code
User prompt
Please fix the bug: 'TypeError: Cannot read properties of undefined (reading 'height')' in or related to this line: 'LK.gui.top.add(new Text2('SONG ENDED', {' Line Number: 920
Code edit (11 edits merged)
Please save this source code
User prompt
Please fix the bug: 'Cannot read properties of undefined (reading 'height')' in or related to this line: 'playerHpBarContainer.y = LK.data.height - 80; // Pozycja Y na dole (LK.data.height to wysokość ekranu)' Line Number: 698
Code edit (1 edits merged)
Please save this source code
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 (6 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 (1 edits merged)
Please save this source code
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