User prompt
create hit circles in 80 rpm
User prompt
add a music
User prompt
change color with number of circle and loop with it
User prompt
change color of hit circles
User prompt
give random color to hit circles
User prompt
no hit circle coming
User prompt
remote last working version
User prompt
game not functioning
User prompt
remote hsv
User prompt
remove last change
User prompt
give a color to each hit circle and loop around hsv colors each time
User prompt
add number text to hit circles and loop after 8
User prompt
remove hold ring
User prompt
give more hit time
User prompt
little more scale to appering circles
Code edit (1 edits merged)
Please save this source code
User prompt
Touch Beat: Rhythm Circles
Initial prompt
i wanna create game like osu
/**** * 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, font: "Lilita One" }); 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; }); // Particle: Simple expanding/fading circle for hit effect var Particle = Container.expand(function () { var self = Container.call(this); var circle = self.attachAsset('hitCircle', { anchorX: 0.5, anchorY: 0.5, color: 0xffffff }); circle.alpha = 0.7; self.life = 350; // ms self.elapsed = 0; self.startScale = 0.5; self.endScale = 1.1; self.startAlpha = 0.7; self.endAlpha = 0.0; self.update = function () { self.elapsed += 1000 / 60; var t = Math.min(1, self.elapsed / self.life); circle.scaleX = circle.scaleY = self.startScale + (self.endScale - self.startScale) * t; circle.alpha = self.startAlpha + (self.endAlpha - self.startAlpha) * t; if (self.elapsed >= self.life) { self.destroy(); } }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x181c24 }); /**** * Game Code ****/ // Display new background image behind the current background image (centered, covers full play area) // Background image: 2048x2732, id is a placeholder, replace with your actual image asset id if needed var bgImageBehind = LK.getAsset('bgImageBehind', { anchorX: 0.5, anchorY: 0.5, x: 2048 / 2, y: 2732 / 2 }); game.addChild(bgImageBehind); var bgImage = LK.getAsset('bgImage', { anchorX: 0.5, anchorY: 0.5, x: 2048 / 2, y: 2732 / 2 }); game.addChild(bgImage); // 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: Generate random hit circles for full song duration --- // Set songDuration to actual song length (LK asset 'song1' end - start in ms) var songDuration = 120000; // 2 minutes in ms var rhythmMap = []; var rpm = 80; var interval = 60000 / rpm; // ms per beat var numCircles = Math.floor(songDuration / interval); for (var i = 0; i < numCircles; i++) { // Random position, but keep inside safe play area (avoid edges) var margin = 250; var x = Math.round(margin + Math.random() * (2048 - 2 * margin)); var y = Math.round(600 + Math.random() * (2000 - 2 * margin)); 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 (removed, now set to 2 min above) 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, font: "Lilita One" }); 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, font: "Lilita One" }); comboTxt.anchor.set(0.5, 0.5); comboTxt.x = 1024; comboTxt.y = 300; game.addChild(comboTxt); // --- Time Bar UI --- // Bar background var timeBarBg = LK.getAsset('comboTextBg', { anchorX: 0, anchorY: 0.5, x: 120, y: 2732 - 100 }); timeBarBg.width = 1800; timeBarBg.height = 44; timeBarBg.alpha = 0.5; game.addChild(timeBarBg); // Foreground progress bar var timeBarFg = LK.getAsset('comboTextBg', { anchorX: 0, anchorY: 0.5, x: 120, y: 2732 - 100, color: 0x00bfff // blue (change this to your desired color) }); timeBarFg.width = 0; timeBarFg.height = 32; timeBarFg.alpha = 1; game.addChild(timeBarFg); // Countdown text for time bar var timeBarCountdown = new Text2('2:00', { size: 60, fill: 0xffffff, font: "Lilita One" }); timeBarCountdown.anchor.set(1, 0.5); timeBarCountdown.x = timeBarBg.x + timeBarBg.width - 10; timeBarCountdown.y = timeBarBg.y; game.addChild(timeBarCountdown); // --- Start Game --- var loadingBar = null; var loadingText = null; var loadingTween = null; function showLoadingBar(duration, onFinish) { // Remove previous if any if (loadingBar) loadingBar.destroy(); if (loadingText) loadingText.destroy(); // Bar background loadingBar = LK.getAsset('comboTextBg', { anchorX: 0.5, anchorY: 0.5, x: 1024, y: 1366 }); loadingBar.width = 800; loadingBar.height = 120; loadingBar.alpha = 0.85; game.addChild(loadingBar); // Foreground bar (progress) var barFg = LK.getAsset('comboTextBg', { anchorX: 0.5, anchorY: 0.5, x: 1024, y: 1366, color: 0x00ff99 }); barFg.width = 0; barFg.height = 80; barFg.alpha = 1; loadingBar.addChild(barFg); // Text loadingText = new Text2('Song coming...', { size: 80, fill: 0xffffff, font: "Lilita One" }); loadingText.anchor.set(0.5, 0.5); loadingText.x = 1024; loadingText.y = 1366; game.addChild(loadingText); // Animate bar var start = Date.now(); function updateBar() { var now = Date.now(); var elapsed = now - start; var frac = Math.min(1, elapsed / duration); barFg.width = 760 * frac; if (frac < 1) { loadingTween = LK.setTimeout(updateBar, 16); } else { if (loadingBar) loadingBar.destroy(); if (loadingText) loadingText.destroy(); loadingBar = null; loadingText = null; if (onFinish) onFinish(); } } updateBar(); } 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; playing = false; LK.setScore(0); LK.stopMusic(); // Start music before loading bar, so music plays during the loading bar wait LK.playMusic('song1'); // Show loading bar for 10s, then start hit circles showLoadingBar(10000, function () { startTime = Date.now(); playing = true; }); } 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, font: "Lilita One" }); 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(); } }); // Hit effect: flash the main circle (not the container) for more visual feedback if (hc.children && hc.children.length > 1) { LK.effects.flashObject(hc.children[1], color, 200); } else { LK.effects.flashObject(hc, color, 200); } // Flash all four screen edges for hit feedback var edges = [ // Top edge { x: 0, y: 0, anchorX: 0, anchorY: 0, width: 2048, height: 80 }, // Bottom edge { x: 0, y: 2732, anchorX: 0, anchorY: 1, width: 2048, height: 80 }, // Left edge { x: 0, y: 0, anchorX: 0, anchorY: 0, width: 80, height: 2732 }, // Right edge { x: 2048, y: 0, anchorX: 1, anchorY: 0, width: 80, height: 2732 }]; var edgeFlashColor = hc && hc.circleColor ? hc.circleColor : color; for (var i = 0; i < edges.length; i++) { var e = edges[i]; var flashRect = LK.getAsset('comboTextBg', { anchorX: e.anchorX, anchorY: e.anchorY, x: e.x, y: e.y }); flashRect.width = e.width; flashRect.height = e.height; flashRect.alpha = 0.7; flashRect.tint = edgeFlashColor; game.addChild(flashRect); tween(flashRect, { alpha: 0 }, { duration: 220, onFinish: function (rect) { return function () { rect.destroy(); }; }(flashRect) }); } // Particle effect at hit location var particle = new Particle(); particle.x = hc.x; particle.y = hc.y; if (hc.circleColor) { if (particle.children && particle.children.length > 0) { particle.children[0].tint = hc.circleColor; } } game.addChild(particle); // Extra: spawn a burst of small particles for more impact for (var i = 0; i < 6; i++) { var p = new Particle(); p.x = hc.x; p.y = hc.y; if (hc.circleColor && p.children && p.children.length > 0) { p.children[0].tint = hc.circleColor; } // Give each particle a random direction and speed var angle = Math.random() * Math.PI * 2; var speed = 8 + Math.random() * 10; p.vx = Math.cos(angle) * speed; p.vy = Math.sin(angle) * speed; // Override update to move outward and fade (function (p) { var baseUpdate = p.update; p.update = function () { p.x += p.vx; p.y += p.vy; // Slow down p.vx *= 0.92; p.vy *= 0.92; baseUpdate.call(p); }; })(p); game.addChild(p); } } // --- 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; } } } // Update time bar if (typeof timeBarFg !== "undefined" && typeof timeBarBg !== "undefined") { // Always set the left edge and width, so the bar grows from left to right var frac = Math.max(0, Math.min(1, songPos / songDuration)); timeBarFg.x = timeBarBg.x; // align left edge timeBarFg.y = timeBarBg.y; timeBarFg.width = timeBarBg.width * frac; timeBarFg.height = timeBarBg.height - 12; // Update countdown text if (typeof timeBarCountdown !== "undefined") { var remaining = Math.max(0, songDuration - songPos); var sec = Math.floor(remaining / 1000); var min = Math.floor(sec / 60); sec = sec % 60; var timeStr = min + ":" + (sec < 10 ? "0" : "") + sec; timeBarCountdown.setText(timeStr); } } // 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,
font: "Lilita One"
});
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;
});
// Particle: Simple expanding/fading circle for hit effect
var Particle = Container.expand(function () {
var self = Container.call(this);
var circle = self.attachAsset('hitCircle', {
anchorX: 0.5,
anchorY: 0.5,
color: 0xffffff
});
circle.alpha = 0.7;
self.life = 350; // ms
self.elapsed = 0;
self.startScale = 0.5;
self.endScale = 1.1;
self.startAlpha = 0.7;
self.endAlpha = 0.0;
self.update = function () {
self.elapsed += 1000 / 60;
var t = Math.min(1, self.elapsed / self.life);
circle.scaleX = circle.scaleY = self.startScale + (self.endScale - self.startScale) * t;
circle.alpha = self.startAlpha + (self.endAlpha - self.startAlpha) * t;
if (self.elapsed >= self.life) {
self.destroy();
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x181c24
});
/****
* Game Code
****/
// Display new background image behind the current background image (centered, covers full play area)
// Background image: 2048x2732, id is a placeholder, replace with your actual image asset id if needed
var bgImageBehind = LK.getAsset('bgImageBehind', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2732 / 2
});
game.addChild(bgImageBehind);
var bgImage = LK.getAsset('bgImage', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2732 / 2
});
game.addChild(bgImage);
// 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: Generate random hit circles for full song duration ---
// Set songDuration to actual song length (LK asset 'song1' end - start in ms)
var songDuration = 120000; // 2 minutes in ms
var rhythmMap = [];
var rpm = 80;
var interval = 60000 / rpm; // ms per beat
var numCircles = Math.floor(songDuration / interval);
for (var i = 0; i < numCircles; i++) {
// Random position, but keep inside safe play area (avoid edges)
var margin = 250;
var x = Math.round(margin + Math.random() * (2048 - 2 * margin));
var y = Math.round(600 + Math.random() * (2000 - 2 * margin));
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 (removed, now set to 2 min above)
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,
font: "Lilita One"
});
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,
font: "Lilita One"
});
comboTxt.anchor.set(0.5, 0.5);
comboTxt.x = 1024;
comboTxt.y = 300;
game.addChild(comboTxt);
// --- Time Bar UI ---
// Bar background
var timeBarBg = LK.getAsset('comboTextBg', {
anchorX: 0,
anchorY: 0.5,
x: 120,
y: 2732 - 100
});
timeBarBg.width = 1800;
timeBarBg.height = 44;
timeBarBg.alpha = 0.5;
game.addChild(timeBarBg);
// Foreground progress bar
var timeBarFg = LK.getAsset('comboTextBg', {
anchorX: 0,
anchorY: 0.5,
x: 120,
y: 2732 - 100,
color: 0x00bfff // blue (change this to your desired color)
});
timeBarFg.width = 0;
timeBarFg.height = 32;
timeBarFg.alpha = 1;
game.addChild(timeBarFg);
// Countdown text for time bar
var timeBarCountdown = new Text2('2:00', {
size: 60,
fill: 0xffffff,
font: "Lilita One"
});
timeBarCountdown.anchor.set(1, 0.5);
timeBarCountdown.x = timeBarBg.x + timeBarBg.width - 10;
timeBarCountdown.y = timeBarBg.y;
game.addChild(timeBarCountdown);
// --- Start Game ---
var loadingBar = null;
var loadingText = null;
var loadingTween = null;
function showLoadingBar(duration, onFinish) {
// Remove previous if any
if (loadingBar) loadingBar.destroy();
if (loadingText) loadingText.destroy();
// Bar background
loadingBar = LK.getAsset('comboTextBg', {
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: 1366
});
loadingBar.width = 800;
loadingBar.height = 120;
loadingBar.alpha = 0.85;
game.addChild(loadingBar);
// Foreground bar (progress)
var barFg = LK.getAsset('comboTextBg', {
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: 1366,
color: 0x00ff99
});
barFg.width = 0;
barFg.height = 80;
barFg.alpha = 1;
loadingBar.addChild(barFg);
// Text
loadingText = new Text2('Song coming...', {
size: 80,
fill: 0xffffff,
font: "Lilita One"
});
loadingText.anchor.set(0.5, 0.5);
loadingText.x = 1024;
loadingText.y = 1366;
game.addChild(loadingText);
// Animate bar
var start = Date.now();
function updateBar() {
var now = Date.now();
var elapsed = now - start;
var frac = Math.min(1, elapsed / duration);
barFg.width = 760 * frac;
if (frac < 1) {
loadingTween = LK.setTimeout(updateBar, 16);
} else {
if (loadingBar) loadingBar.destroy();
if (loadingText) loadingText.destroy();
loadingBar = null;
loadingText = null;
if (onFinish) onFinish();
}
}
updateBar();
}
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;
playing = false;
LK.setScore(0);
LK.stopMusic();
// Start music before loading bar, so music plays during the loading bar wait
LK.playMusic('song1');
// Show loading bar for 10s, then start hit circles
showLoadingBar(10000, function () {
startTime = Date.now();
playing = true;
});
}
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,
font: "Lilita One"
});
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();
}
});
// Hit effect: flash the main circle (not the container) for more visual feedback
if (hc.children && hc.children.length > 1) {
LK.effects.flashObject(hc.children[1], color, 200);
} else {
LK.effects.flashObject(hc, color, 200);
}
// Flash all four screen edges for hit feedback
var edges = [
// Top edge
{
x: 0,
y: 0,
anchorX: 0,
anchorY: 0,
width: 2048,
height: 80
},
// Bottom edge
{
x: 0,
y: 2732,
anchorX: 0,
anchorY: 1,
width: 2048,
height: 80
},
// Left edge
{
x: 0,
y: 0,
anchorX: 0,
anchorY: 0,
width: 80,
height: 2732
},
// Right edge
{
x: 2048,
y: 0,
anchorX: 1,
anchorY: 0,
width: 80,
height: 2732
}];
var edgeFlashColor = hc && hc.circleColor ? hc.circleColor : color;
for (var i = 0; i < edges.length; i++) {
var e = edges[i];
var flashRect = LK.getAsset('comboTextBg', {
anchorX: e.anchorX,
anchorY: e.anchorY,
x: e.x,
y: e.y
});
flashRect.width = e.width;
flashRect.height = e.height;
flashRect.alpha = 0.7;
flashRect.tint = edgeFlashColor;
game.addChild(flashRect);
tween(flashRect, {
alpha: 0
}, {
duration: 220,
onFinish: function (rect) {
return function () {
rect.destroy();
};
}(flashRect)
});
}
// Particle effect at hit location
var particle = new Particle();
particle.x = hc.x;
particle.y = hc.y;
if (hc.circleColor) {
if (particle.children && particle.children.length > 0) {
particle.children[0].tint = hc.circleColor;
}
}
game.addChild(particle);
// Extra: spawn a burst of small particles for more impact
for (var i = 0; i < 6; i++) {
var p = new Particle();
p.x = hc.x;
p.y = hc.y;
if (hc.circleColor && p.children && p.children.length > 0) {
p.children[0].tint = hc.circleColor;
}
// Give each particle a random direction and speed
var angle = Math.random() * Math.PI * 2;
var speed = 8 + Math.random() * 10;
p.vx = Math.cos(angle) * speed;
p.vy = Math.sin(angle) * speed;
// Override update to move outward and fade
(function (p) {
var baseUpdate = p.update;
p.update = function () {
p.x += p.vx;
p.y += p.vy;
// Slow down
p.vx *= 0.92;
p.vy *= 0.92;
baseUpdate.call(p);
};
})(p);
game.addChild(p);
}
}
// --- 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;
}
}
}
// Update time bar
if (typeof timeBarFg !== "undefined" && typeof timeBarBg !== "undefined") {
// Always set the left edge and width, so the bar grows from left to right
var frac = Math.max(0, Math.min(1, songPos / songDuration));
timeBarFg.x = timeBarBg.x; // align left edge
timeBarFg.y = timeBarBg.y;
timeBarFg.width = timeBarBg.width * frac;
timeBarFg.height = timeBarBg.height - 12;
// Update countdown text
if (typeof timeBarCountdown !== "undefined") {
var remaining = Math.max(0, songDuration - songPos);
var sec = Math.floor(remaining / 1000);
var min = Math.floor(sec / 60);
sec = sec % 60;
var timeStr = min + ":" + (sec < 10 ? "0" : "") + sec;
timeBarCountdown.setText(timeStr);
}
}
// 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();
});
*/