User prompt
change flash color to hit object color
User prompt
convert corners to edges for flash effect
User prompt
flash all of the same time
User prompt
add a flash effect to corner when hit
User prompt
remove top particle
User prompt
eac hit drop a particles from top
User prompt
change fonts
User prompt
change game fonts to lilita
User prompt
make hit particle smaller
User prompt
when hit create a particle
User prompt
add another image behind of background image
User prompt
create a background image and give me reference
User prompt
change time bar color
User prompt
change time bar fill color to orange
User prompt
move time bottom
User prompt
time bar need a count down with song coming
User prompt
time bar wont work
User prompt
add a time bar for song coming part
User prompt
add a particle when hitted
User prompt
add hit effect
User prompt
change song duration to 2 min
User prompt
change game time with duration of song. and create hit circles randomly.
User prompt
start music before bar
User prompt
wait for 10 second and start create hit circles and create a "song coming" texted loading bar in center
User prompt
reset song each time
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ // FollowCircle: For "follow the path" notes (future extension, not used in MVP) var FollowCircle = Container.expand(function () { var self = Container.call(this); var circle = self.attachAsset('followCircle', { anchorX: 0.5, anchorY: 0.5 }); circle.alpha = 0.8; return self; }); // HitCircle: Tap or Hold circle var HitCircle = Container.expand(function () { var self = Container.call(this); // Attach approach circle (shrinks towards hit time) var approach = self.attachAsset('approachCircle', { anchorX: 0.5, anchorY: 0.5 }); approach.alpha = 0.5; // Generate a random color for this hit circle self.circleColor = Math.floor(Math.random() * 0xFFFFFF); // Attach main hit circle with random color var circle = self.attachAsset('hitCircle', { anchorX: 0.5, anchorY: 0.5, color: self.circleColor }); circle.alpha = 1; // Text for number or timing var label = new Text2('', { size: 80, fill: 0x222222 }); label.anchor.set(0.5, 0.5); self.addChild(label); self.hitTime = 0; // ms self.type = 'tap'; // 'tap' | 'hold' self.holdDuration = 0; // ms, for hold notes self.hit = false; self.held = false; self.completed = false; self.index = 0; // for combo display self.setType = function (type, holdDuration) { self.type = type; if (type === 'hold') { self.holdDuration = holdDuration; } }; self.setLabel = function (txt) { label.setText(txt); }; self.setApproachScale = function (scale) { approach.scaleX = scale; approach.scaleY = scale; }; self.setCircleAlpha = function (a) { circle.alpha = a; }; self.flash = function (color, duration) { LK.effects.flashObject(circle, color, duration); }; self.hideApproach = function () { approach.visible = false; }; self.hideHoldRing = function () { // No-op: hold ring removed }; self.destroySelf = function () { self.destroy(); }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x181c24 }); /**** * Game Code ****/ // Music (placeholder, actual music asset will be loaded by LK) // Sounds (placeholders, actual sound assets will be loaded by LK) // Circles: main hit objects // --- Rhythm Map Data: Auto-generate 80 RPM hit circles --- // 80 RPM = 80/60 = 1.333... per second, so interval = 60/80 = 0.75s = 750ms // We'll generate enough for the song duration (default 10s) var rhythmMap = []; var rpm = 80; var interval = 60000 / rpm; // ms per beat var numCircles = Math.floor(10000 / interval); // songDuration is 10000ms for (var i = 0; i < numCircles; i++) { // Distribute positions in a circle pattern for variety var angle = i / numCircles * Math.PI * 2; var radius = 600; var centerX = 1024; var centerY = 1400; var x = Math.round(centerX + Math.cos(angle) * radius); var y = Math.round(centerY + Math.sin(angle) * radius); rhythmMap.push({ time: Math.round(1200 + i * interval), x: x, y: y, type: 'tap' }); } // --- Game State --- var hitObjects = []; // All active hit circles var currentIndex = 0; // Next object to spawn var startTime = 0; // ms, when song started var playing = false; var score = 0; var combo = 0; var maxCombo = 0; var lastTapTime = 0; var lastTapObj = null; var holdActive = null; // Currently held hold note var holdStartTime = 0; var holdBroken = false; var songDuration = 10000; // ms, for MVP var approachTime = 900; // ms, time for approach circle to shrink var hitWindow = 320; // ms, allowed timing error for "perfect" (increased) var goodWindow = 500; // ms, allowed for "good" (increased) var missWindow = 700; // ms, after this it's a miss (increased) // --- UI Elements --- var scoreTxt = new Text2('0', { size: 120, fill: 0xFFFFFF }); scoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(scoreTxt); var comboBg = LK.getAsset('comboTextBg', { anchorX: 0.5, anchorY: 0.5, x: 1024, y: 300 }); comboBg.alpha = 0.7; game.addChild(comboBg); var comboTxt = new Text2('', { size: 90, fill: 0x00FF99 }); comboTxt.anchor.set(0.5, 0.5); comboTxt.x = 1024; comboTxt.y = 300; game.addChild(comboTxt); // --- Start Game --- function startGame() { // Reset state for (var i = 0; i < hitObjects.length; i++) hitObjects[i].destroySelf(); hitObjects = []; currentIndex = 0; score = 0; combo = 0; maxCombo = 0; holdActive = null; holdBroken = false; scoreTxt.setText('0'); comboTxt.setText(''); comboBg.visible = false; startTime = Date.now(); playing = true; LK.setScore(0); LK.stopMusic(); LK.playMusic('song1'); } startGame(); // --- Spawning Hit Circles --- function spawnHitObject(obj, idx) { var hc = new HitCircle(); hc.x = obj.x; hc.y = obj.y; hc.hitTime = obj.time; hc.index = idx + 1; // Set label to (idx mod 8) + 1, so it loops 1-8 var circleNum = idx % 8 + 1; hc.setLabel(circleNum + ''); // Define a palette of 8 distinct colors (looped) var palette = [0xff5555, // red 0xffc300, // yellow 0x4dd0e1, // cyan 0x81c784, // green 0xba68c8, // purple 0xff8a65, // orange 0x90caf9, // blue 0xf06292 // pink ]; var colorIdx = (circleNum - 1) % palette.length; var color = palette[colorIdx]; // Set the hit circle's color to match its number if (hc.children && hc.children.length > 1) { hc.children[1].tint = color; } hc.circleColor = color; if (obj.type === 'hold') { hc.setType('hold', obj.hold); } // Start approach circle at a larger scale for more dramatic appearance hc.setApproachScale(1.5); game.addChild(hc); hitObjects.push(hc); } // --- Scoring --- function addScore(val) { score += val; LK.setScore(score); scoreTxt.setText(score); } function setCombo(val) { combo = val; if (combo > maxCombo) maxCombo = combo; if (combo > 1) { comboTxt.setText(combo + 'x'); comboBg.visible = true; } else { comboTxt.setText(''); comboBg.visible = false; } } // --- Judgement Feedback --- function showJudgement(hc, type) { var txt = ''; var color = 0xffffff; if (type === 'perfect') { txt = 'Perfect!'; color = 0x00ff99; } else if (type === 'good') { txt = 'Good'; color = 0xffe066; } else if (type === 'miss') { txt = 'Miss'; color = 0xff3333; } var judge = new Text2(txt, { size: 90, fill: 0xFFFFFF }); judge.anchor.set(0.5, 0.5); judge.x = hc.x; judge.y = hc.y - 120; game.addChild(judge); tween(judge, { alpha: 0, y: hc.y - 220 }, { duration: 600, easing: tween.easeOut, onFinish: function onFinish() { judge.destroy(); } }); LK.effects.flashObject(hc, color, 200); } // --- Input Handling --- game.down = function (x, y, obj) { if (!playing) return; var now = Date.now(); var found = false; for (var i = 0; i < hitObjects.length; i++) { var hc = hitObjects[i]; if (hc.hit) continue; // Only allow tap if within approach window var dt = now - startTime - hc.hitTime; if (Math.abs(dt) > missWindow + 200) continue; // Check if tap is inside circle var dx = x - hc.x; var dy = y - hc.y; var r = hc.width / 2; if (dx * dx + dy * dy < r * r) { found = true; if (hc.type === 'tap') { judgeTap(hc, dt); } else if (hc.type === 'hold') { // Start hold if (Math.abs(dt) <= hitWindow) { holdActive = hc; holdStartTime = now; holdBroken = false; hc.held = true; hc.hideApproach(); LK.getSound('hold').play(); } else { judgeTap(hc, dt); // treat as tap if too early/late } } break; } } if (!found) { // Miss: tap not on any circle setCombo(0); LK.getSound('miss').play(); } }; game.up = function (x, y, obj) { if (holdActive && holdActive.held && !holdActive.completed) { var now = Date.now(); var heldTime = now - holdStartTime; if (heldTime >= holdActive.holdDuration - 120) { // Success judgeHold(holdActive, heldTime); } else { // Released too early holdBroken = true; judgeHold(holdActive, heldTime); } holdActive.held = false; holdActive = null; } }; // --- Judgement Logic --- function judgeTap(hc, dt) { if (hc.hit) return; var absdt = Math.abs(dt); if (absdt <= hitWindow) { // Perfect addScore(300); setCombo(combo + 1); showJudgement(hc, 'perfect'); LK.getSound('hit').play(); } else if (absdt <= goodWindow) { addScore(100); setCombo(combo + 1); showJudgement(hc, 'good'); LK.getSound('hit').play(); } else if (absdt <= missWindow) { addScore(0); setCombo(0); showJudgement(hc, 'miss'); LK.getSound('miss').play(); } else { return; // too early/late, ignore } hc.hit = true; hc.setCircleAlpha(0.3); hc.hideApproach(); tween(hc, { alpha: 0 }, { duration: 300, onFinish: function onFinish() { hc.destroySelf(); } }); } function judgeHold(hc, heldTime) { if (hc.hit) return; if (holdBroken || heldTime < hc.holdDuration - 120) { // Miss addScore(0); setCombo(0); showJudgement(hc, 'miss'); LK.getSound('miss').play(); } else { addScore(400); setCombo(combo + 1); showJudgement(hc, 'perfect'); LK.getSound('hit').play(); } hc.hit = true; hc.completed = true; hc.setCircleAlpha(0.3); hc.hideHoldRing(); tween(hc, { alpha: 0 }, { duration: 300, onFinish: function onFinish() { hc.destroySelf(); } }); } // --- Main Update Loop --- game.update = function () { if (!playing) return; var now = Date.now(); var songPos = now - startTime; // Spawn new hit objects while (currentIndex < rhythmMap.length && rhythmMap[currentIndex].time - approachTime <= songPos) { spawnHitObject(rhythmMap[currentIndex], currentIndex); currentIndex++; } // Update hit objects for (var i = hitObjects.length - 1; i >= 0; i--) { var hc = hitObjects[i]; if (hc.hit) continue; var dt = songPos - hc.hitTime; // Approach circle shrinks var approachScale = Math.max(0.2, 1.0 - (dt + approachTime) / approachTime); hc.setApproachScale(approachScale); // Missed? if (dt > missWindow && !hc.hit) { // Miss addScore(0); setCombo(0); showJudgement(hc, 'miss'); LK.getSound('miss').play(); hc.hit = true; hc.setCircleAlpha(0.2); hc.hideApproach(); tween(hc, { alpha: 0 }, { duration: 300, onFinish: function onFinish() { hc.destroySelf(); } }); } // For hold notes: if held, show progress if (hc.type === 'hold' && hc.held && !hc.completed) { var heldTime = now - holdStartTime; if (heldTime >= hc.holdDuration) { judgeHold(hc, heldTime); holdActive = null; } } } // End of song if (songPos > songDuration + 1000) { playing = false; LK.showYouWin(); } }; // --- Music Start --- // (Music is started in startGame) // --- Game Over/Win Handling (handled by LK) --- // --- Touchscreen: No keyboard controls needed --- // --- Prevent elements in top left 100x100 px --- comboBg.x = 1024; comboBg.y = 300; comboTxt.x = 1024; comboTxt.y = 300; // --- (Optional) Restart on game over/win --- /* LK.on('gameover', function(){ startGame(); }); LK.on('youwin', function(){ startGame(); }); */
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// FollowCircle: For "follow the path" notes (future extension, not used in MVP)
var FollowCircle = Container.expand(function () {
var self = Container.call(this);
var circle = self.attachAsset('followCircle', {
anchorX: 0.5,
anchorY: 0.5
});
circle.alpha = 0.8;
return self;
});
// HitCircle: Tap or Hold circle
var HitCircle = Container.expand(function () {
var self = Container.call(this);
// Attach approach circle (shrinks towards hit time)
var approach = self.attachAsset('approachCircle', {
anchorX: 0.5,
anchorY: 0.5
});
approach.alpha = 0.5;
// Generate a random color for this hit circle
self.circleColor = Math.floor(Math.random() * 0xFFFFFF);
// Attach main hit circle with random color
var circle = self.attachAsset('hitCircle', {
anchorX: 0.5,
anchorY: 0.5,
color: self.circleColor
});
circle.alpha = 1;
// Text for number or timing
var label = new Text2('', {
size: 80,
fill: 0x222222
});
label.anchor.set(0.5, 0.5);
self.addChild(label);
self.hitTime = 0; // ms
self.type = 'tap'; // 'tap' | 'hold'
self.holdDuration = 0; // ms, for hold notes
self.hit = false;
self.held = false;
self.completed = false;
self.index = 0; // for combo display
self.setType = function (type, holdDuration) {
self.type = type;
if (type === 'hold') {
self.holdDuration = holdDuration;
}
};
self.setLabel = function (txt) {
label.setText(txt);
};
self.setApproachScale = function (scale) {
approach.scaleX = scale;
approach.scaleY = scale;
};
self.setCircleAlpha = function (a) {
circle.alpha = a;
};
self.flash = function (color, duration) {
LK.effects.flashObject(circle, color, duration);
};
self.hideApproach = function () {
approach.visible = false;
};
self.hideHoldRing = function () {
// No-op: hold ring removed
};
self.destroySelf = function () {
self.destroy();
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x181c24
});
/****
* Game Code
****/
// Music (placeholder, actual music asset will be loaded by LK)
// Sounds (placeholders, actual sound assets will be loaded by LK)
// Circles: main hit objects
// --- Rhythm Map Data: Auto-generate 80 RPM hit circles ---
// 80 RPM = 80/60 = 1.333... per second, so interval = 60/80 = 0.75s = 750ms
// We'll generate enough for the song duration (default 10s)
var rhythmMap = [];
var rpm = 80;
var interval = 60000 / rpm; // ms per beat
var numCircles = Math.floor(10000 / interval); // songDuration is 10000ms
for (var i = 0; i < numCircles; i++) {
// Distribute positions in a circle pattern for variety
var angle = i / numCircles * Math.PI * 2;
var radius = 600;
var centerX = 1024;
var centerY = 1400;
var x = Math.round(centerX + Math.cos(angle) * radius);
var y = Math.round(centerY + Math.sin(angle) * radius);
rhythmMap.push({
time: Math.round(1200 + i * interval),
x: x,
y: y,
type: 'tap'
});
}
// --- Game State ---
var hitObjects = []; // All active hit circles
var currentIndex = 0; // Next object to spawn
var startTime = 0; // ms, when song started
var playing = false;
var score = 0;
var combo = 0;
var maxCombo = 0;
var lastTapTime = 0;
var lastTapObj = null;
var holdActive = null; // Currently held hold note
var holdStartTime = 0;
var holdBroken = false;
var songDuration = 10000; // ms, for MVP
var approachTime = 900; // ms, time for approach circle to shrink
var hitWindow = 320; // ms, allowed timing error for "perfect" (increased)
var goodWindow = 500; // ms, allowed for "good" (increased)
var missWindow = 700; // ms, after this it's a miss (increased)
// --- UI Elements ---
var scoreTxt = new Text2('0', {
size: 120,
fill: 0xFFFFFF
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
var comboBg = LK.getAsset('comboTextBg', {
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: 300
});
comboBg.alpha = 0.7;
game.addChild(comboBg);
var comboTxt = new Text2('', {
size: 90,
fill: 0x00FF99
});
comboTxt.anchor.set(0.5, 0.5);
comboTxt.x = 1024;
comboTxt.y = 300;
game.addChild(comboTxt);
// --- Start Game ---
function startGame() {
// Reset state
for (var i = 0; i < hitObjects.length; i++) hitObjects[i].destroySelf();
hitObjects = [];
currentIndex = 0;
score = 0;
combo = 0;
maxCombo = 0;
holdActive = null;
holdBroken = false;
scoreTxt.setText('0');
comboTxt.setText('');
comboBg.visible = false;
startTime = Date.now();
playing = true;
LK.setScore(0);
LK.stopMusic();
LK.playMusic('song1');
}
startGame();
// --- Spawning Hit Circles ---
function spawnHitObject(obj, idx) {
var hc = new HitCircle();
hc.x = obj.x;
hc.y = obj.y;
hc.hitTime = obj.time;
hc.index = idx + 1;
// Set label to (idx mod 8) + 1, so it loops 1-8
var circleNum = idx % 8 + 1;
hc.setLabel(circleNum + '');
// Define a palette of 8 distinct colors (looped)
var palette = [0xff5555,
// red
0xffc300,
// yellow
0x4dd0e1,
// cyan
0x81c784,
// green
0xba68c8,
// purple
0xff8a65,
// orange
0x90caf9,
// blue
0xf06292 // pink
];
var colorIdx = (circleNum - 1) % palette.length;
var color = palette[colorIdx];
// Set the hit circle's color to match its number
if (hc.children && hc.children.length > 1) {
hc.children[1].tint = color;
}
hc.circleColor = color;
if (obj.type === 'hold') {
hc.setType('hold', obj.hold);
}
// Start approach circle at a larger scale for more dramatic appearance
hc.setApproachScale(1.5);
game.addChild(hc);
hitObjects.push(hc);
}
// --- Scoring ---
function addScore(val) {
score += val;
LK.setScore(score);
scoreTxt.setText(score);
}
function setCombo(val) {
combo = val;
if (combo > maxCombo) maxCombo = combo;
if (combo > 1) {
comboTxt.setText(combo + 'x');
comboBg.visible = true;
} else {
comboTxt.setText('');
comboBg.visible = false;
}
}
// --- Judgement Feedback ---
function showJudgement(hc, type) {
var txt = '';
var color = 0xffffff;
if (type === 'perfect') {
txt = 'Perfect!';
color = 0x00ff99;
} else if (type === 'good') {
txt = 'Good';
color = 0xffe066;
} else if (type === 'miss') {
txt = 'Miss';
color = 0xff3333;
}
var judge = new Text2(txt, {
size: 90,
fill: 0xFFFFFF
});
judge.anchor.set(0.5, 0.5);
judge.x = hc.x;
judge.y = hc.y - 120;
game.addChild(judge);
tween(judge, {
alpha: 0,
y: hc.y - 220
}, {
duration: 600,
easing: tween.easeOut,
onFinish: function onFinish() {
judge.destroy();
}
});
LK.effects.flashObject(hc, color, 200);
}
// --- Input Handling ---
game.down = function (x, y, obj) {
if (!playing) return;
var now = Date.now();
var found = false;
for (var i = 0; i < hitObjects.length; i++) {
var hc = hitObjects[i];
if (hc.hit) continue;
// Only allow tap if within approach window
var dt = now - startTime - hc.hitTime;
if (Math.abs(dt) > missWindow + 200) continue;
// Check if tap is inside circle
var dx = x - hc.x;
var dy = y - hc.y;
var r = hc.width / 2;
if (dx * dx + dy * dy < r * r) {
found = true;
if (hc.type === 'tap') {
judgeTap(hc, dt);
} else if (hc.type === 'hold') {
// Start hold
if (Math.abs(dt) <= hitWindow) {
holdActive = hc;
holdStartTime = now;
holdBroken = false;
hc.held = true;
hc.hideApproach();
LK.getSound('hold').play();
} else {
judgeTap(hc, dt); // treat as tap if too early/late
}
}
break;
}
}
if (!found) {
// Miss: tap not on any circle
setCombo(0);
LK.getSound('miss').play();
}
};
game.up = function (x, y, obj) {
if (holdActive && holdActive.held && !holdActive.completed) {
var now = Date.now();
var heldTime = now - holdStartTime;
if (heldTime >= holdActive.holdDuration - 120) {
// Success
judgeHold(holdActive, heldTime);
} else {
// Released too early
holdBroken = true;
judgeHold(holdActive, heldTime);
}
holdActive.held = false;
holdActive = null;
}
};
// --- Judgement Logic ---
function judgeTap(hc, dt) {
if (hc.hit) return;
var absdt = Math.abs(dt);
if (absdt <= hitWindow) {
// Perfect
addScore(300);
setCombo(combo + 1);
showJudgement(hc, 'perfect');
LK.getSound('hit').play();
} else if (absdt <= goodWindow) {
addScore(100);
setCombo(combo + 1);
showJudgement(hc, 'good');
LK.getSound('hit').play();
} else if (absdt <= missWindow) {
addScore(0);
setCombo(0);
showJudgement(hc, 'miss');
LK.getSound('miss').play();
} else {
return; // too early/late, ignore
}
hc.hit = true;
hc.setCircleAlpha(0.3);
hc.hideApproach();
tween(hc, {
alpha: 0
}, {
duration: 300,
onFinish: function onFinish() {
hc.destroySelf();
}
});
}
function judgeHold(hc, heldTime) {
if (hc.hit) return;
if (holdBroken || heldTime < hc.holdDuration - 120) {
// Miss
addScore(0);
setCombo(0);
showJudgement(hc, 'miss');
LK.getSound('miss').play();
} else {
addScore(400);
setCombo(combo + 1);
showJudgement(hc, 'perfect');
LK.getSound('hit').play();
}
hc.hit = true;
hc.completed = true;
hc.setCircleAlpha(0.3);
hc.hideHoldRing();
tween(hc, {
alpha: 0
}, {
duration: 300,
onFinish: function onFinish() {
hc.destroySelf();
}
});
}
// --- Main Update Loop ---
game.update = function () {
if (!playing) return;
var now = Date.now();
var songPos = now - startTime;
// Spawn new hit objects
while (currentIndex < rhythmMap.length && rhythmMap[currentIndex].time - approachTime <= songPos) {
spawnHitObject(rhythmMap[currentIndex], currentIndex);
currentIndex++;
}
// Update hit objects
for (var i = hitObjects.length - 1; i >= 0; i--) {
var hc = hitObjects[i];
if (hc.hit) continue;
var dt = songPos - hc.hitTime;
// Approach circle shrinks
var approachScale = Math.max(0.2, 1.0 - (dt + approachTime) / approachTime);
hc.setApproachScale(approachScale);
// Missed?
if (dt > missWindow && !hc.hit) {
// Miss
addScore(0);
setCombo(0);
showJudgement(hc, 'miss');
LK.getSound('miss').play();
hc.hit = true;
hc.setCircleAlpha(0.2);
hc.hideApproach();
tween(hc, {
alpha: 0
}, {
duration: 300,
onFinish: function onFinish() {
hc.destroySelf();
}
});
}
// For hold notes: if held, show progress
if (hc.type === 'hold' && hc.held && !hc.completed) {
var heldTime = now - holdStartTime;
if (heldTime >= hc.holdDuration) {
judgeHold(hc, heldTime);
holdActive = null;
}
}
}
// End of song
if (songPos > songDuration + 1000) {
playing = false;
LK.showYouWin();
}
};
// --- Music Start ---
// (Music is started in startGame)
// --- Game Over/Win Handling (handled by LK) ---
// --- Touchscreen: No keyboard controls needed ---
// --- Prevent elements in top left 100x100 px ---
comboBg.x = 1024;
comboBg.y = 300;
comboTxt.x = 1024;
comboTxt.y = 300;
// --- (Optional) Restart on game over/win ---
/*
LK.on('gameover', function(){
startGame();
});
LK.on('youwin', function(){
startGame();
});
*/