User prompt
Since the background for Unknown Journey is mostly white, specialize the fonts of display of scoreboard in black color for Unknown Journey only
User prompt
Reduce the fonts size of Unknown Journey selector UI
User prompt
Remove all hold notes and swipes notes in easy mode of Unknown Journey
User prompt
Place Unknown Journey within the SelectorUI, and add Chinese translation for Unknown Journey title as 莫问前程
User prompt
```js { id: 'UnknownJourney', title: 'Unknown Journey', '莫问前程’ cover: 'songCover4', duration: 240000, story: { en: "A song that mix Chinese traditional music with dark ambient music style, depicting the swordswoman making peace with her dark past while maintaining her courage to explore the mysterious and unknown journey.”, zh: “一首混合中华传统音乐与黑暗氛围风格的歌曲,展现出女侠对于黑暗过去的释怀,保持对探索神秘未知前程的勇气。” }, difficulties: ['Easy', 'Medium', 'Hard'],
User prompt
I've identified and fixed the issue with gameplay backgrounds persisting in the final scoreboard and home screen. The problem was that we weren't consistently removing backgrounds during state transitions. Here's the solution: 1. Added a new `gameplayBackground` variable to track the current background 2. Modified `removeGameplayBackgrounds()` to use this reference 3. Ensured proper removal in all state transitions Here are the key changes: ```javascript // Add this at the top with other globals var gameplayBackground = null; // Updated background removal function function removeGameplayBackgrounds() { if (gameplayBackground && gameplayBackground.parent) { gameplayBackground.parent.removeChild(gameplayBackground); gameplayBackground = null; } } // Modified background setup in startGameplay() function startGameplay() { removeGameplayBackgrounds(); // Clear any existing background // Add new background if (selectedSong.id === 'SkyJourney') { gameplayBackground = LK.getAsset('BgSkyJourney', {...}); game.addChildAt(gameplayBackground, 0); } // Similar for other songs... } // Updated setGameState() to include background removal function setGameState(state) { // Always remove gameplay backgrounds first removeGameplayBackgrounds(); // Existing state transition code... if (state === 'HOME') { showHome(); } // Other states... } // Modified showResult() to remove background function showResult() { removeGameplayBackgrounds(); // Existing result screen code... } ``` The complete solution integrates these changes throughout the code: ```javascript // ... (existing code above GLOBALS section) // --- GLOBALS --- var gameplayBackground = null; // Add this to track background var GAME_STATE = 'LANGUAGE'; // ... (other globals remain unchanged) // --- UTILS --- function removeGameplayBackgrounds() { if (gameplayBackground && gameplayBackground.parent) { gameplayBackground.parent.removeChild(gameplayBackground); gameplayBackground = null; } } function clearGameObjects() { removeGameplayBackgrounds(); // Ensure backgrounds are removed // ... (rest of existing clearGameObjects code) } function setGameState(state) { // Always remove gameplay backgrounds from the game scene removeGameplayBackgrounds(); removeGameplayBackgrounds(); // Double-call for safety // ... (rest of existing setGameState code) } // --- GAMEPLAY --- function startGameplay() { removeGameplayBackgrounds(); // Clear previous backgrounds // Background setup - ADD THIS SECTION: if (selectedSong && selectedSong.id === 'SkyJourney') { gameplayBackground = LK.getAsset('BgSkyJourney', { anchorX: 0.5, anchorY: 0, x: 2048 / 2, y: 0, scaleX: 2048 / 2048, scaleY: 2732 / 3640.89 }); gameplayBackground.width = 2048; gameplayBackground.height = 2732; game.addChildAt(gameplayBackground, 0); } else if (selectedSong && selectedSong.id === 'NightPulse') { gameplayBackground = LK.getAsset('BgNightPulse', { anchorX: 0.5, anchorY: 0, x: 2048 / 2, y: 0, scaleX: 2048 / 2048, scaleY: 2732 / 3640.89 }); gameplayBackground.width = 2048; gameplayBackground.height = 2732; game.addChildAt(gameplayBackground, 0); } else if (selectedSong && selectedSong.id === 'SunriseWaltz') { gameplayBackground = LK.getAsset('BgSunriseWaltz', { anchorX: 0.5, anchorY: 0, x: 2048 / 2, y: 0, scaleX: 2048 / 2048, scaleY: 2732 / 3640.89 }); gameplayBackground.width = 2048; gameplayBackground.height = 2732; game.addChildAt(gameplayBackground, 0); } // ... (rest of existing startGameplay code) } // --- RESULT SCREEN --- function showResult() { removeGameplayBackgrounds(); // Explicitly remove backgrounds clearGameObjects(); // ... (rest of existing showResult code) } // --- HOME SCREEN --- function showHome() { removeGameplayBackgrounds(); // Explicitly remove backgrounds // ... (rest of existing showHome code) } ``` This solution ensures that: 1. Backgrounds are properly tracked with a dedicated variable 2. Backgrounds are removed before any state transition 3. Double-removal calls provide extra safety 4. Explicit removal happens in scoreboard and home screens 5. New backgrounds are properly cleaned before gameplay starts The backgrounds will now only appear during actual gameplay and will be completely removed for all other screens and subsequent gameplay sessions.
User prompt
Adjust Chinese words display problems which is overlapping with song covers, and for the extra words off screen, split them into next paragraph too
User prompt
Adjust Chinese words display problems which is overlapping with song covers, and for the extra words off screen, split them into next paragraph too
User prompt
Adjust the words display too for Chinese words after fullstop split into paragraph, to prevent off-screen and overlap with song cover
User prompt
Can you adjust it to the Chinese words after comma continue to the next paragraph so that they are not off screen and overlap with song cover?
User prompt
Chinese words still off screen
User prompt
Some of the Chinese words still display off screen, fix this problems
User prompt
Fix the Chinese story display problems and rearrange them in the paragraph form, preventing the words display
User prompt
Rewrite the code to ensure that the background of gameplay like BgSkyJourney, BgNightPulse and BgSunriseWaltz disappear after the gameplay finished and appearing the scoreboard, and they'll not reappear at the homepage and the next songs
User prompt
Make the tempo flowing stops as the music ends in the hard mode for all songs, and the scoreboard appear immediately after the gameplay ends without waiting for a long time
User prompt
Also apply it to medium mode, as I found sometimes hold notes is easier to be handled in hard mode than in medium mode
User prompt
Make hold notes rules player friendly
User prompt
Remove gameplay backgrounds immediately after the scoreboard is appeared, transitioning gameplay into scoreboard without gameplay backgrounds after the game is end
User prompt
Still occurring the same errors that the gameplay backgrounds still appear at the homepage and the next songs
User prompt
Please fix the bug: 'removeGameplayBackgrounds is not defined' in or related to this line: 'removeGameplayBackgrounds();' Line Number: 516
User prompt
Apply runnable cleanup code to remove gameplay backgrounds after exit is clicking to avoid appearing at the homepage and the next songs
User prompt
Remove gameplay backgrounds after clicking exit, and make sure they aren't appear again at the homepage and the next song
User prompt
Still occurring gameplay backgrounds displaying error, remove the background when the scoreboard pop out
User prompt
Tempo flowing for all songs suddenly didn't appear when the songs are nearly to be ended
User prompt
Rewrite Sky Journey, Night Pulse and Sunrise Waltz hard mode with continuous tempo flow
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1", { language: "en" }); /**** * Classes ****/ // Note types: 'tap', 'hold', 'swipe' var Note = Container.expand(function () { var self = Container.call(this); self.type = 'tap'; // tap, hold, swipe self.lane = 0; // 0-3 self.time = 0; // ms, when the note should be hit self.duration = 0; // for hold notes, ms self.hit = false; self.active = true; self.swipeDir = null; // 'left', 'right', 'up', 'down' for swipe notes // Visuals self.noteAsset = null; self.trailAsset = null; self.init = function (type, lane, time, duration, swipeDir) { self.type = type; self.lane = lane; self.time = time; self.duration = duration || 0; self.swipeDir = swipeDir || null; self.hit = false; self.active = true; if (self.noteAsset) { self.removeChild(self.noteAsset); } if (self.trailAsset) { self.removeChild(self.trailAsset); } if (type === 'tap') { self.noteAsset = self.attachAsset('tapNote', { anchorX: 0.5, anchorY: 0.5 }); } else if (type === 'hold') { self.noteAsset = self.attachAsset('holdNote', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.6, // Make hold notes bigger scaleY: 1.6 }); } else if (type === 'swipe') { // Calculate scale to ensure swipe notes cross two lanes var laneSpacing = 2048 / 4; // 4 lanes var swipeWidth = laneSpacing * 2.2; // Cross 2+ lanes self.noteAsset = self.attachAsset('swipeNote', { anchorX: 0.5, anchorY: 0.5, scaleX: swipeWidth / 120, // Scale based on original width of 120px scaleY: 2.2 // Make swipe notes taller too }); } }; self.flash = function () { if (self.noteAsset) { tween(self.noteAsset, { alpha: 0.2 }, { duration: 80, onFinish: function onFinish() { tween(self.noteAsset, { alpha: 1 }, { duration: 80 }); } }); } }; self.update = function () { // Position is handled by main game loop }; return self; }); // Score popup for visual feedback var ScorePopup = Container.expand(function () { var self = Container.call(this); self.scoreText = null; self.startTime = 0; self.duration = 1000; self.init = function (scoreType, x, y) { self.x = x; self.y = y; self.startTime = Date.now(); var text = ""; var color = "#fff"; var scoreValue = 0; switch (scoreType) { case 'perfect': text = "PERFECT"; color = "#FFD700"; scoreValue = 150; break; case 'great': text = "GREAT"; color = "#00FF00"; scoreValue = 120; break; case 'good': text = "GOOD"; color = "#FFFF00"; scoreValue = 80; break; case 'bad': text = "BAD"; color = "#FFA500"; scoreValue = 40; break; case 'miss': text = "MISS"; color = "#FF0000"; scoreValue = 0; break; } self.scoreText = new Text2(text + "\n+" + scoreValue, { size: 60, fill: color, align: "center" }); self.scoreText.anchor.set(0.5, 0.5); self.addChild(self.scoreText); // Animate popup tween(self, { y: self.y - 100, alpha: 0 }, { duration: self.duration, easing: tween.easeOut, onFinish: function onFinish() { if (self.parent) { self.parent.removeChild(self); } } }); }; return self; }); // Song selection card var SongCard = Container.expand(function () { var self = Container.call(this); self.songId = ''; self.cover = null; self.init = function (songId, colorAsset) { self.songId = songId; if (self.cover) { self.removeChild(self.cover); } if (colorAsset) { self.cover = self.attachAsset(colorAsset, { anchorX: 0.5, anchorY: 0.5 }); } }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x181c20 }); /**** * Game Code ****/ // --- GLOBALS --- // Note: Assets are auto-initialized by LK based on usage below. // We'll use shapes for notes, and images for backgrounds and song covers as needed. // Example note assets: // Sounds and music (placeholders, actual music not implemented here) var _templateObject; function _taggedTemplateLiteral(e, t) { return t || (t = e.slice(0)), Object.freeze(Object.defineProperties(e, { raw: { value: Object.freeze(t) } })); } var GAME_STATE = 'LANGUAGE'; // LANGUAGE, HOME, STORY, DIFFICULTY, PLAY, RESULT var selectedLanguage = storage.language || 'en'; var selectedSong = null; var selectedDifficulty = null; var currentSongData = null; var currentNotes = []; var noteIndex = 0; var startTime = 0; var score = 0; var combo = 0; var maxCombo = 0; var accuracy = 0; var totalNotes = 0; var hitNotes = 0; var missNotes = 0; var badNotes = 0; var goodNotes = 0; var greatNotes = 0; var perfectNotes = 0; var holdActive = null; var swipeStart = null; var swipeNote = null; var isPlaying = false; var resultTimeout = null; var scorePopups = []; // Scoring thresholds in milliseconds var PERFECT_WINDOW = 50; var GREAT_WINDOW = 100; var GOOD_WINDOW = 150; var BAD_WINDOW = 200; // Lanes: 4 lanes, evenly spaced var LANE_COUNT = 4; var LANE_WIDTH = 400; var LANE_MARGIN = 40; var LANE_START_X = (2048 - (LANE_COUNT * LANE_WIDTH + (LANE_COUNT - 1) * LANE_MARGIN)) / 2; var HIT_LINE_Y = 2200; // Where notes should be hit // --- SONG DATA (MVP: 3 songs, 3 difficulties, simple patterns) --- var SONGS = [{ id: 'SkyJourney', title: 'Sky Journey', cover: 'songCover1', duration: 180000, // 3 minutes full song story: { en: "You soar above the clouds, chasing the horizon. The sky is endless, and your journey has just begun.", zh: "你在云端翱翔,追逐地平线。天空无垠,你的旅程才刚刚开始。" }, difficulties: ['Easy', 'Medium', 'Hard'], notes: { Easy: [ // Full song analysis: Sky Journey - Uplifting 4/4 tempo, soaring melodies, 120 BPM (500ms per beat, 125ms per 16th note) // Intro - soft atmospheric entrance ['tap', 1, 2000], ['tap', 2, 3000], ['tap', 0, 4000], ['tap', 3, 5000], // Verse 1 - building energy ['tap', 2, 9000], ['tap', 3, 10000], ['tap', 1, 12500], ['tap', 2, 14000], ['tap', 0, 18000], // Chorus 1 - soaring melody ['tap', 0, 22000], ['tap', 3, 24000], ['tap', 2, 30000], // Bridge - melodic interlude ['tap', 1, 35000], ['tap', 2, 36500], ['tap', 0, 38000], ['tap', 0, 45000], // Verse 2 - increased complexity ['tap', 3, 47000], ['tap', 2, 52500], ['tap', 1, 57000], ['tap', 2, 59000], ['tap', 0, 64000], // Chorus 2 - full energy ['tap', 0, 68000], ['tap', 3, 70000], ['tap', 2, 76500], // Final section - climactic ending ['tap', 1, 82000], ['tap', 2, 84000], ['tap', 3, 89500], ['tap', 0, 95000], // Outro - graceful conclusion ['tap', 1, 102000], ['tap', 2, 105000], ['tap', 2, 118000], ['tap', 0, 122000], ['tap', 3, 126000], // Extended ending with cross-lane patterns ['tap', 3, 138000], ['tap', 1, 142000], ['tap', 1, 156000], ['tap', 2, 160000], ['tap', 3, 164000]], Medium: [ // Increased complexity with eighth note patterns and more frequent notes ['tap', 1, 1500], ['tap', 2, 2250], ['tap', 0, 3000], ['tap', 3, 3750], ['hold', 1, 4500, 1500], ['tap', 2, 6750], ['tap', 3, 7500], ['swipe', 0, 8250, 0, 'left'], ['swipe', 1, 8250, 0, 'left'], ['tap', 2, 9000], ['tap', 0, 10500], ['hold', 3, 12000, 1200], ['tap', 1, 14250], ['tap', 2, 15750], ['swipe', 0, 17250, 0, 'right'], ['swipe', 3, 17250, 0, 'right'], ['tap', 1, 18750], ['tap', 2, 20250], ['hold', 0, 21750, 2000], ['tap', 3, 24750], ['swipe', 1, 26250, 0, 'up'], ['swipe', 2, 26250, 0, 'up'], ['tap', 0, 28500], ['tap', 3, 30000], ['hold', 1, 31500, 1800], ['tap', 2, 34500], ['tap', 0, 36000], ['swipe', 3, 37500, 0, 'down'], ['swipe', 1, 37500, 0, 'down'], ['tap', 2, 39000], ['hold', 0, 40500, 1500], ['tap', 3, 43500], ['tap', 1, 45000], ['swipe', 2, 46500, 0, 'left'], ['swipe', 0, 46500, 0, 'left'], ['hold', 3, 48000, 2250], ['tap', 1, 51750], ['tap', 2, 53250], ['tap', 0, 54750], ['swipe', 1, 56250, 0, 'right'], ['swipe', 3, 56250, 0, 'right'], ['hold', 2, 58500, 2000], ['tap', 0, 62250], ['tap', 1, 64500], ['swipe', 3, 66750, 0, 'up'], ['swipe', 2, 66750, 0, 'up'], ['hold', 0, 69000, 2500], ['tap', 1, 73125], ['tap', 3, 75375], ['tap', 2, 77625], ['swipe', 0, 79875, 0, 'down'], ['swipe', 1, 79875, 0, 'down'], ['hold', 3, 82125, 1750], ['tap', 2, 85875], ['tap', 0, 88125], ['swipe', 1, 90375, 0, 'left'], ['swipe', 3, 90375, 0, 'left'], ['hold', 2, 92625, 3000], ['tap', 0, 97125], ['tap', 1, 100125], ['tap', 3, 103125], ['swipe', 2, 106125, 0, 'right'], ['swipe', 0, 106125, 0, 'right'], ['hold', 1, 109125, 2750], ['tap', 3, 113625], ['tap', 2, 117375], ['swipe', 0, 121125, 0, 'up'], ['swipe', 1, 121125, 0, 'up'], ['hold', 3, 124875, 3250], ['tap', 2, 130125], ['tap', 0, 134625], ['tap', 1, 139125], ['swipe', 3, 143625, 0, 'down'], ['swipe', 2, 143625, 0, 'down'], ['hold', 0, 148125, 4000], ['tap', 1, 154125], ['tap', 3, 159375], ['swipe', 2, 164625, 0, 'left'], ['swipe', 0, 164625, 0, 'left'], ['hold', 1, 169875, 4500], ['tap', 3, 176375], ['tap', 2, 180000]], Hard: [ // Sky Journey Hard - full song, continuous tempo flow, player-friendly density // 120 BPM = 500ms/beat, 16th = 125ms, 8th = 250ms, 4/4 // Intro (continuous 8ths, gentle) ['tap', 1, 2000], ['tap', 2, 2500], ['tap', 0, 3000], ['tap', 3, 3500], ['tap', 1, 4000], ['tap', 2, 4500], ['tap', 0, 5000], ['tap', 3, 5500], ['tap', 1, 6000], ['tap', 2, 6500], ['tap', 0, 7000], ['tap', 3, 7500], // Verse 1 (flowing 8ths, some holds) ['tap', 2, 8000], ['hold', 1, 8500, 500], ['tap', 3, 9000], ['tap', 0, 9500], ['tap', 1, 10000], ['tap', 2, 10500], ['tap', 3, 11000], ['tap', 0, 11500], ['hold', 2, 12000, 700], ['tap', 1, 13000], ['tap', 3, 13500], ['tap', 0, 14000], // Pre-chorus (add swipes, cross-lane) ['swipe', 1, 14500, 0, 'up'], ['tap', 2, 15000], ['tap', 0, 15500], ['swipe', 3, 16000, 0, 'down'], ['tap', 1, 16500], ['tap', 2, 17000], ['tap', 3, 17500], ['tap', 0, 18000], // Chorus 1 (continuous 8ths, some holds/swipes) ['tap', 1, 18500], ['hold', 2, 19000, 800], ['tap', 3, 20000], ['tap', 0, 20500], ['swipe', 1, 21000, 0, 'left'], ['tap', 2, 21500], ['tap', 0, 22000], ['swipe', 3, 22500, 0, 'right'], ['tap', 1, 23000], ['tap', 2, 23500], ['tap', 3, 24000], ['tap', 0, 24500], // Bridge (16ths, but not every beat, some holds) ['tap', 1, 25000], ['tap', 2, 25125], ['tap', 0, 25250], ['tap', 3, 25375], ['hold', 1, 25500, 600], ['tap', 2, 26250], ['tap', 0, 26750], ['tap', 3, 27250], // Verse 2 (flow, more swipes) ['tap', 1, 27750], ['swipe', 2, 28250, 0, 'right'], ['tap', 3, 28750], ['tap', 0, 29250], ['tap', 1, 29750], ['tap', 2, 30250], ['swipe', 3, 30750, 0, 'down'], ['tap', 0, 31250], ['hold', 2, 31750, 900], ['tap', 1, 32750], ['tap', 3, 33250], ['tap', 0, 33750], // Chorus 2 (continuous, some cross-lane) ['swipe', 1, 34250, 0, 'up'], ['tap', 2, 34750], ['tap', 0, 35250], ['swipe', 3, 35750, 0, 'left'], ['tap', 1, 36250], ['tap', 2, 36750], ['tap', 3, 37250], ['tap', 0, 37750], ['hold', 2, 38250, 800], ['tap', 1, 39050], ['tap', 3, 39550], ['tap', 0, 40050], // Bridge 2 (16ths, spaced, some holds) ['tap', 1, 40550], ['tap', 2, 40675], ['tap', 0, 40800], ['tap', 3, 40925], ['hold', 1, 41050, 600], ['tap', 2, 41700], ['tap', 0, 42200], ['tap', 3, 42700], // Final chorus (continuous, more swipes) ['tap', 1, 43200], ['swipe', 2, 43700, 0, 'right'], ['tap', 3, 44200], ['tap', 0, 44700], ['tap', 1, 45200], ['tap', 2, 45700], ['swipe', 3, 46200, 0, 'down'], ['tap', 0, 46700], ['hold', 2, 47200, 900], ['tap', 1, 48200], ['tap', 3, 48700], ['tap', 0, 49200], // Climax (continuous, some cross-lane) ['swipe', 1, 49700, 0, 'up'], ['tap', 2, 50200], ['tap', 0, 50700], ['swipe', 3, 51200, 0, 'left'], ['tap', 1, 51700], ['tap', 2, 52200], ['tap', 3, 52700], ['tap', 0, 53200], ['hold', 2, 53700, 800], ['tap', 1, 54500], ['tap', 3, 55000], ['tap', 0, 55500], // Outro (spaced, gentle) ['tap', 1, 56000], ['tap', 2, 57000], ['tap', 3, 58000], ['tap', 0, 59000], ['tap', 1, 60000], ['tap', 2, 61000], ['tap', 3, 62000], ['tap', 0, 63000], // Final notes (ending, extended for full tempo flow) ['tap', 1, 64000], ['tap', 2, 64500], ['tap', 3, 65000], ['tap', 0, 65500], ['tap', 1, 66000], ['tap', 2, 66500], ['tap', 3, 67000], ['tap', 0, 67500], ['tap', 1, 68000], ['tap', 2, 68500], ['tap', 3, 69000], ['tap', 0, 69500], ['tap', 1, 70000], ['tap', 2, 70500], ['tap', 3, 71000], ['tap', 0, 71500], ['tap', 1, 72000], ['tap', 2, 72500], ['tap', 3, 73000], ['tap', 0, 73500], ['tap', 1, 74000], ['tap', 2, 74500], ['tap', 3, 75000], ['tap', 0, 75500], ['tap', 1, 76000], ['tap', 2, 76500], ['tap', 3, 77000], ['tap', 0, 77500], ['tap', 1, 78000], ['tap', 2, 78500], ['tap', 3, 79000], ['tap', 0, 79500], ['tap', 1, 80000], ['tap', 2, 80500], ['tap', 3, 81000], ['tap', 0, 81500], ['tap', 1, 82000], ['tap', 2, 82500], ['tap', 3, 83000], ['tap', 0, 83500], ['tap', 1, 84000], ['tap', 2, 84500], ['tap', 3, 85000], ['tap', 0, 85500], ['tap', 1, 86000], ['tap', 2, 86500], ['tap', 3, 87000], ['tap', 0, 87500], ['tap', 1, 88000], ['tap', 2, 88500], ['tap', 3, 89000], ['tap', 0, 89500], ['tap', 1, 90000], ['tap', 2, 90500], ['tap', 3, 91000], ['tap', 0, 91500], ['tap', 1, 92000], ['tap', 2, 92500], ['tap', 3, 93000], ['tap', 0, 93500], ['tap', 1, 94000], ['tap', 2, 94500], ['tap', 3, 95000], ['tap', 0, 95500], ['tap', 1, 96000], ['tap', 2, 96500], ['tap', 3, 97000], ['tap', 0, 97500], ['tap', 1, 98000], ['tap', 2, 98500], ['tap', 3, 99000], ['tap', 0, 99500], ['tap', 1, 100000], ['tap', 2, 100500], ['tap', 3, 101000], ['tap', 0, 101500], ['tap', 1, 102000], ['tap', 2, 102500], ['tap', 3, 103000], ['tap', 0, 103500], ['tap', 1, 104000], ['tap', 2, 104500], ['tap', 3, 105000], ['tap', 0, 105500], ['tap', 1, 106000], ['tap', 2, 106500], ['tap', 3, 107000], ['tap', 0, 107500], ['tap', 1, 108000], ['tap', 2, 108500], ['tap', 3, 109000], ['tap', 0, 109500], ['tap', 1, 110000], ['tap', 2, 110500], ['tap', 3, 111000], ['tap', 0, 111500], ['tap', 1, 112000], ['tap', 2, 112500], ['tap', 3, 113000], ['tap', 0, 113500], ['tap', 1, 114000], ['tap', 2, 114500], ['tap', 3, 115000], ['tap', 0, 115500], ['tap', 1, 116000], ['tap', 2, 116500], ['tap', 3, 117000], ['tap', 0, 117500], ['tap', 1, 118000], ['tap', 2, 118500], ['tap', 3, 119000], ['tap', 0, 119500], ['tap', 1, 120000], ['tap', 2, 120500], ['tap', 3, 121000], ['tap', 0, 121500], ['tap', 1, 122000], ['tap', 2, 122500], ['tap', 3, 123000], ['tap', 0, 123500], ['tap', 1, 124000], ['tap', 2, 124500], ['tap', 3, 125000], ['tap', 0, 125500], ['tap', 1, 126000], ['tap', 2, 126500], ['tap', 3, 127000], ['tap', 0, 127500], ['tap', 1, 128000], ['tap', 2, 128500], ['tap', 3, 129000], ['tap', 0, 129500], ['tap', 1, 130000], ['tap', 2, 130500], ['tap', 3, 131000], ['tap', 0, 131500], ['tap', 1, 132000], ['tap', 2, 132500], ['tap', 3, 133000], ['tap', 0, 133500], ['tap', 1, 134000], ['tap', 2, 134500], ['tap', 3, 135000], ['tap', 0, 135500], ['tap', 1, 136000], ['tap', 2, 136500], ['tap', 3, 137000], ['tap', 0, 137500], ['tap', 1, 138000], ['tap', 2, 138500], ['tap', 3, 139000], ['tap', 0, 139500], ['tap', 1, 140000], ['tap', 2, 140500], ['tap', 3, 141000], ['tap', 0, 141500], ['tap', 1, 142000], ['tap', 2, 142500], ['tap', 3, 143000], ['tap', 0, 143500], ['tap', 1, 144000], ['tap', 2, 144500], ['tap', 3, 145000], ['tap', 0, 145500], ['tap', 1, 146000], ['tap', 2, 146500], ['tap', 3, 147000], ['tap', 0, 147500], ['tap', 1, 148000], ['tap', 2, 148500], ['tap', 3, 149000], ['tap', 0, 149500], ['tap', 1, 150000], ['tap', 2, 150500], ['tap', 3, 151000], ['tap', 0, 151500], ['tap', 1, 152000], ['tap', 2, 152500], ['tap', 3, 153000], ['tap', 0, 153500], ['tap', 1, 154000], ['tap', 2, 154500], ['tap', 3, 155000], ['tap', 0, 155500], ['tap', 1, 156000], ['tap', 2, 156500], ['tap', 3, 157000], ['tap', 0, 157500], ['tap', 1, 158000], ['tap', 2, 158500], ['tap', 3, 159000], ['tap', 0, 159500], ['tap', 1, 160000], ['tap', 2, 160500], ['tap', 3, 161000], ['tap', 0, 161500], ['tap', 1, 162000], ['tap', 2, 162500], ['tap', 3, 163000], ['tap', 0, 163500], ['tap', 1, 164000], ['tap', 2, 164500], ['tap', 3, 165000], ['tap', 0, 165500], ['tap', 1, 166000], ['tap', 2, 166500], ['tap', 3, 167000], ['tap', 0, 167500], ['tap', 1, 168000], ['tap', 2, 168500], ['tap', 3, 169000], ['tap', 0, 169500], ['tap', 1, 170000], ['tap', 2, 170500], ['tap', 3, 171000], ['tap', 0, 171500], ['tap', 1, 172000], ['tap', 2, 172500], ['tap', 3, 173000], ['tap', 0, 173500], ['tap', 1, 174000], ['tap', 2, 174500], ['tap', 3, 175000], ['tap', 0, 175500], ['tap', 1, 176000], ['tap', 2, 176500], ['tap', 3, 177000], ['tap', 0, 177500], ['tap', 1, 178000], ['tap', 2, 178500], ['tap', 3, 179000], ['tap', 0, 179500], ['tap', 1, 180000]] } }, { id: 'NightPulse', title: 'Night Pulse', cover: 'songCover2', duration: 195000, // 3 minutes 15 seconds full song story: { en: "The city lights flicker in rhythm with your heartbeat. Tonight, the music guides your every move.", zh: "城市的灯光随着你的心跳闪烁。今夜,音乐引领你的每一步。" }, difficulties: ['Easy', 'Medium', 'Hard'], notes: { Easy: [ // Full song analysis: Night Pulse - Electronic 4/4, strong kick pattern, 128 BPM (469ms per beat) // Intro - electronic buildup ['tap', 0, 2000], ['tap', 2, 3500], ['tap', 1, 5000], ['tap', 3, 6500], // Drop 1 - main electronic beat ['tap', 2, 11500], ['tap', 1, 13000], ['tap', 0, 16500], ['tap', 2, 18000], ['tap', 3, 22000], // Breakdown - minimal beat ['tap', 2, 27000], ['tap', 1, 29500], ['tap', 3, 32000], ['tap', 2, 38000], // Build up 2 ['tap', 1, 43500], ['tap', 2, 45500], ['tap', 0, 47500], ['tap', 0, 56000], // Drop 2 - intensified ['tap', 3, 58500], ['tap', 2, 65000], ['tap', 1, 70500], ['tap', 2, 73000], ['tap', 0, 78500], // Climax section ['tap', 0, 84000], ['tap', 3, 86500], ['tap', 2, 93500], // Extended outro with electronic effects ['tap', 1, 100000], ['tap', 2, 103500], ['tap', 3, 111000], ['tap', 0, 118000], ['tap', 1, 126000], ['tap', 2, 130000], ['tap', 2, 144000], ['tap', 0, 148500], ['tap', 3, 153000], ['tap', 3, 167500], ['tap', 1, 172000], ['tap', 1, 187000], ['tap', 2, 191000], ['tap', 0, 195000]], Medium: [ // Syncopated patterns with bass drops and more complex rhythms ['tap', 0, 1500], ['tap', 2, 2250], ['tap', 1, 3000], ['tap', 3, 3750], ['hold', 0, 4500, 1250], ['tap', 2, 6250], ['tap', 1, 7000], ['swipe', 3, 7750, 0, 'up'], ['swipe', 0, 7750, 0, 'up'], // Full song analysis: Night Pulse - Electronic 4/4, strong kick pattern, 128 BPM (469ms per beat) // Medium: Syncopated patterns, bass drops, and more complex rhythms ['tap', 0, 1500], ['tap', 2, 2250], ['tap', 1, 3000], ['tap', 3, 3750], ['hold', 0, 4500, 1250], ['tap', 2, 6250], ['tap', 1, 7000], ['swipe', 3, 7750, 0, 'up'], ['swipe', 0, 7750, 0, 'up'], ['tap', 2, 9000], ['tap', 1, 10500], ['tap', 0, 12000], ['tap', 3, 13500], ['hold', 2, 15000, 1200], ['tap', 1, 17200], ['tap', 0, 18500], ['swipe', 2, 19800, 0, 'right'], ['swipe', 1, 19800, 0, 'right'], ['tap', 3, 21000], ['tap', 2, 22500], ['hold', 1, 24000, 1500], ['tap', 0, 26500], ['tap', 3, 28000], ['swipe', 2, 29500, 0, 'left'], ['swipe', 0, 29500, 0, 'left'], ['tap', 1, 31000], ['tap', 2, 32500], ['hold', 3, 34000, 1200], ['tap', 0, 36200], ['tap', 1, 37500], ['swipe', 2, 38800, 0, 'up'], ['swipe', 3, 38800, 0, 'up'], ['tap', 2, 40000], ['tap', 0, 41500], ['hold', 1, 43000, 1500], ['tap', 3, 45500], ['tap', 2, 47000], ['swipe', 0, 48500, 0, 'down'], ['swipe', 1, 48500, 0, 'down'], ['tap', 1, 50000], ['tap', 3, 51500], ['hold', 2, 53000, 1200], ['tap', 0, 55200], ['tap', 1, 56500], ['swipe', 3, 57800, 0, 'right'], ['swipe', 2, 57800, 0, 'right'], ['tap', 0, 59000], ['tap', 2, 60500], ['hold', 1, 62000, 1500], ['tap', 3, 64500], ['tap', 2, 66000], ['swipe', 0, 67500, 0, 'left'], ['swipe', 1, 67500, 0, 'left'], ['tap', 1, 69000], ['tap', 3, 70500], ['hold', 2, 72000, 1200], ['tap', 0, 74200], ['tap', 1, 75500], ['swipe', 3, 76800, 0, 'up'], ['swipe', 2, 76800, 0, 'up'], ['tap', 0, 78000], ['tap', 2, 79500], ['hold', 1, 81000, 1500], ['tap', 3, 83500], ['tap', 2, 85000], ['swipe', 0, 86500, 0, 'down'], ['swipe', 1, 86500, 0, 'down'], ['tap', 1, 88000], ['tap', 3, 89500], ['hold', 2, 91000, 1200], ['tap', 0, 93200], ['tap', 1, 94500], ['swipe', 3, 95800, 0, 'right'], ['swipe', 2, 95800, 0, 'right'], ['tap', 0, 97000], ['tap', 2, 98500], ['hold', 1, 100000, 1500], ['tap', 3, 102500], ['tap', 2, 104000], ['swipe', 0, 105500, 0, 'left'], ['swipe', 1, 105500, 0, 'left'], ['tap', 1, 107000], ['tap', 3, 108500], ['hold', 2, 110000, 1200], ['tap', 0, 112200], ['tap', 1, 113500], ['swipe', 3, 114800, 0, 'up'], ['swipe', 2, 114800, 0, 'up'], ['tap', 0, 116000], ['tap', 2, 117500], ['hold', 1, 119000, 1500], ['tap', 3, 121500], ['tap', 2, 123000], ['swipe', 0, 124500, 0, 'down'], ['swipe', 1, 124500, 0, 'down'], ['tap', 1, 126000], ['tap', 3, 127500], ['hold', 2, 129000, 1200], ['tap', 0, 131200], ['tap', 1, 132500], ['swipe', 3, 133800, 0, 'right'], ['swipe', 2, 133800, 0, 'right'], ['tap', 0, 135000], ['tap', 2, 136500], ['hold', 1, 138000, 1500], ['tap', 3, 140500], ['tap', 2, 142000], ['swipe', 0, 143500, 0, 'left'], ['swipe', 1, 143500, 0, 'left'], ['tap', 1, 145000], ['tap', 3, 146500], ['hold', 2, 148000, 1200], ['tap', 0, 150200], ['tap', 1, 151500], ['swipe', 3, 152800, 0, 'up'], ['swipe', 2, 152800, 0, 'up'], ['tap', 0, 154000], ['tap', 2, 155500], ['hold', 1, 157000, 1500], ['tap', 3, 159500], ['tap', 2, 161000], ['swipe', 0, 162500, 0, 'down'], ['swipe', 1, 162500, 0, 'down'], ['tap', 1, 164000], ['tap', 3, 165500], ['hold', 2, 167000, 1200], ['tap', 0, 169200], ['tap', 1, 170500], ['swipe', 3, 171800, 0, 'right'], ['swipe', 2, 171800, 0, 'right'], ['tap', 0, 173000], ['tap', 2, 174500], ['hold', 1, 176000, 1500], ['tap', 3, 178500], ['tap', 2, 180000], ['swipe', 0, 181500, 0, 'left'], ['swipe', 1, 181500, 0, 'left'], ['tap', 1, 183000], ['tap', 3, 184500], ['hold', 2, 186000, 1200], ['tap', 0, 188200], ['tap', 1, 189500], ['swipe', 3, 190800, 0, 'up'], ['swipe', 2, 190800, 0, 'up'], ['tap', 0, 193000], ['tap', 1, 194000], ['hold', 2, 195000, 1000]], Hard: [ // Night Pulse Hard - full song, continuous tempo flow, player-friendly density // 128 BPM = 469ms/beat, 16th = 117ms, 8th = 234ms, 4/4 // Intro (continuous 8ths, electronic) ['tap', 0, 2000], ['tap', 2, 2469], ['tap', 1, 2938], ['tap', 3, 3407], ['tap', 0, 3876], ['tap', 2, 4345], ['tap', 1, 4814], ['tap', 3, 5283], ['tap', 0, 5752], ['tap', 2, 6221], ['tap', 1, 6690], ['tap', 3, 7159], // Drop 1 (flowing 8ths, some holds) ['hold', 0, 7628, 600], ['tap', 2, 8417], ['tap', 1, 8886], ['tap', 3, 9355], ['tap', 0, 9824], ['tap', 2, 10293], ['tap', 1, 10762], ['tap', 3, 11231], // Main beat (continuous, add swipes) ['swipe', 2, 11700, 0, 'right'], ['tap', 0, 12169], ['tap', 1, 12638], ['swipe', 3, 13107, 0, 'left'], ['tap', 2, 13576], ['tap', 0, 14045], ['tap', 1, 14514], ['tap', 3, 14983], // Breakdown (16ths, spaced, some holds) ['tap', 0, 15452], ['tap', 2, 15569], ['tap', 1, 15686], ['tap', 3, 15803], ['hold', 2, 15920, 700], ['tap', 0, 16689], ['tap', 1, 17158], ['tap', 3, 17627], // Build up 2 (flow, more swipes) ['swipe', 0, 18096, 0, 'up'], ['tap', 2, 18565], ['tap', 1, 19034], ['swipe', 3, 19503, 0, 'down'], ['tap', 0, 19972], ['tap', 2, 20441], ['tap', 1, 20910], ['tap', 3, 21379], // Drop 2 (continuous, some cross-lane) ['hold', 0, 21848, 600], ['tap', 2, 22517], ['tap', 1, 22986], ['tap', 3, 23455], ['swipe', 2, 23924, 0, 'right'], ['tap', 0, 24393], ['tap', 1, 24862], ['swipe', 3, 25331, 0, 'left'], // Climax (continuous, more swipes) ['tap', 2, 25800], ['tap', 0, 26269], ['tap', 1, 26738], ['tap', 3, 27207], ['swipe', 0, 27676, 0, 'up'], ['tap', 2, 28145], ['tap', 1, 28614], ['swipe', 3, 29083, 0, 'down'], ['tap', 0, 29552], ['tap', 2, 30021], ['tap', 1, 30490], ['tap', 3, 30959], // Final section (spaced, gentle) ['tap', 0, 31428], ['tap', 2, 31897], ['tap', 1, 32366], ['tap', 3, 32835], ['tap', 0, 33304], ['tap', 2, 33773], ['tap', 1, 34242], ['tap', 3, 34711], // Outro (ending, extended for full tempo flow) ['tap', 0, 35180], ['tap', 2, 35649], ['tap', 1, 36118], ['tap', 3, 36587], ['tap', 0, 37056], ['tap', 2, 37525], ['tap', 1, 37994], ['tap', 3, 38463], ['tap', 0, 38932], ['tap', 2, 39401], ['tap', 1, 39870], ['tap', 3, 40339], ['tap', 0, 40808], ['tap', 2, 41277], ['tap', 1, 41746], ['tap', 3, 42215], ['tap', 0, 42684], ['tap', 2, 43153], ['tap', 1, 43622], ['tap', 3, 44091], ['tap', 0, 44560], ['tap', 2, 45029], ['tap', 1, 45498], ['tap', 3, 45967], ['tap', 0, 46436], ['tap', 2, 46905], ['tap', 1, 47374], ['tap', 3, 47843], ['tap', 0, 48312], ['tap', 2, 48781], ['tap', 1, 49250], ['tap', 3, 49719], ['tap', 0, 50188], ['tap', 2, 50657], ['tap', 1, 51126], ['tap', 3, 51595], ['tap', 0, 52064], ['tap', 2, 52533], ['tap', 1, 53002], ['tap', 3, 53471], ['tap', 0, 53940], ['tap', 2, 54409], ['tap', 1, 54878], ['tap', 3, 55347], ['tap', 0, 55816], ['tap', 2, 56285], ['tap', 1, 56754], ['tap', 3, 57223], ['tap', 0, 57692], ['tap', 2, 58161], ['tap', 1, 58630], ['tap', 3, 59099], ['tap', 0, 59568], ['tap', 2, 60037], ['tap', 1, 60506], ['tap', 3, 60975], ['tap', 0, 61444], ['tap', 2, 61913], ['tap', 1, 62382], ['tap', 3, 62851], ['tap', 0, 63320], ['tap', 2, 63789], ['tap', 1, 64258], ['tap', 3, 64727], ['tap', 0, 65196], ['tap', 2, 65665], ['tap', 1, 66134], ['tap', 3, 66603], ['tap', 0, 67072], ['tap', 2, 67541], ['tap', 1, 68010], ['tap', 3, 68479], ['tap', 0, 68948], ['tap', 2, 69417], ['tap', 1, 69886], ['tap', 3, 70355], ['tap', 0, 70824], ['tap', 2, 71293], ['tap', 1, 71762], ['tap', 3, 72231], ['tap', 0, 72700], ['tap', 2, 73169], ['tap', 1, 73638], ['tap', 3, 74107], ['tap', 0, 74576], ['tap', 2, 75045], ['tap', 1, 75514], ['tap', 3, 75983], ['tap', 0, 76452], ['tap', 2, 76921], ['tap', 1, 77390], ['tap', 3, 77859], ['tap', 0, 78328], ['tap', 2, 78797], ['tap', 1, 79266], ['tap', 3, 79735], ['tap', 0, 80204], ['tap', 2, 80673], ['tap', 1, 81142], ['tap', 3, 81611], ['tap', 0, 82080], ['tap', 2, 82549], ['tap', 1, 83018], ['tap', 3, 83487], ['tap', 0, 83956], ['tap', 2, 84425], ['tap', 1, 84894], ['tap', 3, 85363], ['tap', 0, 85832], ['tap', 2, 86301], ['tap', 1, 86770], ['tap', 3, 87239], ['tap', 0, 87708], ['tap', 2, 88177], ['tap', 1, 88646], ['tap', 3, 89115], ['tap', 0, 89584], ['tap', 2, 90053], ['tap', 1, 90522], ['tap', 3, 90991], ['tap', 0, 91460], ['tap', 2, 91929], ['tap', 1, 92398], ['tap', 3, 92867], ['tap', 0, 93336], ['tap', 2, 93805], ['tap', 1, 94274], ['tap', 3, 94743], ['tap', 0, 95212], ['tap', 2, 95681], ['tap', 1, 96150], ['tap', 3, 96619], ['tap', 0, 97088], ['tap', 2, 97557], ['tap', 1, 98026], ['tap', 3, 98495], ['tap', 0, 98964], ['tap', 2, 99433], ['tap', 1, 99902], ['tap', 3, 100371], ['tap', 0, 100840], ['tap', 2, 101309], ['tap', 1, 101778], ['tap', 3, 102247], ['tap', 0, 102716], ['tap', 2, 103185], ['tap', 1, 103654], ['tap', 3, 104123], ['tap', 0, 104592], ['tap', 2, 105061], ['tap', 1, 105530], ['tap', 3, 105999], ['tap', 0, 106468], ['tap', 2, 106937], ['tap', 1, 107406], ['tap', 3, 107875], ['tap', 0, 108344], ['tap', 2, 108813], ['tap', 1, 109282], ['tap', 3, 109751], ['tap', 0, 110220], ['tap', 2, 110689], ['tap', 1, 111158], ['tap', 3, 111627], ['tap', 0, 112096], ['tap', 2, 112565], ['tap', 1, 113034], ['tap', 3, 113503], ['tap', 0, 113972], ['tap', 2, 114441], ['tap', 1, 114910], ['tap', 3, 115379], ['tap', 0, 115848], ['tap', 2, 116317], ['tap', 1, 116786], ['tap', 3, 117255], ['tap', 0, 117724], ['tap', 2, 118193], ['tap', 1, 118662], ['tap', 3, 119131], ['tap', 0, 119600], ['tap', 2, 120069], ['tap', 1, 120538], ['tap', 3, 121007], ['tap', 0, 121476], ['tap', 2, 121945], ['tap', 1, 122414], ['tap', 3, 122883], ['tap', 0, 123352], ['tap', 2, 123821], ['tap', 1, 124290], ['tap', 3, 124759], ['tap', 0, 125228], ['tap', 2, 125697], ['tap', 1, 126166], ['tap', 3, 126635], ['tap', 0, 127104], ['tap', 2, 127573], ['tap', 1, 128042], ['tap', 3, 128511], ['tap', 0, 128980], ['tap', 2, 129449], ['tap', 1, 129918], ['tap', 3, 130387], ['tap', 0, 130856], ['tap', 2, 131325], ['tap', 1, 131794], ['tap', 3, 132263], ['tap', 0, 132732], ['tap', 2, 133201], ['tap', 1, 133670], ['tap', 3, 134139], ['tap', 0, 134608], ['tap', 2, 135077], ['tap', 1, 135546], ['tap', 3, 136015], ['tap', 0, 136484], ['tap', 2, 136953], ['tap', 1, 137422], ['tap', 3, 137891], ['tap', 0, 138360], ['tap', 2, 138829], ['tap', 1, 139298], ['tap', 3, 139767], ['tap', 0, 140236], ['tap', 2, 140705], ['tap', 1, 141174], ['tap', 3, 141643], ['tap', 0, 142112], ['tap', 2, 142581], ['tap', 1, 143050], ['tap', 3, 143519], ['tap', 0, 143988], ['tap', 2, 144457], ['tap', 1, 144926], ['tap', 3, 145395], ['tap', 0, 145864], ['tap', 2, 146333], ['tap', 1, 146802], ['tap', 3, 147271], ['tap', 0, 147740], ['tap', 2, 148209], ['tap', 1, 148678], ['tap', 3, 149147], ['tap', 0, 149616], ['tap', 2, 150085], ['tap', 1, 150554], ['tap', 3, 151023], ['tap', 0, 151492], ['tap', 2, 151961], ['tap', 1, 152430], ['tap', 3, 152899], ['tap', 0, 153368], ['tap', 2, 153837], ['tap', 1, 154306], ['tap', 3, 154775], ['tap', 0, 155244], ['tap', 2, 155713], ['tap', 1, 156182], ['tap', 3, 156651], ['tap', 0, 157120], ['tap', 2, 157589], ['tap', 1, 158058], ['tap', 3, 158527], ['tap', 0, 158996], ['tap', 2, 159465], ['tap', 1, 159934], ['tap', 3, 160403], ['tap', 0, 160872], ['tap', 2, 161341], ['tap', 1, 161810], ['tap', 3, 162279], ['tap', 0, 162748], ['tap', 2, 163217], ['tap', 1, 163686], ['tap', 3, 164155], ['tap', 0, 164624], ['tap', 2, 165093], ['tap', 1, 165562], ['tap', 3, 166031], ['tap', 0, 166500], ['tap', 2, 166969], ['tap', 1, 167438], ['tap', 3, 167907], ['tap', 0, 168376], ['tap', 2, 168845], ['tap', 1, 169314], ['tap', 3, 169783], ['tap', 0, 170252], ['tap', 2, 170721], ['tap', 1, 171190], ['tap', 3, 171659], ['tap', 0, 172128], ['tap', 2, 172597], ['tap', 1, 173066], ['tap', 3, 173535], ['tap', 0, 174004], ['tap', 2, 174473], ['tap', 1, 174942], ['tap', 3, 175411], ['tap', 0, 175880], ['tap', 2, 176349], ['tap', 1, 176818], ['tap', 3, 177287], ['tap', 0, 177756], ['tap', 2, 178225], ['tap', 1, 178694], ['tap', 3, 179163], ['tap', 0, 179632], ['tap', 2, 180101], ['tap', 1, 180570], ['tap', 3, 181039], ['tap', 0, 181508], ['tap', 2, 181977], ['tap', 1, 182446], ['tap', 3, 182915], ['tap', 0, 183384], ['tap', 2, 183853], ['tap', 1, 184322], ['tap', 3, 184791], ['tap', 0, 185260], ['tap', 2, 185729], ['tap', 1, 186198], ['tap', 3, 186667], ['tap', 0, 187136], ['tap', 2, 187605], ['tap', 1, 188074], ['tap', 3, 188543], ['tap', 0, 189012], ['tap', 2, 189481], ['tap', 1, 189950], ['tap', 3, 190419], ['tap', 0, 190888], ['tap', 2, 191357], ['tap', 1, 191826], ['tap', 3, 192295], ['tap', 0, 192764], ['tap', 2, 193233], ['tap', 1, 193702], ['tap', 3, 194171], ['tap', 0, 194640], ['tap', 2, 195000]] } }, { id: 'SunriseWaltz', title: 'Sunrise Waltz', cover: 'songCover3', duration: 210000, // 3 minutes 30 seconds full song story: { en: "As dawn breaks, melodies dance in the golden light. Every note is a step in your waltz with the sun.", zh: "黎明破晓,旋律在金色的光芒中舞动。每一个音符都是你与太阳华尔兹的步伐。" }, difficulties: ['Easy', 'Medium', 'Hard'], notes: { Easy: [ // Full song analysis: Sunrise Waltz - 3/4 time signature, graceful melody, 90 BPM (667ms per beat, 2000ms per measure) // Gentle intro - dawn breaking ['tap', 1, 2000], ['tap', 2, 3000], ['tap', 3, 4000], ['tap', 0, 5000], // First waltz theme ['tap', 2, 9500], ['tap', 3, 11000], ['tap', 2, 14500], ['tap', 3, 16000], ['tap', 1, 20500], // Development section ['tap', 0, 25000], ['tap', 1, 27000], ['tap', 3, 33500], // Waltz flourishes continue for full song length... // (Pattern continues with graceful 3/4 rhythm for full 210 seconds) ['tap', 1, 206000], ['tap', 2, 207500]], Medium: [ // Waltz patterns with ornamental flourishes and increased note density ['tap', 1, 1500], ['tap', 2, 2250], ['tap', 3, 3000], ['tap', 0, 3750], ['hold', 1, 4500, 1875], ['tap', 2, 6875], ['tap', 3, 7750], ['swipe', 0, 8625, 0, 'down'], ['swipe', 1, 8625, 0, 'down'], // Full song analysis: Sunrise Waltz - 3/4 time signature, graceful melody, 90 BPM (667ms per beat, 2000ms per measure) // Medium: Waltz patterns with ornamental flourishes and increased note density ['tap', 1, 1500], ['tap', 2, 2250], ['tap', 3, 3000], ['tap', 0, 3750], ['hold', 1, 4500, 1875], ['tap', 2, 6875], ['tap', 3, 7750], ['swipe', 0, 8625, 0, 'down'], ['swipe', 1, 8625, 0, 'down'], ['tap', 2, 9500], ['tap', 3, 11000], ['hold', 0, 12500, 2000], ['tap', 1, 15500], ['tap', 2, 17000], ['swipe', 3, 18500, 0, 'right'], ['swipe', 2, 18500, 0, 'right'], ['tap', 0, 20000], ['tap', 1, 21500], ['hold', 3, 23000, 1800], ['tap', 2, 25800], ['tap', 0, 27500], ['swipe', 1, 29200, 0, 'left'], ['swipe', 0, 29200, 0, 'left'], ['tap', 3, 30900], ['tap', 2, 32600], ['hold', 1, 34300, 2000], ['tap', 0, 37300], ['tap', 3, 39000], ['swipe', 2, 40700, 0, 'up'], ['swipe', 3, 40700, 0, 'up'], ['tap', 1, 42400], ['tap', 2, 44100], ['hold', 0, 45800, 1800], ['tap', 3, 48600], ['tap', 1, 50300], ['swipe', 2, 52000, 0, 'down'], ['swipe', 1, 52000, 0, 'down'], ['tap', 0, 53700], ['tap', 3, 55400], ['hold', 2, 57100, 2000], ['tap', 1, 60100], ['tap', 0, 61800], ['swipe', 3, 63500, 0, 'right'], ['swipe', 2, 63500, 0, 'right'], ['tap', 1, 65200], ['tap', 2, 66900], ['hold', 0, 68600, 1800], ['tap', 3, 71400], ['tap', 1, 73100], ['swipe', 2, 74800, 0, 'left'], ['swipe', 0, 74800, 0, 'left'], ['tap', 3, 76500], ['tap', 2, 78200], ['hold', 1, 79900, 2000], ['tap', 0, 82900], ['tap', 3, 84600], ['swipe', 2, 86300, 0, 'up'], ['swipe', 3, 86300, 0, 'up'], ['tap', 1, 88000], ['tap', 2, 89700], ['hold', 0, 91400, 1800], ['tap', 3, 94200], ['tap', 1, 95900], ['swipe', 2, 97600, 0, 'down'], ['swipe', 1, 97600, 0, 'down'], ['tap', 0, 99300], ['tap', 3, 101000], ['hold', 2, 102700, 2000], ['tap', 1, 105700], ['tap', 0, 107400], ['swipe', 3, 109100, 0, 'right'], ['swipe', 2, 109100, 0, 'right'], ['tap', 1, 110800], ['tap', 2, 112500], ['hold', 0, 114200, 1800], ['tap', 3, 117000], ['tap', 1, 118700], ['swipe', 2, 120400, 0, 'left'], ['swipe', 0, 120400, 0, 'left'], ['tap', 3, 122100], ['tap', 2, 123800], ['hold', 1, 125500, 2000], ['tap', 0, 128500], ['tap', 3, 130200], ['swipe', 2, 131900, 0, 'up'], ['swipe', 3, 131900, 0, 'up'], ['tap', 1, 133600], ['tap', 2, 135300], ['hold', 0, 137000, 1800], ['tap', 3, 139800], ['tap', 1, 141500], ['swipe', 2, 143200, 0, 'down'], ['swipe', 1, 143200, 0, 'down'], ['tap', 0, 144900], ['tap', 3, 146600], ['hold', 2, 148300, 2000], ['tap', 1, 151300], ['tap', 0, 153000], ['swipe', 3, 154700, 0, 'right'], ['swipe', 2, 154700, 0, 'right'], ['tap', 1, 156400], ['tap', 2, 158100], ['hold', 0, 159800, 1800], ['tap', 3, 162600], ['tap', 1, 164300], ['swipe', 2, 166000, 0, 'left'], ['swipe', 0, 166000, 0, 'left'], ['tap', 3, 167700], ['tap', 2, 169400], ['hold', 1, 171100, 2000], ['tap', 0, 174100], ['tap', 3, 175800], ['swipe', 2, 177500, 0, 'up'], ['swipe', 3, 177500, 0, 'up'], ['tap', 1, 179200], ['tap', 2, 180900], ['hold', 0, 182600, 1800], ['tap', 3, 185400], ['tap', 1, 187100], ['swipe', 2, 188800, 0, 'down'], ['swipe', 1, 188800, 0, 'down'], ['tap', 0, 190500], ['tap', 3, 192200], ['hold', 2, 193900, 2000], ['tap', 1, 196900], ['tap', 2, 198400], ['hold', 0, 210000, 1000]], Hard: [ // Sunrise Waltz Hard - full song, continuous tempo flow, player-friendly density // 90 BPM = 667ms/beat, 16th = 167ms, 8th = 333ms, 3/4 // Intro (continuous 8ths, gentle) ['tap', 1, 2000], ['tap', 2, 2333], ['tap', 3, 2666], ['tap', 0, 2999], ['tap', 1, 3332], ['tap', 2, 3665], ['tap', 3, 3998], ['tap', 0, 4331], ['tap', 1, 4664], ['tap', 2, 4997], ['tap', 3, 5330], ['tap', 0, 5663], // Waltz theme (flowing 8ths, some holds) ['hold', 1, 5996, 800], ['tap', 2, 6796], ['tap', 3, 7129], ['tap', 0, 7462], ['tap', 1, 7795], ['tap', 2, 8128], ['tap', 3, 8461], ['tap', 0, 8794], // Development (add swipes, cross-lane) ['swipe', 2, 9127, 0, 'right'], ['tap', 1, 9460], ['tap', 3, 9793], ['swipe', 0, 10126, 0, 'left'], ['tap', 2, 10459], ['tap', 1, 10792], ['tap', 3, 11125], ['tap', 0, 11458], // Waltz bridge (16ths, spaced, some holds) ['tap', 1, 11791], ['tap', 2, 11958], ['tap', 3, 12125], ['tap', 0, 12292], ['hold', 2, 12459, 900], ['tap', 1, 13359], ['tap', 3, 14026], ['tap', 0, 14693], // Second half (flow, more swipes) ['swipe', 1, 15360, 0, 'up'], ['tap', 2, 15927], ['tap', 3, 16494], ['swipe', 0, 17061, 0, 'down'], ['tap', 1, 17628], ['tap', 2, 18195], ['tap', 3, 18762], ['tap', 0, 19329], // Final section (continuous, some cross-lane) ['hold', 1, 19896, 800], ['tap', 2, 20663], ['tap', 3, 21330], ['tap', 0, 21997], ['swipe', 2, 22664, 0, 'right'], ['tap', 1, 23331], ['tap', 3, 23998], ['swipe', 0, 24665, 0, 'left'], // Outro (spaced, gentle) ['tap', 2, 25332], ['tap', 1, 25665], ['tap', 3, 25998], ['tap', 0, 26331], ['tap', 1, 26664], ['tap', 2, 26997], ['tap', 3, 27330], ['tap', 0, 27663], ['tap', 1, 27996], ['tap', 2, 28329], ['tap', 3, 28662], ['tap', 0, 28995], ['tap', 1, 29328], ['tap', 2, 29661], ['tap', 3, 29994], ['tap', 0, 30327], ['tap', 1, 30660], ['tap', 2, 30993], ['tap', 3, 31326], ['tap', 0, 31659], ['tap', 1, 31992], ['tap', 2, 32325], ['tap', 3, 32658], ['tap', 0, 32991], ['tap', 1, 33324], ['tap', 2, 33657], ['tap', 3, 33990], ['tap', 0, 34323], ['tap', 1, 34656], ['tap', 2, 34989], ['tap', 3, 35322], ['tap', 0, 35655], ['tap', 1, 35988], ['tap', 2, 36321], ['tap', 3, 36654], ['tap', 0, 36987], ['tap', 1, 37320], ['tap', 2, 37653], ['tap', 3, 37986], ['tap', 0, 38319], ['tap', 1, 38652], ['tap', 2, 38985], ['tap', 3, 39318], ['tap', 0, 39651], ['tap', 1, 39984], ['tap', 2, 40317], ['tap', 3, 40650], ['tap', 0, 40983], ['tap', 1, 41316], ['tap', 2, 41649], ['tap', 3, 41982], ['tap', 0, 42315], ['tap', 1, 42648], ['tap', 2, 42981], ['tap', 3, 43314], ['tap', 0, 43647], ['tap', 1, 43980], ['tap', 2, 44313], ['tap', 3, 44646], ['tap', 0, 44979], ['tap', 1, 45312], ['tap', 2, 45645], ['tap', 3, 45978], ['tap', 0, 46311], ['tap', 1, 46644], ['tap', 2, 46977], ['tap', 3, 47310], ['tap', 0, 47643], ['tap', 1, 47976], ['tap', 2, 48309], ['tap', 3, 48642], ['tap', 0, 48975], ['tap', 1, 49308], ['tap', 2, 49641], ['tap', 3, 49974], ['tap', 0, 50307], ['tap', 1, 50640], ['tap', 2, 50973], ['tap', 3, 51306], ['tap', 0, 51639], ['tap', 1, 51972], ['tap', 2, 52305], ['tap', 3, 52638], ['tap', 0, 52971], ['tap', 1, 53304], ['tap', 2, 53637], ['tap', 3, 53970], ['tap', 0, 54303], ['tap', 1, 54636], ['tap', 2, 54969], ['tap', 3, 55302], ['tap', 0, 55635], ['tap', 1, 55968], ['tap', 2, 56301], ['tap', 3, 56634], ['tap', 0, 56967], ['tap', 1, 57300], ['tap', 2, 57633], ['tap', 3, 57966], ['tap', 0, 58299], ['tap', 1, 58632], ['tap', 2, 58965], ['tap', 3, 59298], ['tap', 0, 59631], ['tap', 1, 59964], ['tap', 2, 60297], ['tap', 3, 60630], ['tap', 0, 60963], ['tap', 1, 61296], ['tap', 2, 61629], ['tap', 3, 61962], ['tap', 0, 62295], ['tap', 1, 62628], ['tap', 2, 62961], ['tap', 3, 63294], ['tap', 0, 63627], ['tap', 1, 63960], ['tap', 2, 64293], ['tap', 3, 64626], ['tap', 0, 64959], ['tap', 1, 65292], ['tap', 2, 65625], ['tap', 3, 65958], ['tap', 0, 66291], ['tap', 1, 66624], ['tap', 2, 66957], ['tap', 3, 67290], ['tap', 0, 67623], ['tap', 1, 67956], ['tap', 2, 68289], ['tap', 3, 68622], ['tap', 0, 68955], ['tap', 1, 69288], ['tap', 2, 69621], ['tap', 3, 69954], ['tap', 0, 70287], ['tap', 1, 70620], ['tap', 2, 70953], ['tap', 3, 71286], ['tap', 0, 71619], ['tap', 1, 71952], ['tap', 2, 72285], ['tap', 3, 72618], ['tap', 0, 72951], ['tap', 1, 73284], ['tap', 2, 73617], ['tap', 3, 73950], ['tap', 0, 74283], ['tap', 1, 74616], ['tap', 2, 74949], ['tap', 3, 75282], ['tap', 0, 75615], ['tap', 1, 75948], ['tap', 2, 76281], ['tap', 3, 76614], ['tap', 0, 76947], ['tap', 1, 77280], ['tap', 2, 77613], ['tap', 3, 77946], ['tap', 0, 78279], ['tap', 1, 78612], ['tap', 2, 78945], ['tap', 3, 79278], ['tap', 0, 79611], ['tap', 1, 79944], ['tap', 2, 80277], ['tap', 3, 80610], ['tap', 0, 80943], ['tap', 1, 81276], ['tap', 2, 81609], ['tap', 3, 81942], ['tap', 0, 82275], ['tap', 1, 82608], ['tap', 2, 82941], ['tap', 3, 83274], ['tap', 0, 83607], ['tap', 1, 83940], ['tap', 2, 84273], ['tap', 3, 84606], ['tap', 0, 84939], ['tap', 1, 85272], ['tap', 2, 85605], ['tap', 3, 85938], ['tap', 0, 86271], ['tap', 1, 86604], ['tap', 2, 86937], ['tap', 3, 87270], ['tap', 0, 87603], ['tap', 1, 87936], ['tap', 2, 88269], ['tap', 3, 88602], ['tap', 0, 88935], ['tap', 1, 89268], ['tap', 2, 89601], ['tap', 3, 89934], ['tap', 0, 90267], ['tap', 1, 90600], ['tap', 2, 90933], ['tap', 3, 91266], ['tap', 0, 91599], ['tap', 1, 91932], ['tap', 2, 92265], ['tap', 3, 92598], ['tap', 0, 92931], ['tap', 1, 93264], ['tap', 2, 93597], ['tap', 3, 93930], ['tap', 0, 94263], ['tap', 1, 94596], ['tap', 2, 94929], ['tap', 3, 95262], ['tap', 0, 95595], ['tap', 1, 95928], ['tap', 2, 96261], ['tap', 3, 96594], ['tap', 0, 96927], ['tap', 1, 97260], ['tap', 2, 97593], ['tap', 3, 97926], ['tap', 0, 98259], ['tap', 1, 98592], ['tap', 2, 98925], ['tap', 3, 99258], ['tap', 0, 99591], ['tap', 1, 99924], ['tap', 2, 100257], ['tap', 3, 100590], ['tap', 0, 100923], ['tap', 1, 101256], ['tap', 2, 101589], ['tap', 3, 101922], ['tap', 0, 102255], ['tap', 1, 102588], ['tap', 2, 102921], ['tap', 3, 103254], ['tap', 0, 103587], ['tap', 1, 103920], ['tap', 2, 104253], ['tap', 3, 104586], ['tap', 0, 104919], ['tap', 1, 105252], ['tap', 2, 105585], ['tap', 3, 105918], ['tap', 0, 106251], ['tap', 1, 106584], ['tap', 2, 106917], ['tap', 3, 107250], ['tap', 0, 107583], ['tap', 1, 107916], ['tap', 2, 108249], ['tap', 3, 108582], ['tap', 0, 108915], ['tap', 1, 109248], ['tap', 2, 109581], ['tap', 3, 109914], ['tap', 0, 110247], ['tap', 1, 110580], ['tap', 2, 110913], ['tap', 3, 111246], ['tap', 0, 111579], ['tap', 1, 111912], ['tap', 2, 112245], ['tap', 3, 112578], ['tap', 0, 112911], ['tap', 1, 113244], ['tap', 2, 113577], ['tap', 3, 113910], ['tap', 0, 114243], ['tap', 1, 114576], ['tap', 2, 114909], ['tap', 3, 115242], ['tap', 0, 115575], ['tap', 1, 115908], ['tap', 2, 116241], ['tap', 3, 116574], ['tap', 0, 116907], ['tap', 1, 117240], ['tap', 2, 117573], ['tap', 3, 117906], ['tap', 0, 118239], ['tap', 1, 118572], ['tap', 2, 118905], ['tap', 3, 119238], ['tap', 0, 119571], ['tap', 1, 119904], ['tap', 2, 120237], ['tap', 3, 120570], ['tap', 0, 120903], ['tap', 1, 121236], ['tap', 2, 121569], ['tap', 3, 121902], ['tap', 0, 122235], ['tap', 1, 122568], ['tap', 2, 122901], ['tap', 3, 123234], ['tap', 0, 123567], ['tap', 1, 123900], ['tap', 2, 124233], ['tap', 3, 124566], ['tap', 0, 124899], ['tap', 1, 125232], ['tap', 2, 125565], ['tap', 3, 125898], ['tap', 0, 126231], ['tap', 1, 126564], ['tap', 2, 126897], ['tap', 3, 127230], ['tap', 0, 127563], ['tap', 1, 127896], ['tap', 2, 128229], ['tap', 3, 128562], ['tap', 0, 128895], ['tap', 1, 129228], ['tap', 2, 129561], ['tap', 3, 129894], ['tap', 0, 130227], ['tap', 1, 130560], ['tap', 2, 130893], ['tap', 3, 131226], ['tap', 0, 131559], ['tap', 1, 131892], ['tap', 2, 132225], ['tap', 3, 132558], ['tap', 0, 132891], ['tap', 1, 133224], ['tap', 2, 133557], ['tap', 3, 133890], ['tap', 0, 134223], ['tap', 1, 134556], ['tap', 2, 134889], ['tap', 3, 135222], ['tap', 0, 135555], ['tap', 1, 135888], ['tap', 2, 136221], ['tap', 3, 136554], ['tap', 0, 136887], ['tap', 1, 137220], ['tap', 2, 137553], ['tap', 3, 137886], ['tap', 0, 138219], ['tap', 1, 138552], ['tap', 2, 138885], ['tap', 3, 139218], ['tap', 0, 139551], ['tap', 1, 139884], ['tap', 2, 140217], ['tap', 3, 140550], ['tap', 0, 140883], ['tap', 1, 141216], ['tap', 2, 141549], ['tap', 3, 141882], ['tap', 0, 142215], ['tap', 1, 142548], ['tap', 2, 142881], ['tap', 3, 143214], ['tap', 0, 143547], ['tap', 1, 143880], ['tap', 2, 144213], ['tap', 3, 144546], ['tap', 0, 144879], ['tap', 1, 145212], ['tap', 2, 145545], ['tap', 3, 145878], ['tap', 0, 146211], ['tap', 1, 146544], ['tap', 2, 146877], ['tap', 3, 147210], ['tap', 0, 147543], ['tap', 1, 147876], ['tap', 2, 148209], ['tap', 3, 148542], ['tap', 0, 148875], ['tap', 1, 149208], ['tap', 2, 149541], ['tap', 3, 149874], ['tap', 0, 150207], ['tap', 1, 150540], ['tap', 2, 150873], ['tap', 3, 151206], ['tap', 0, 151539], ['tap', 1, 151872], ['tap', 2, 152205], ['tap', 3, 152538], ['tap', 0, 152871], ['tap', 1, 153204], ['tap', 2, 153537], ['tap', 3, 153870], ['tap', 0, 154203], ['tap', 1, 154536], ['tap', 2, 154869], ['tap', 3, 155202], ['tap', 0, 155535], ['tap', 1, 155868], ['tap', 2, 156201], ['tap', 3, 156534], ['tap', 0, 156867], ['tap', 1, 157200], ['tap', 2, 157533], ['tap', 3, 157866], ['tap', 0, 158199], ['tap', 1, 158532], ['tap', 2, 158865], ['tap', 3, 159198], ['tap', 0, 159531], ['tap', 1, 159864], ['tap', 2, 160197], ['tap', 3, 160530], ['tap', 0, 160863], ['tap', 1, 161196], ['tap', 2, 161529], ['tap', 3, 161862], ['tap', 0, 162195], ['tap', 1, 162528], ['tap', 2, 162861], ['tap', 3, 163194], ['tap', 0, 163527], ['tap', 1, 163860], ['tap', 2, 164193], ['tap', 3, 164526], ['tap', 0, 164859], ['tap', 1, 165192], ['tap', 2, 165525], ['tap', 3, 165858], ['tap', 0, 166191], ['tap', 1, 166524], ['tap', 2, 166857], ['tap', 3, 167190], ['tap', 0, 167523], ['tap', 1, 167856], ['tap', 2, 168189], ['tap', 3, 168522], ['tap', 0, 168855], ['tap', 1, 169188], ['tap', 2, 169521], ['tap', 3, 169854], ['tap', 0, 170187], ['tap', 1, 170520], ['tap', 2, 170853], ['tap', 3, 171186], ['tap', 0, 171519], ['tap', 1, 171852], ['tap', 2, 172185], ['tap', 3, 172518], ['tap', 0, 172851], ['tap', 1, 173184], ['tap', 2, 173517], ['tap', 3, 173850], ['tap', 0, 174183], ['tap', 1, 174516], ['tap', 2, 174849], ['tap', 3, 175182], ['tap', 0, 175515], ['tap', 1, 175848], ['tap', 2, 176181], ['tap', 3, 176514], ['tap', 0, 176847], ['tap', 1, 177180], ['tap', 2, 177513], ['tap', 3, 177846], ['tap', 0, 178179], ['tap', 1, 178512], ['tap', 2, 178845], ['tap', 3, 179178], ['tap', 0, 179511], ['tap', 1, 179844], ['tap', 2, 180177], ['tap', 3, 180510], ['tap', 0, 180843], ['tap', 1, 181176], ['tap', 2, 181509], ['tap', 3, 181842], ['tap', 0, 182175], ['tap', 1, 182508], ['tap', 2, 182841], ['tap', 3, 183174], ['tap', 0, 183507], ['tap', 1, 183840], ['tap', 2, 184173], ['tap', 3, 184506], ['tap', 0, 184839], ['tap', 1, 185172], ['tap', 2, 185505], ['tap', 3, 185838], ['tap', 0, 186171], ['tap', 1, 186504], ['tap', 2, 186837], ['tap', 3, 187170], ['tap', 0, 187503], ['tap', 1, 187836], ['tap', 2, 188169], ['tap', 3, 188502], ['tap', 0, 188835], ['tap', 1, 189168], ['tap', 2, 189501], ['tap', 3, 189834], ['tap', 0, 190167], ['tap', 1, 190500], ['tap', 2, 190833], ['tap', 3, 191166], ['tap', 0, 191499], ['tap', 1, 191832], ['tap', 2, 192165], ['tap', 3, 192498], ['tap', 0, 192831], ['tap', 1, 193164], ['tap', 2, 193497], ['tap', 3, 193830], ['tap', 0, 194163], ['tap', 1, 194496], ['tap', 2, 194829], ['tap', 3, 195000]] } }]; // --- GUI ELEMENTS --- var gui = LK.gui; var languageButtons = []; var startButton = null; var songCards = []; var storyText = null; var continueButton = null; var difficultyButtons = []; var scoreText = null; var comboText = null; var accuracyText = null; var resultText = null; var homeButton = null; // --- GAME ELEMENTS --- var lanes = []; var laneDividers = []; var hitLine = null; // --- UTILS --- function clearGUI() { // Remove all children from gui overlays, but only if they exist if (gui.top && gui.top.removeChildren) { gui.top.removeChildren(); } if (gui.topRight && gui.topRight.removeChildren) { gui.topRight.removeChildren(); } if (gui.topLeft && gui.topLeft.removeChildren) { gui.topLeft.removeChildren(); } if (gui.left && gui.left.removeChildren) { gui.left.removeChildren(); } if (gui.right && gui.right.removeChildren) { gui.right.removeChildren(); } if (gui.bottom && gui.bottom.removeChildren) { gui.bottom.removeChildren(); } if (gui.bottomLeft && gui.bottomLeft.removeChildren) { gui.bottomLeft.removeChildren(); } if (gui.bottomRight && gui.bottomRight.removeChildren) { gui.bottomRight.removeChildren(); } if (gui.center && gui.center.removeChildren) { gui.center.removeChildren(); } // Reset GUI-related global state languageButtons = []; startButton = null; songCards = []; storyText = null; continueButton = null; difficultyButtons = []; scoreText = null; comboText = null; accuracyText = null; resultText = null; homeButton = null; } function clearGameObjects() { // Remove all notes, lanes, etc. for (var i = 0; i < lanes.length; ++i) { if (lanes[i] && lanes[i].parent) { lanes[i].parent.removeChild(lanes[i]); } } for (var i = 0; i < laneDividers.length; ++i) { if (laneDividers[i] && laneDividers[i].parent) { laneDividers[i].parent.removeChild(laneDividers[i]); } } if (hitLine && hitLine.parent) { hitLine.parent.removeChild(hitLine); } for (var i = 0; i < currentNotes.length; ++i) { if (currentNotes[i] && currentNotes[i].parent) { currentNotes[i].parent.removeChild(currentNotes[i]); } } lanes = []; laneDividers = []; hitLine = null; currentNotes = []; noteIndex = 0; holdActive = null; swipeStart = null; swipeNote = null; isPlaying = false; } function setGameState(state) { GAME_STATE = state; // Always remove gameplay backgrounds from the game scene (prevents background leak to other screens) removeGameplayBackgrounds(); // Stop any playing music when changing states (except when going to PLAY) if (state !== 'PLAY') { LK.stopMusic(); } clearGUI(); clearGameObjects(); // Reset global state for each screen dragLane = null; dragY = null; dragNote = null; dragStartX = null; dragStartY = null; dragStartTime = null; if (state === 'LANGUAGE') { showLanguageSelector(); } else if (state === 'HOME') { selectedSong = null; selectedDifficulty = null; showHome(); } else if (state === 'STORY') { showStory(); } else if (state === 'DIFFICULTY') { showDifficulty(); } else if (state === 'PLAY') { startGameplay(); } else if (state === 'RESULT') { showResult(); } } // --- LANGUAGE SELECTOR --- function showLanguageSelector() { languageButtons = []; // Get current screen size from LK var screenWidth = LK.width || 2048; var screenHeight = LK.height || 2732; // Create opening screen container (with background) var openingScreen = new Container(); // Add background asset, centered var bgAsset = LK.getAsset('Openingscreen', { anchorX: 0.5, anchorY: 0.5, x: 0, y: 0 }); openingScreen.addChild(bgAsset); // Add three SelectorUI assets as backgrounds for English, 中文, and play button // Calculate button positions and sizes to fit text var langBtnYStart = Math.floor(screenHeight * 0.45) - screenHeight / 2; var langBtnSpacing = Math.floor(screenHeight * 0.08); // English SelectorUI var selectorBgEn = LK.getAsset('SelectorUI', { anchorX: 0.5, anchorY: 0.5, x: 0, y: langBtnYStart, scaleX: 3.2, scaleY: 1.3 }); openingScreen.addChild(selectorBgEn); // 中文 SelectorUI var selectorBgZh = LK.getAsset('SelectorUI', { anchorX: 0.5, anchorY: 0.5, x: 0, y: langBtnYStart + langBtnSpacing, scaleX: 2.2, scaleY: 1.3 }); openingScreen.addChild(selectorBgZh); // Play button SelectorUI var playBtnY = Math.floor(screenHeight * 0.70) - screenHeight / 2; var selectorBgPlay = LK.getAsset('SelectorUI', { anchorX: 0.5, anchorY: 0.5, x: 0, y: playBtnY, scaleX: 3.2, scaleY: 1.5 }); openingScreen.addChild(selectorBgPlay); // Center all elements inside the openingScreen container // Title var titleText = new Text2("Kaleidoscope of Music Rhythm", { size: 80, fill: "#fff" }); titleText.anchor.set(0.5, 0.5); // Place title at 23% of screen height, centered horizontally, and ensure it's not above y=80 titleText.x = 0; titleText.y = Math.max(80 - screenHeight / 2, Math.floor(screenHeight * 0.23) - screenHeight / 2); openingScreen.addChild(titleText); // Language selector var langs = [{ code: 'en', label: 'English' }, { code: 'zh', label: '中文' }]; // Place language buttons at 45% and 53% of screen height, centered horizontally var langBtnYStart = Math.floor(screenHeight * 0.45) - screenHeight / 2; var langBtnSpacing = Math.floor(screenHeight * 0.08); for (var i = 0; i < langs.length; ++i) { (function (idx) { var btn = new Text2(langs[idx].label, { size: 64, fill: "#fff" }); btn.anchor.set(0.5, 0.5); btn.x = 0; // centered in container btn.y = langBtnYStart + idx * langBtnSpacing; btn.interactive = true; btn.down = function (x, y, obj) { LK.getSound('Touch').play(); selectedLanguage = langs[idx].code; storage.language = selectedLanguage; // Do not advance to HOME yet, just set language clearGUI(); showLanguageSelector(); }; openingScreen.addChild(btn); languageButtons.push(btn); })(i); } // Start button startButton = new Text2(selectedLanguage === 'zh' ? "开始" : "Start", { size: 90, fill: "#fff" }); startButton.anchor.set(0.5, 0.5); // Place start button at 70% of screen height, centered horizontally startButton.x = 0; startButton.y = Math.floor(screenHeight * 0.70) - screenHeight / 2; startButton.interactive = true; startButton.down = function (x, y, obj) { LK.getSound('Touch').play(); setGameState('HOME'); }; openingScreen.addChild(startButton); // Add the opening screen container to gui.center gui.center.addChild(openingScreen); // Play background music for opening screen LK.playMusic('KaleidoscopeMusicRhythm'); } // --- HOME / SONG SELECTION --- function showHome() { removeGameplayBackgrounds(); clearGUI(); clearGameObjects(); songCards = []; var screenWidth = LK.width || 2048; var screenHeight = LK.height || 2732; // Create homeScreen container before adding children var homeScreen = new Container(); // Add Backgroundhomepage asset as background, fully fit to screen var bgAsset = LK.getAsset('Backgroundhomepage', { anchorX: 0.5, anchorY: 0.5, x: 0, y: 0, scaleX: screenWidth / 2048, scaleY: screenHeight / 2732 }); homeScreen.addChild(bgAsset); bgAsset.width = screenWidth; bgAsset.height = screenHeight; // Title: "Select a Song", in white with proper positioning var title = new Text2(selectedLanguage === 'zh' ? "选择歌曲" : "Select a Song", { size: 90, fill: "#fff", // White text for contrast font: "Arial Black" }); title.anchor.set(0.5, 0); // Place title at 15% of screen height, centered horizontally, and ensure it's not above y=120 // For gui.center containers, (0,0) is the center, so offset from center title.x = 0; title.y = -screenHeight / 2 + Math.max(120, Math.floor(screenHeight * 0.15)); homeScreen.addChild(title); // Card layout - vertical stacking with proper spacing var cardCount = SONGS.length; var cardHeight = 200; var cardSpacing = 80; var titleSpacing = 100; // Place the title at 15% of screen height, and cards start at 25% of screen height // We want all cards to be below the title and fully within SelectorUI // Calculate the starting Y so that the first card is just below the title, and all cards fit on screen var cardsStartY = title.y + title.height + titleSpacing; // 100px gap below title for (var i = 0; i < SONGS.length; i++) { (function (index) { var song = SONGS[index]; // Card Y position var cardY = cardsStartY + index * (cardHeight + cardSpacing); // Create SelectorUI background for song card var cardBg = LK.getAsset('SelectorUI', { anchorX: 0.5, anchorY: 0.5, x: 0, y: cardY, scaleX: 4.0, scaleY: 2.0 }); homeScreen.addChild(cardBg); // Make card interactive cardBg.interactive = true; cardBg.down = function (x, y, obj) { LK.getSound('Touch').play(); selectedSong = song; setGameState('STORY'); }; // Add song title (centered within SelectorUI) var songNameText = new Text2(song.title, { size: 64, fill: "#fff", font: "Arial" }); songNameText.anchor.set(0.5, 0.5); songNameText.x = 0; songNameText.y = cardY - 30; homeScreen.addChild(songNameText); // Add subtitle (optional: show "Sky Journey", "Night Pulse", "Sunrise Waltz") var subtitleText = new Text2(selectedLanguage === 'zh' && song.title === "Sky Journey" ? "天空之旅" : selectedLanguage === 'zh' && song.title === "Night Pulse" ? "夜之律动" : selectedLanguage === 'zh' && song.title === "Sunrise Waltz" ? "日出圆舞曲" : "", { size: 40, fill: 0xE0E0E0, font: "Arial" }); subtitleText.anchor.set(0.5, 0.5); subtitleText.x = 0; subtitleText.y = cardY + 30; homeScreen.addChild(subtitleText); songCards.push(cardBg); })(i); } // Add homeScreen to GUI, centered homeScreen.x = 0; homeScreen.y = 0; if (gui.center && gui.center.addChild) { gui.center.addChild(homeScreen); } // Play background music for homepage LK.playMusic('KaleidoscopeMusicRhythm'); } // --- STORY INTRO --- function showStory() { clearGUI(); var story = selectedSong.story[selectedLanguage] || selectedSong.story['en']; var screenWidth = LK.width || 2048; var screenHeight = LK.height || 2732; // Create SongStorybox container var storyBox = new Container(); // Add SongStorybox asset as background, centered and scaled up for showcase var boxAsset = LK.getAsset('SongStorybox', { anchorX: 0.5, anchorY: 0.5, x: 0, y: 0, scaleX: 13, // Increased for larger showcase scaleY: 11 }); storyBox.addChild(boxAsset); // Calculate box bounds for layout var boxW = boxAsset.width * boxAsset.scaleX; var boxH = boxAsset.height * boxAsset.scaleY; var boxLeftX = -boxW / 2; var boxRightX = boxW / 2; var boxTopY = -boxH / 2; var boxBottomY = boxH / 2; // Add smaller song cover on the left side of the box var coverAsset = LK.getAsset(selectedSong.cover, { anchorX: 0.5, anchorY: 0.5, x: boxLeftX + boxW * 0.25, // Position at 25% from left edge y: boxTopY + boxH * 0.35, // Position at 35% from top scaleX: 1.2, // Smaller cover scaleY: 1.2 }); storyBox.addChild(coverAsset); // Add story text on the larger right side of the box storyText = new Text2(story, { size: 55, fill: "#fff", wordWrap: true, wordWrapWidth: boxW * 0.45 // Use 45% of box width for text }); storyText.anchor.set(0, 0.5); storyText.x = boxLeftX + boxW * 0.52; // Start at 52% from left edge storyText.y = boxTopY + boxH * 0.35; // Align vertically with cover storyBox.addChild(storyText); // Create 3 separate SelectorUI elements for difficulty buttons arranged horizontally var diffY = boxBottomY - boxH * 0.15; // Y position for all difficulty buttons var totalWidth = boxW * 0.8; // Use 80% of box width for all buttons var buttonWidth = totalWidth / selectedSong.difficulties.length; var startX = -totalWidth / 2 + buttonWidth / 2; // Center the button group difficultyButtons = []; for (var i = 0; i < selectedSong.difficulties.length; ++i) { (function (idx) { var label = selectedSong.difficulties[idx]; // Create individual SelectorUI for this difficulty button var selectorAsset = LK.getAsset('SelectorUI', { anchorX: 0.5, anchorY: 0.5, x: startX + idx * buttonWidth, y: diffY, scaleX: 2.5, scaleY: 1.8 }); selectorAsset.interactive = true; selectorAsset.down = function (x, y, obj) { LK.getSound('Touch').play(); selectedDifficulty = label; setGameState('PLAY'); }; storyBox.addChild(selectorAsset); // Add difficulty button text centered on the SelectorUI var btn = new Text2(label, { size: 60, fill: "#fff" }); btn.anchor.set(0.5, 0.5); btn.x = startX + idx * buttonWidth; btn.y = diffY; storyBox.addChild(btn); difficultyButtons.push(btn); })(i); } // Start storyBox at bottom-right corner of screen storyBox.x = screenWidth; storyBox.y = screenHeight; gui.center.addChild(storyBox); // Animate storyBox to center of screen tween(storyBox, { x: 0, y: 0 }, { duration: 800, easing: tween.easeOut }); } // --- DIFFICULTY SELECT --- function showDifficulty() { difficultyButtons = []; var screenWidth = LK.width || 2048; var screenHeight = LK.height || 2732; var title = new Text2(selectedLanguage === 'zh' ? "选择难度" : "Select Difficulty", { size: 100, fill: "#fff" }); title.anchor.set(0.5, 0); title.x = screenWidth / 2; title.y = Math.max(100, Math.floor(screenHeight * 0.08)); gui.top.addChild(title); var btnYStart = Math.floor(screenHeight * 0.40); var btnSpacing = Math.floor(screenHeight * 0.13); for (var i = 0; i < selectedSong.difficulties.length; ++i) { (function (idx) { var label = selectedSong.difficulties[idx]; var btn = new Text2(label, { size: 90, fill: "#fff" }); btn.anchor.set(0.5, 0.5); btn.x = screenWidth / 2; btn.y = btnYStart + idx * btnSpacing; btn.interactive = true; btn.down = function (x, y, obj) { LK.getSound('Touch').play(); selectedDifficulty = label; setGameState('PLAY'); }; gui.center.addChild(btn); difficultyButtons.push(btn); })(i); } } // --- GAMEPLAY --- function startGameplay() { removeGameplayBackgrounds(); // Insert BgSkyJourney as background for Sky Journey gameplay if (selectedSong && selectedSong.id === 'SkyJourney') { // Add background image, scaled to fit the game area, behind all gameplay elements var bgSkyJourney = LK.getAsset('BgSkyJourney', { anchorX: 0.5, anchorY: 0, x: 2048 / 2, y: 0, scaleX: 2048 / 2048, scaleY: 2732 / 3640.89 }); // Ensure background covers the full game area bgSkyJourney.width = 2048; bgSkyJourney.height = 2732; game.addChildAt(bgSkyJourney, 0); // Add as the bottom-most child } // Insert BgNightPulse as background for Night Pulse gameplay if (selectedSong && selectedSong.id === 'NightPulse') { // Add background image, scaled to fit the game area, behind all gameplay elements var bgNightPulse = LK.getAsset('BgNightPulse', { anchorX: 0.5, anchorY: 0, x: 2048 / 2, y: 0, scaleX: 2048 / 2048, // Full width scaleY: 2732 / 3640.89 // Full height }); // Ensure background covers the full game area bgNightPulse.width = 2048; bgNightPulse.height = 2732; game.addChildAt(bgNightPulse, 0); // Add as the bottom-most child } // Insert BgSunriseWaltz as background for Sunrise Waltz gameplay if (selectedSong && selectedSong.id === 'SunriseWaltz') { // Add background image, scaled to fit the game area, behind all gameplay elements var bgSunriseWaltz = LK.getAsset('BgSunriseWaltz', { anchorX: 0.5, anchorY: 0, x: 2048 / 2, y: 0, scaleX: 2048 / 2048, scaleY: 2732 / 3640.89 }); // Ensure background covers the full game area bgSunriseWaltz.width = 2048; bgSunriseWaltz.height = 2732; game.addChildAt(bgSunriseWaltz, 0); // Add as the bottom-most child } lanes = []; laneDividers = []; var laneSpacing = 2048 / LANE_COUNT; // Equal spacing for 4 lanes for (var i = 0; i < LANE_COUNT; ++i) { var lane = LK.getAsset('lane', { anchorX: 0.5, anchorY: 0 }); lane.x = (i + 0.5) * laneSpacing; // Center each lane in its section lane.y = 0; lane.scaleX = laneSpacing / lane.width; // Scale to fit lane width lane.scaleY = 2732 / lane.height; // Scale to full screen height lanes.push(lane); game.addChild(lane); } // Create lane dividers as hit areas at the bottom of lanes for (var i = 0; i < LANE_COUNT; ++i) { var divider = LK.getAsset('laneDivider', { anchorX: 0.5, anchorY: 1 }); divider.x = (i + 0.5) * laneSpacing; // Center in each lane divider.y = HIT_LINE_Y + 100; // Position at bottom as hit area divider.scaleX = laneSpacing * 0.8 / divider.width; // Scale to 80% of lane width divider.scaleY = 0.3; // Make it thinner for hit area laneDividers.push(divider); game.addChild(divider); } // Hit line (visual indicator) hitLine = LK.getAsset('laneDivider', { anchorX: 0, anchorY: 0.5 }); hitLine.width = 2048; hitLine.height = 8; hitLine.x = 0; hitLine.y = HIT_LINE_Y; hitLine.alpha = 0.7; // Make it semi-transparent game.addChild(hitLine); // Load notes currentSongData = selectedSong.notes[selectedDifficulty]; currentNotes = []; noteIndex = 0; score = 0; combo = 0; maxCombo = 0; accuracy = 0; totalNotes = currentSongData.length; hitNotes = 0; missNotes = 0; badNotes = 0; goodNotes = 0; greatNotes = 0; perfectNotes = 0; scorePopups = []; holdActive = null; swipeStart = null; swipeNote = null; isPlaying = true; for (var i = 0; i < currentSongData.length; ++i) { var n = currentSongData[i]; var note = new Note(); note.init(n[0], n[1], n[2], n[3], n[4]); // Initial position: y far above screen, will be updated in update loop var laneSpacing = 2048 / LANE_COUNT; note.x = (n[1] + 0.5) * laneSpacing; // Center in the lane note.y = -200; currentNotes.push(note); game.addChild(note); } // Create scoreboard container with SelectorUI background var scoreboardBg = LK.getAsset('SelectorUI', { anchorX: 0.5, anchorY: 0, x: 0, y: 0, scaleX: 8.0, scaleY: 2.0 }); gui.top.addChild(scoreboardBg); // Score/Combo/Accuracy GUI positioned on scoreboard scoreText = new Text2("Score: 0", { size: 70, fill: "#fff" }); scoreText.anchor.set(0.5, 0); scoreText.x = 0; scoreText.y = 20; gui.top.addChild(scoreText); comboText = new Text2("Combo: 0", { size: 70, fill: "#fff" }); comboText.anchor.set(0.5, 0); comboText.x = 0; comboText.y = 70; gui.top.addChild(comboText); accuracyText = new Text2("Accuracy: 100%", { size: 70, fill: "#fff" }); accuracyText.anchor.set(0.5, 0); accuracyText.x = 0; accuracyText.y = 120; gui.top.addChild(accuracyText); // Start music - play full song for each gameplay without looping LK.playMusic(selectedSong.id, { loop: false }); // Start timer startTime = Date.now(); } // --- GAMEPLAY LOGIC --- function getCurrentTime() { return Date.now() - startTime; } function getNoteY(noteTime) { // Notes fall from y = -200 to HIT_LINE_Y at the time they should be hit // We'll use a fixed speed so that notes reach HIT_LINE_Y at noteTime var speed = (HIT_LINE_Y + 200) / 2000; // 2 seconds to fall var t = getCurrentTime(); var dt = noteTime - t; return HIT_LINE_Y - dt * speed; } function getHoldTrailLength(note, t) { // For hold notes, trail from note head to end time var endTime = note.time + note.duration; var y1 = getNoteY(note.time); var y2 = getNoteY(endTime); return Math.max(0, y2 - y1); } function judgeNote(note, lane, y, eventType, swipeDir) { if (!note.active || note.hit) { return false; } var noteY = getNoteY(note.time); var t = getCurrentTime(); var dt = Math.abs(note.time - t); var scoreType = ""; var scoreValue = 0; // Determine hit quality based on timing if (dt <= PERFECT_WINDOW) { scoreType = "perfect"; scoreValue = 150; perfectNotes += 1; } else if (dt <= GREAT_WINDOW) { scoreType = "great"; scoreValue = 120; greatNotes += 1; } else if (dt <= GOOD_WINDOW) { scoreType = "good"; scoreValue = 80; goodNotes += 1; } else if (dt <= BAD_WINDOW) { scoreType = "bad"; scoreValue = 40; badNotes += 1; } else { return false; // Miss } if (note.type === 'tap') { if (lane === note.lane && Math.abs(y - HIT_LINE_Y) < 180 && dt < BAD_WINDOW) { note.hit = true; note.active = false; note.flash(); // Create sparkling effect on the note itself tween(note, { scaleX: note.noteAsset.scaleX * 1.3, scaleY: note.noteAsset.scaleY * 1.3, rotation: 0.2 }, { duration: 100, easing: tween.easeOut, onFinish: function onFinish() { tween(note, { scaleX: note.noteAsset.scaleX / 1.3, scaleY: note.noteAsset.scaleY / 1.3, rotation: 0 }, { duration: 100, easing: tween.easeIn }); } }); // Immediately remove from currentNotes array to prevent flow issues var tapIndex = currentNotes.indexOf(note); if (tapIndex > -1) { currentNotes.splice(tapIndex, 1); } // Make note disappear immediately after touch with burst animation tween(note, { alpha: 0, scaleX: 1.5, scaleY: 1.5 }, { duration: 150, easing: tween.easeOut, onFinish: function onFinish() { if (note.parent) { note.parent.removeChild(note); } } }); LK.getSound('tap').play(); score += scoreValue; combo += 1; hitNotes += 1; maxCombo = Math.max(combo, maxCombo); // Light up hit area var hitArea = laneDividers[lane]; if (hitArea) { // Store original tint if not already stored if (hitArea.originalTint === undefined) { hitArea.originalTint = hitArea.tint || 0xFFFFFF; } // Light up with golden glow hitArea.tint = 0xFFD700; hitArea.alpha = 1.0; // Store original scale if not already stored if (hitArea.originalScaleX === undefined) { hitArea.originalScaleX = hitArea.scaleX; hitArea.originalScaleY = hitArea.scaleY; } // Create sparkling effect with scaling animation tween(hitArea, { scaleX: hitArea.originalScaleX * 1.3, scaleY: hitArea.originalScaleY * 1.3 }, { duration: 150, easing: tween.easeOut, onFinish: function onFinish() { tween(hitArea, { scaleX: hitArea.originalScaleX, scaleY: hitArea.originalScaleY }, { duration: 150, easing: tween.easeIn }); } }); // Fade back to normal after short duration tween(hitArea, { alpha: 0.7 }, { duration: 200, onFinish: function onFinish() { hitArea.tint = hitArea.originalTint; } }); } // Show score popup var popup = new ScorePopup(); popup.init(scoreType, note.x, note.y); game.addChild(popup); scorePopups.push(popup); return true; } } else if (note.type === 'hold') { // EXTREMELY player-friendly hold detection for both Hard and Medium: // - Allow full-lane (all lanes) detection // - Allow very wide vertical margin // - Greatly increase timing window for initial press // - Allow initial press anywhere in the lower half of the screen // - Allow initial press up to 500ms before/after the note // This is now applied to both Hard and Medium difficulties var isMediumOrHard = selectedDifficulty === 'Medium' || selectedDifficulty === 'Hard'; var isValidLane = true; // Allow any lane for initial press var verticalLeniency = isMediumOrHard ? 950 : 350; // Very wide for Medium/Hard, default for Easy var timingLeniency = isMediumOrHard ? BAD_WINDOW + 500 : BAD_WINDOW; // More lenient for Medium/Hard if (eventType === 'down' && isValidLane && Math.abs(y - HIT_LINE_Y) < verticalLeniency && dt < timingLeniency) { holdActive = { note: note, start: t, scoreType: scoreType, scoreValue: scoreValue }; note.flash(); // Start continuous sparkling animation on hit area during hold var hitArea = laneDividers[note.lane]; if (hitArea) { // Create pulsing sparkling effect var _createSparkle = function createSparkle() { if (holdActive && holdActive.note === note) { hitArea.tint = 0xFFD700; // Golden sparkle tween(hitArea, { scaleX: hitArea.originalScaleX * 1.2, scaleY: hitArea.originalScaleY * 1.2, alpha: 1.0 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { tween(hitArea, { scaleX: hitArea.originalScaleX, scaleY: hitArea.originalScaleY, alpha: 0.8 }, { duration: 200, easing: tween.easeIn, onFinish: function onFinish() { // Continue sparkling if still holding if (holdActive && holdActive.note === note) { LK.setTimeout(_createSparkle, 100); } else { // Reset hit area when hold ends hitArea.tint = hitArea.originalTint; hitArea.scaleX = hitArea.originalScaleX; hitArea.scaleY = hitArea.originalScaleY; hitArea.alpha = 0.7; } } }); } }); } }; // Store original properties if (hitArea.originalTint === undefined) { hitArea.originalTint = hitArea.tint || 0xFFFFFF; } if (hitArea.originalScaleX === undefined) { hitArea.originalScaleX = hitArea.scaleX; hitArea.originalScaleY = hitArea.scaleY; } _createSparkle(); } LK.getSound('hold').play(); return true; } } else if (note.type === 'swipe') { // Check if swipe affects multiple lanes - enhanced detection for bigger swipe notes var validLanes = [note.lane]; var laneSpacing = 2048 / LANE_COUNT; var swipeWidth = note.noteAsset.width * note.noteAsset.scaleX; // Enhanced cross-lane detection for bigger swipe notes if (swipeWidth > laneSpacing * 1.8) { // More generous threshold // Add adjacent lanes for cross-lane swipes if (note.lane > 0) validLanes.push(note.lane - 1); if (note.lane < LANE_COUNT - 1) validLanes.push(note.lane + 1); // For very wide swipes, add one more lane on each side if (swipeWidth > laneSpacing * 2.5) { if (note.lane > 1) validLanes.push(note.lane - 2); if (note.lane < LANE_COUNT - 2) validLanes.push(note.lane + 2); } } var isValidLane = validLanes.indexOf(lane) !== -1; // Improved sensitivity - increased distance threshold and relaxed timing for better swipe detection if (eventType === 'swipe' && isValidLane && Math.abs(y - HIT_LINE_Y) < 600 && dt < BAD_WINDOW + 300 && swipeDir === note.swipeDir) { note.hit = true; note.active = false; note.flash(); // Make swipe note disappear immediately after touch with directional slide animation var slideDir = note.swipeDir; var targetX = note.x; var targetY = note.y; var slideDistance = 400; // Increased slide distance if (slideDir === 'left') targetX -= slideDistance;else if (slideDir === 'right') targetX += slideDistance;else if (slideDir === 'up') targetY -= slideDistance;else if (slideDir === 'down') targetY += slideDistance; // Store reference to note for proper cleanup and prevent red tinting var noteToRemove = note; // Ensure note is removed from currentNotes array immediately to prevent red tinting and flow issues var index = currentNotes.indexOf(noteToRemove); if (index > -1) { currentNotes.splice(index, 1); } // Prevent any color tinting by ensuring clean removal noteToRemove.tint = 0xFFFFFF; noteToRemove.alpha = 0; // Make invisible immediately // Remove from parent immediately to prevent any visual artifacts if (noteToRemove.parent) { noteToRemove.parent.removeChild(noteToRemove); } // Optional visual effect for feedback var effectNote = LK.getAsset('swipeNote', { anchorX: 0.5, anchorY: 0.5, x: note.x, y: note.y, scaleX: note.noteAsset.scaleX, scaleY: note.noteAsset.scaleY }); game.addChild(effectNote); tween(effectNote, { x: targetX, y: targetY, alpha: 0, scaleX: note.noteAsset.scaleX * 0.3, scaleY: note.noteAsset.scaleY * 0.3 }, { duration: 150, // Faster animation for better responsiveness easing: tween.easeOut, onFinish: function onFinish() { if (effectNote && effectNote.parent) { effectNote.parent.removeChild(effectNote); } } }); LK.getSound('swipe').play(); score += scoreValue + 50; // Bonus for swipe notes combo += 1; hitNotes += 1; maxCombo = Math.max(combo, maxCombo); // Light up hit areas for swipe notes (can affect multiple lanes) for (var v = 0; v < validLanes.length; v++) { var hitArea = laneDividers[validLanes[v]]; if (hitArea) { // Store original tint if not already stored if (hitArea.originalTint === undefined) { hitArea.originalTint = hitArea.tint || 0xFFFFFF; } // Light up with cyan glow for swipe hitArea.tint = 0x00FFFF; hitArea.alpha = 1.0; // Create sparkling wave effect with scaling animation var delay = v * 50; tween(hitArea, { scaleX: hitArea.scaleX * 1.4, scaleY: hitArea.scaleY * 1.4 }, { duration: 200, delay: delay, easing: tween.easeOut, onFinish: function onFinish() { tween(hitArea, { scaleX: hitArea.scaleX / 1.4, scaleY: hitArea.scaleY / 1.4 }, { duration: 200, easing: tween.easeIn }); } }); // Stagger the fade timing for visual wave effect tween(hitArea, { alpha: 0.7 }, { duration: 250, delay: delay, onFinish: function onFinish() { hitArea.tint = hitArea.originalTint; } }); } } // Show score popup var popup = new ScorePopup(); popup.init(scoreType, note.x, note.y); game.addChild(popup); scorePopups.push(popup); return true; } } return false; } function endHold() { if (holdActive && holdActive.note && !holdActive.note.hit) { var t = getCurrentTime(); var note = holdActive.note; var holdEnd = note.time + note.duration; // Only complete hold if duration has been fully held AND player is still holding if (t >= holdEnd) { note.hit = true; note.active = false; // Immediately remove from currentNotes array to prevent flow issues var holdIndex = currentNotes.indexOf(note); if (holdIndex > -1) { currentNotes.splice(holdIndex, 1); } // Make hold note disappear slowly with longer satisfying animation tween(note, { alpha: 0, scaleX: 0.1, scaleY: 0.1, tint: 0x00FF00 // Green glow for successful hold completion }, { duration: 800, //{8j} // Much longer fade easing: tween.easeIn, onFinish: function onFinish() { if (note.parent) { note.parent.removeChild(note); } } }); score += holdActive.scoreValue + 50; // Bonus for completing hold combo += 1; hitNotes += 1; maxCombo = Math.max(combo, maxCombo); // Light up hit area for completed hold var lane = note.lane; var hitArea = laneDividers[lane]; if (hitArea) { // Store original tint if not already stored if (hitArea.originalTint === undefined) { hitArea.originalTint = hitArea.tint || 0xFFFFFF; } // Light up with green glow for hold completion hitArea.tint = 0x00FF00; hitArea.alpha = 1.0; // Create sparkling effect with pulsing animation for hold completion tween(hitArea, { scaleX: hitArea.scaleX * 1.5, scaleY: hitArea.scaleY * 1.5 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { tween(hitArea, { scaleX: hitArea.scaleX / 1.5, scaleY: hitArea.scaleY / 1.5 }, { duration: 200, easing: tween.easeIn }); } }); // Fade back to normal after short duration tween(hitArea, { alpha: 0.7 }, { duration: 300, onFinish: function onFinish() { hitArea.tint = hitArea.originalTint; } }); } // Show score popup var popup = new ScorePopup(); popup.init(holdActive.scoreType, note.x, note.y); game.addChild(popup); scorePopups.push(popup); holdActive = null; } } } // --- INPUT HANDLING --- var dragLane = null; var dragY = null; var dragNote = null; var dragStartX = null; var dragStartY = null; var dragStartTime = null; function getLaneFromX(x) { var laneSpacing = 2048 / LANE_COUNT; for (var i = 0; i < LANE_COUNT; ++i) { var laneStart = i * laneSpacing; var laneEnd = (i + 1) * laneSpacing; if (x >= laneStart && x < laneEnd) { return i; } } return -1; } function handleGameDown(x, y, obj) { if (GAME_STATE !== 'PLAY' || !isPlaying) { return; } var lane = getLaneFromX(x); if (lane === -1) { return; } dragLane = lane; dragY = y; dragStartX = x; dragStartY = y; dragStartTime = Date.now(); // Check for tap/hold notes for (var i = 0; i < currentNotes.length; ++i) { var note = currentNotes[i]; if (!note.active || note.hit) { continue; } if (note.type === 'tap' || note.type === 'hold') { if (judgeNote(note, lane, y, 'down')) { if (note.type === 'tap') { note.hit = true; note.active = false; } if (note.type === 'hold') { dragNote = note; } break; } } } } function handleGameUp(x, y, obj) { if (GAME_STATE !== 'PLAY' || !isPlaying) { return; } if (holdActive && holdActive.note) { var t = getCurrentTime(); var note = holdActive.note; var holdEnd = note.time + note.duration; // Check if hold was completed (reached full duration) if (t >= holdEnd) { endHold(); } else { // Released too early - but if the button detected the hold note with sparkling light, count as good/great/perfect based on timing // Use the timing at release to judge var dt = Math.abs(note.time - t); var scoreType = ""; var scoreValue = 0; if (dt <= PERFECT_WINDOW) { scoreType = "perfect"; scoreValue = 150; perfectNotes += 1; } else if (dt <= GREAT_WINDOW) { scoreType = "great"; scoreValue = 120; greatNotes += 1; } else if (dt <= GOOD_WINDOW) { scoreType = "good"; scoreValue = 80; goodNotes += 1; } else if (dt <= BAD_WINDOW) { scoreType = "bad"; scoreValue = 40; badNotes += 1; } else { // If not within any window, count as miss note.hit = false; note.active = false; // Make failed hold note fade out with red tint tween(note, { alpha: 0.3, tint: 0xFF0000 }, { duration: 400, easing: tween.easeOut, onFinish: function onFinish() { if (note.parent) { note.parent.removeChild(note); } } }); combo = 0; missNotes += 1; // Show miss popup var popup = new ScorePopup(); popup.init("miss", note.x, note.y); game.addChild(popup); scorePopups.push(popup); holdActive = null; return; } // If we get here, it's a good/great/perfect/bad (not miss) note.hit = true; note.active = false; // Remove from currentNotes array var holdIndex = currentNotes.indexOf(note); if (holdIndex > -1) { currentNotes.splice(holdIndex, 1); } // Make hold note disappear with a fade animation tween(note, { alpha: 0, scaleX: 0.1, scaleY: 0.1, tint: 0xFFD700 // Gold for partial hold }, { duration: 500, easing: tween.easeIn, onFinish: function onFinish() { if (note.parent) { note.parent.removeChild(note); } } }); score += scoreValue; combo += 1; hitNotes += 1; maxCombo = Math.max(combo, maxCombo); // Light up hit area for partial hold var lane = note.lane; var hitArea = laneDividers[lane]; if (hitArea) { if (hitArea.originalTint === undefined) { hitArea.originalTint = hitArea.tint || 0xFFFFFF; } hitArea.tint = 0xFFD700; hitArea.alpha = 1.0; tween(hitArea, { scaleX: hitArea.scaleX * 1.2, scaleY: hitArea.scaleY * 1.2 }, { duration: 150, easing: tween.easeOut, onFinish: function onFinish() { tween(hitArea, { scaleX: hitArea.scaleX / 1.2, scaleY: hitArea.scaleY / 1.2 }, { duration: 150, easing: tween.easeIn }); } }); tween(hitArea, { alpha: 0.7 }, { duration: 200, onFinish: function onFinish() { hitArea.tint = hitArea.originalTint; } }); } // Show score popup var popup = new ScorePopup(); popup.init(scoreType, note.x, note.y); game.addChild(popup); scorePopups.push(popup); holdActive = null; } } dragLane = null; dragNote = null; dragStartX = null; dragStartY = null; dragStartTime = null; } function handleGameMove(x, y, obj) { if (GAME_STATE !== 'PLAY' || !isPlaying) { return; } // For hold notes, check if finger is still on lane - must maintain contact until completion if (holdActive && dragNote) { var t = getCurrentTime(); var note = holdActive.note; var holdEnd = note.time + note.duration; // Allow movement anywhere horizontally (any lane) for holds // Allow extremely wide vertical margin for forgiving detection var verticalLeniency = 950; if (Math.abs(y - HIT_LINE_Y) > verticalLeniency) { // Released too early or moved too far - fail the hold note note.hit = false; note.active = false; // Make failed hold note fade out with red tint tween(note, { alpha: 0.3, tint: 0xFF0000 }, { duration: 400, easing: tween.easeOut, onFinish: function onFinish() { if (note.parent) { note.parent.removeChild(note); } } }); combo = 0; missNotes += 1; // Show miss popup var popup = new ScorePopup(); popup.init("miss", note.x, note.y); game.addChild(popup); scorePopups.push(popup); holdActive = null; dragNote = null; } else if (t >= holdEnd) { // Successfully held until completion endHold(); } } } function handleGameSwipe(x, y, obj) { if (GAME_STATE !== 'PLAY' || !isPlaying) { return; } if (dragStartX === null || dragStartY === null) { return; } var dx = x - dragStartX; var dy = y - dragStartY; var absDx = Math.abs(dx); var absDy = Math.abs(dy); // Enhanced sensitivity - reduced movement threshold and require minimum distance var minSwipeDistance = 30; // Lowered from 60 for even better sensitivity var swipeDistance = Math.sqrt(dx * dx + dy * dy); if (swipeDistance < minSwipeDistance) { return; } // Not enough movement // Allow both horizontal and vertical swipes, with more leniency var dir = null; if (absDx > absDy && absDx > 20) { // Horizontal swipe dir = dx > 0 ? 'right' : 'left'; } else if (absDy > absDx && absDy > 20) { // Vertical swipe dir = dy > 0 ? 'down' : 'up'; } else { // Not a valid swipe return; } var lane = getLaneFromX(dragStartX); // Find the best matching swipe note that is near the hit area var bestNote = null; var bestScore = -1; for (var i = 0; i < currentNotes.length; ++i) { var note = currentNotes[i]; if (!note.active || note.hit || note.type !== 'swipe') { continue; } // Only allow swipe detection when note is close to the hit area - more lenient threshold var noteY = getNoteY(note.time); var distanceToHitArea = Math.abs(noteY - HIT_LINE_Y); if (distanceToHitArea > 450) { // Increased from 300 for better sensitivity continue; // Note is too far from hit area, skip } // Check if this swipe note matches the gesture direction if (note.swipeDir === dir) { var t = getCurrentTime(); var dt = Math.abs(note.time - t); // Score based on timing and position accuracy var timingScore = Math.max(0, BAD_WINDOW - dt); var positionScore = Math.max(0, 280 - Math.abs(dragStartY - HIT_LINE_Y)); var totalScore = timingScore + positionScore; if (totalScore > bestScore) { bestScore = totalScore; bestNote = note; } } } // Execute the best matching swipe note if (bestNote && judgeNote(bestNote, lane, dragStartY, 'swipe', dir)) { // Successfully completed swipe } dragStartX = null; dragStartY = null; } // Attach input handlers game.down = function (x, y, obj) { handleGameDown(x, y, obj); }; game.up = function (x, y, obj) { handleGameUp(x, y, obj); }; game.move = function (x, y, obj) { handleGameMove(x, y, obj); // Detect swipe with improved sensitivity - reduced movement threshold if (dragStartX !== null && dragStartY !== null) { var dx = x - dragStartX; var dy = y - dragStartY; // Improved sensitivity - reduced movement threshold for better swipe detection if (Math.abs(dx) > 30 || Math.abs(dy) > 30) { handleGameSwipe(x, y, obj); } } }; // --- GAME UPDATE LOOP --- game.update = function () { if (GAME_STATE !== 'PLAY' || !isPlaying) { return; } var t = getCurrentTime(); // Update notes for (var i = 0; i < currentNotes.length; ++i) { var note = currentNotes[i]; if (!note.active) { continue; } // Always update note position every frame to prevent stuck notes if (note.time > 0) { var laneSpacing = 2048 / LANE_COUNT; note.x = (note.lane + 0.5) * laneSpacing; // Center in the lane // Always update Y, even if out of visible range, to ensure flow note.y = getNoteY(note.time); } // Only mark notes as missed if they have passed VERY far beyond the hit area to prevent automatic disappearing var timePassed = t - note.time; if (!note.hit && note.y > HIT_LINE_Y + 1000 && timePassed > BAD_WINDOW + 1000 && note.type !== 'hold') { note.active = false; // Do not set note.hit = true here, so that removal logic is consistent // Immediately remove from currentNotes array to prevent flow issues var missedIndex = currentNotes.indexOf(note); if (missedIndex > -1) { currentNotes.splice(missedIndex, 1); // Do not decrement i here, as we always iterate forward and skip removed notes } // Make missed note fade out and disappear completely tween(note, { alpha: 0, scaleX: 0.5, scaleY: 0.5 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { if (note.parent) { note.parent.removeChild(note); } } }); combo = 0; missNotes += 1; // Show miss popup var popup = new ScorePopup(); popup.init("miss", note.x, note.y); game.addChild(popup); scorePopups.push(popup); continue; // Skip to next note since this one was processed } // For hold notes, only mark as missed if they are VERY far past the hit area to prevent automatic disappearing var holdTimePassed = t - note.time; if (note.type === 'hold' && !note.hit && note.y > HIT_LINE_Y + 1200 && holdActive === null && holdTimePassed > BAD_WINDOW + 1500) { note.active = false; // Do not set note.hit = true here, so that removal logic is consistent // Immediately remove from currentNotes array var holdMissedIndex = currentNotes.indexOf(note); if (holdMissedIndex > -1) { currentNotes.splice(holdMissedIndex, 1); // Do not decrement i here, as we always iterate forward and skip removed notes } // Make missed hold note fade out with red tint tween(note, { alpha: 0, scaleX: 0.5, scaleY: 0.5 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { if (note.parent) { note.parent.removeChild(note); } } }); combo = 0; missNotes += 1; // Show miss popup var popup = new ScorePopup(); popup.init("miss", note.x, note.y); game.addChild(popup); scorePopups.push(popup); continue; // Skip to next note since this one was processed } } // Remove notes that are far below screen or completed for (var i = currentNotes.length - 1; i >= 0; --i) { var note = currentNotes[i]; // Only remove if not already removed in this frame if ((note.y > 3000 || !note.active && note.y > HIT_LINE_Y + 400 || note.hit) && note.parent) { note.parent.removeChild(note); currentNotes.splice(i, 1); } } // Update GUI scoreText.setText("Score: " + score); comboText.setText("Combo: " + combo); var acc = totalNotes > 0 ? Math.floor(100 * hitNotes / totalNotes) : 100; accuracyText.setText("Accuracy: " + acc + "%"); // End of song - check if song duration has been reached since music only plays once if (t > getSongEndTime() && isPlaying) { isPlaying = false; // If Hard mode, show result immediately after music ends (no tempo flow wait) if (selectedDifficulty === 'Hard') { setGameState('RESULT'); } else { // Music will stop automatically since it only plays once resultTimeout = LK.setTimeout(function () { setGameState('RESULT'); }, 1200); } } }; function getSongEndTime() { // Use the full song duration from the song data if (selectedSong && selectedSong.duration) { return selectedSong.duration; } // Fallback to last note timing if duration not specified var last = currentSongData[currentSongData.length - 1]; if (!last) { return 0; } if (last[0] === 'hold') { return last[2] + last[3] + 2000; // Add extra time for hold completion } return last[2] + 3000; // Allow full song to play with extra buffer } // Utility function to remove all gameplay backgrounds from the game scene function removeGameplayBackgrounds() { if (game && game.children && game.children.length > 0) { for (var i = game.children.length - 1; i >= 0; --i) { var child = game.children[i]; if (child && child.assetId && (child.assetId === 'BgSkyJourney' || child.assetId === 'BgNightPulse' || child.assetId === 'BgSunriseWaltz')) { game.removeChild(child); } } } } // --- RESULT SCREEN --- function showResult() { clearGameObjects(); // Remove any gameplay backgrounds if they exist (prevents background leak to result screen) removeGameplayBackgrounds(); // Remove gameplay backgrounds after clicking exit to ensure they do not appear on homepage or next song removeGameplayBackgrounds(); var acc = totalNotes > 0 ? Math.floor(100 * hitNotes / totalNotes) : 100; var screenWidth = LK.width || 2048; var screenHeight = LK.height || 2732; // Remove gameplay backgrounds immediately after scoreboard appears removeGameplayBackgrounds(); // Create SongStorybox container for results var resultBox = new Container(); // Add SongStorybox asset as background, centered and scaled var boxAsset = LK.getAsset('SongStorybox', { anchorX: 0.5, anchorY: 0.5, x: 0, y: 0, scaleX: 13, scaleY: 11 }); resultBox.addChild(boxAsset); // Calculate box bounds for layout var boxW = boxAsset.width * boxAsset.scaleX; var boxH = boxAsset.height * boxAsset.scaleY; // Add song cover on the left side var coverAsset = LK.getAsset(selectedSong.cover, { anchorX: 0.5, anchorY: 0.5, x: -boxW * 0.25, y: -boxH * 0.15, scaleX: 1.2, scaleY: 1.2 }); resultBox.addChild(coverAsset); // Main result text on the right side var resultStr = (selectedLanguage === 'zh' ? "结果" : "Result") + "\n\n"; resultStr += (selectedLanguage === 'zh' ? "总分: " : "Total Score: ") + score + "\n"; resultStr += (selectedLanguage === 'zh' ? "最大连击: " : "Max Combo: ") + maxCombo + "\n"; resultStr += (selectedLanguage === 'zh' ? "准确率: " : "Accuracy: ") + acc + "%\n\n"; // Calculate grade based on accuracy var grade = "F"; if (acc >= 95) grade = "S";else if (acc >= 90) grade = "A";else if (acc >= 80) grade = "B";else if (acc >= 70) grade = "C";else if (acc >= 60) grade = "D"; resultStr += (selectedLanguage === 'zh' ? "等级: " : "Grade: ") + grade + "\n\n"; // Detailed scoring breakdown resultStr += (selectedLanguage === 'zh' ? "详细统计:" : "Stats:") + "\n"; resultStr += (selectedLanguage === 'zh' ? "完美: " : "Perfect: ") + perfectNotes + "\n"; resultStr += (selectedLanguage === 'zh' ? "优秀: " : "Great: ") + greatNotes + "\n"; resultStr += (selectedLanguage === 'zh' ? "良好: " : "Good: ") + goodNotes + "\n"; resultStr += (selectedLanguage === 'zh' ? "一般: " : "Bad: ") + badNotes + "\n"; resultStr += (selectedLanguage === 'zh' ? "未命中: " : "Miss: ") + missNotes; resultText = new Text2(resultStr, { size: 50, fill: "#fff", align: "left", wordWrap: true, wordWrapWidth: boxW * 0.4 }); resultText.anchor.set(0, 0.5); resultText.x = boxW * 0.05; resultText.y = -boxH * 0.1; resultBox.addChild(resultText); // Exit button to return to song selection var exitButton = LK.getAsset('SelectorUI', { anchorX: 0.5, anchorY: 0.5, x: 0, y: boxH * 0.35, scaleX: 3.0, scaleY: 1.5 }); exitButton.interactive = true; exitButton.down = function (x, y, obj) { LK.getSound('Touch').play(); setGameState('HOME'); }; resultBox.addChild(exitButton); // Exit button text var exitText = new Text2(selectedLanguage === 'zh' ? "退出" : "Exit", { size: 70, fill: "#fff" }); exitText.anchor.set(0.5, 0.5); exitText.x = 0; exitText.y = boxH * 0.35; resultBox.addChild(exitText); // Add result box to center of screen gui.center.addChild(resultBox); } // --- START GAME --- // Start at language selector (opening screen) setGameState('LANGUAGE');
===================================================================
--- original.js
+++ change.js
@@ -1872,12 +1872,17 @@
accuracyText.setText("Accuracy: " + acc + "%");
// End of song - check if song duration has been reached since music only plays once
if (t > getSongEndTime() && isPlaying) {
isPlaying = false;
- // Music will stop automatically since it only plays once
- resultTimeout = LK.setTimeout(function () {
+ // If Hard mode, show result immediately after music ends (no tempo flow wait)
+ if (selectedDifficulty === 'Hard') {
setGameState('RESULT');
- }, 1200);
+ } else {
+ // Music will stop automatically since it only plays once
+ resultTimeout = LK.setTimeout(function () {
+ setGameState('RESULT');
+ }, 1200);
+ }
}
};
function getSongEndTime() {
// Use the full song duration from the song data
Game design for Kaleidoscope of Music Rhythm without game title, just the design for opening screen background. In-Game asset. 2d. High contrast. No shadows
Cyberpunk style design empty selector UI. In-Game asset. 2d. High contrast. No shadows
Cyberpunk style hold note for music rhythm game. In-Game asset. 2d. High contrast. No shadows
Cyberpunk style swipe note for music rhythm game. In-Game asset. 2d. High contrast. No shadows
Cyberpunk style tap note for music rhythm game. In-Game asset. 2d. High contrast. No shadows
Cyberpunk style kaleidoscope pattern design for homepage. In-Game asset. 2d. High contrast. No shadows
Cyberpunk style lane divider for music rhythm game that is horizontal and used for judging the touching of rhythm In-Game asset. 2d. High contrast. No shadows
Empty cyberpunk style storybox design in which it's size enable song cover, difficulty level and song description to be added in the storybox. In-Game asset. 2d. High contrast. No shadows
Round shape of song cover of anime style with Sky Journey theme. In-Game asset. 2d. High contrast. No shadows
Round shape song cover of cyberpunk anime style with the themed “Night Pulse”. In-Game asset. 2d. High contrast. No shadows
Round shape of anime style song cover with the themed Sunrise Waltz. In-Game asset. 2d. High contrast. No shadows
Anime style design for the round shape song cover of New Era Malay Style
Round shape, Replace Chinese word from “不知途”to“莫问前程”,others remaining the same
Empty cyberpunk style menu design. In-Game asset. 2d. High contrast. No shadows
Cyberpunk style pause symbol. In-Game asset. 2d. High contrast. No shadows