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 = Date.now(); ===================================================================
--- original.js
+++ change.js
@@ -475,5 +475,5 @@
checkGameEnd();
};
// --- Start Game ---
resetGameState();
-gameStartTime = LK.getTime();
\ No newline at end of file
+gameStartTime = Date.now();
\ No newline at end of file