/**** * 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();
});
*/