User prompt
Remove small words 新纪元马来风, only one 新纪元马来风
User prompt
Reserve the English title New Era Malay Style in the Chinese version
User prompt
Add Chinese translation for New Era Malay Style Chinese version
User prompt
Make sure the words New Era Malay Style is within the SelectorUI
User prompt
Increase the size of SelectorUI for New Era Malay Style
User prompt
Add BgNewEraMalayStyle as the gameplay background for New Era Ma
User prompt
Add SongCover5 for New Era Malay Style
User prompt
Insert New Era Malay Style as song selection
User prompt
Insert all the assets for New Era Malay Style
User prompt
Fix the hit area becomes bigger error without returning to it's original shape
User prompt
Change the result screen font color back to white for Unknown Journey
User prompt
The tempo flowing suddenly stops after Unknown Journey play for a while, reanalyzes Unknown Journey and remake the tempo flowing according to each of the difficulty levels
User prompt
Make sure the tempo is flowing for the full song Unknown Journey
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
/**** * 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 ****/ // Sounds and music (placeholders, actual music not implemented here) // Example note assets: // We'll use shapes for notes, and images for backgrounds and song covers as needed. // Note: Assets are auto-initialized by LK based on usage below. // --- GLOBALS --- var _templateObject; function _taggedTemplateLiteral(e, t) { return t || (t = e.slice(0)), Object.freeze(Object.defineProperties(e, { raw: { value: Object.freeze(t) } })); } var gameplayBackground = null; // Track the current gameplay background 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: 'UnknownJourney', title: 'Unknown Journey', title_zh: '莫问前程', 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'], notes: { Easy: [ // Unknown Journey Easy: Continuous tap flow, 120 BPM (500ms/beat), 4/4, 240s = 480 beats // Intro (first 8 beats, spaced) ['tap', 0, 2000], ['tap', 1, 3000], ['tap', 2, 4000], ['tap', 3, 5000], ['tap', 0, 6000], ['tap', 1, 7000], ['tap', 2, 8000], ['tap', 3, 9000] // Main section: every 500ms, alternate lanes, full song // 480 beats, start at 10000ms, end at 240000ms ].concat(function () { var arr = []; var t = 10000; var lane = 0; for (; t <= 238000; t += 500) { arr.push(['tap', lane, t]); lane = (lane + 1) % 4; } // Final section (slow down, every 1000ms) arr.push(['tap', 0, 239000]); arr.push(['tap', 1, 240000]); return arr; }()), Medium: [ // Unknown Journey Medium: More density, add some holds and swipes, 120 BPM (500ms/beat) // Intro (first 8 beats, spaced) ['tap', 0, 1500], ['tap', 1, 2500], ['hold', 2, 3500, 1200], ['tap', 3, 5000], ['tap', 0, 6500], ['tap', 1, 8000], ['hold', 2, 9500, 1200], ['tap', 3, 11000] // Main flow: every 500ms, alternate lanes, add swipes every 4th beat, holds every 8th beat ].concat(function () { var arr = []; var t = 12000; var lane = 0; for (var i = 0; t <= 238000; t += 500, i++) { if (i % 8 === 4) { // Every 8th beat, add a hold arr.push(['hold', lane, t, 1000]); } else if (i % 4 === 2) { // Every 4th beat, add a swipe var dir = ['left', 'right', 'up', 'down'][i / 4 % 4 | 0]; arr.push(['swipe', lane, t, 0, dir]); } else { arr.push(['tap', lane, t]); } lane = (lane + 1) % 4; } // Final section (slow down, every 1000ms) arr.push(['tap', 0, 239000]); arr.push(['tap', 1, 240000]); return arr; }()), Hard: [ // Unknown Journey Hard: High density, more holds and swipes, 120 BPM (500ms/beat), 16th notes (250ms), cross-lane // Intro (first 8 beats, 250ms apart) ['tap', 0, 1000], ['tap', 1, 1250], ['tap', 2, 1500], ['tap', 3, 1750], ['tap', 0, 2000], ['tap', 1, 2250], ['tap', 2, 2500], ['tap', 3, 2750] // Main flow: every 250ms, alternate lanes, add swipes every 8th, holds every 16th ].concat(function () { var arr = []; var t = 3000; var lane = 0; for (var i = 0; t <= 239000; t += 250, i++) { if (i % 16 === 8) { // Every 16th, add a hold arr.push(['hold', lane, t, 900]); } else if (i % 8 === 4) { // Every 8th, add a swipe var dir = ['left', 'right', 'up', 'down'][i / 8 % 4 | 0]; arr.push(['swipe', lane, t, 0, dir]); } else { arr.push(['tap', lane, t]); } lane = (lane + 1) % 4; } // Final section (slow down, every 500ms) arr.push(['tap', 0, 239500]); arr.push(['tap', 1, 240000]); return arr; }()) } }, // --- New Era Malay Style song entry --- { id: 'NewEraMalayStyle', title: 'New Era Malay Style', title_zh: '新纪元马来风', cover: 'songCoverNewEraMalayStyle', duration: 180000, story: { en: "A vibrant fusion of Malay traditional melodies and modern beats, celebrating the spirit of a new era.", zh: "融合马来传统旋律与现代节奏,展现新时代的活力与精神。" }, difficulties: ['Easy', 'Medium', 'Hard'], notes: { Easy: [ // Simple tap pattern, 100 BPM (600ms/beat), 3/4, 180s = 300 beats // Intro (first 4 beats, spaced) ['tap', 0, 2000], ['tap', 1, 3200], ['tap', 2, 4400], ['tap', 3, 5600]].concat(function () { var arr = []; var t = 7000; var lane = 0; for (; t <= 178000; t += 600) { arr.push(['tap', lane, t]); lane = (lane + 1) % 4; } // Final section (slow down, every 1200ms) arr.push(['tap', 0, 179200]); arr.push(['tap', 1, 180000]); return arr; }()), Medium: [ // More density, add some holds and swipes, 100 BPM (600ms/beat) // Intro (first 4 beats, spaced) ['tap', 0, 1500], ['tap', 1, 2700], ['hold', 2, 3900, 1200], ['tap', 3, 5600]].concat(function () { var arr = []; var t = 7000; var lane = 0; for (var i = 0; t <= 178000; t += 600, i++) { if (i % 8 === 4) { // Every 8th beat, add a hold arr.push(['hold', lane, t, 1000]); } else if (i % 4 === 2) { // Every 4th beat, add a swipe var dir = ['left', 'right', 'up', 'down'][i / 4 % 4 | 0]; arr.push(['swipe', lane, t, 0, dir]); } else { arr.push(['tap', lane, t]); } lane = (lane + 1) % 4; } // Final section (slow down, every 1200ms) arr.push(['tap', 0, 179200]); arr.push(['tap', 1, 180000]); return arr; }()), Hard: [ // High density, more holds and swipes, 100 BPM (600ms/beat), 16th notes (300ms), cross-lane // Intro (first 8 beats, 300ms apart) ['tap', 0, 1000], ['tap', 1, 1300], ['tap', 2, 1600], ['tap', 3, 1900], ['tap', 0, 2200], ['tap', 1, 2500], ['tap', 2, 2800], ['tap', 3, 3100]].concat(function () { var arr = []; var t = 3400; var lane = 0; for (var i = 0; t <= 179000; t += 300, i++) { if (i % 16 === 8) { // Every 16th, add a hold arr.push(['hold', lane, t, 900]); } else if (i % 8 === 4) { // Every 8th, add a swipe var dir = ['left', 'right', 'up', 'down'][i / 8 % 4 | 0]; arr.push(['swipe', lane, t, 0, dir]); } else { arr.push(['tap', lane, t]); } lane = (lane + 1) % 4; } // Final section (slow down, every 600ms) arr.push(['tap', 0, 179600]); arr.push(['tap', 1, 180000]); return arr; }()) } }, { 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() { removeGameplayBackgrounds(); // Ensure backgrounds are removed // 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(); removeGameplayBackgrounds(); // Double-call for safety // 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(); // Remove gameplay backgrounds again to ensure they do not reappear on homepage removeGameplayBackgrounds(); 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 selectorScaleX = 4.0; var selectorScaleY = 2.0; if (song.id === "NewEraMalayStyle") { selectorScaleX = 5.0; selectorScaleY = 2.5; } var cardBg = LK.getAsset('SelectorUI', { anchorX: 0.5, anchorY: 0.5, x: 0, y: cardY, scaleX: selectorScaleX, scaleY: selectorScaleY }); 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; if (song.id === "UnknownJourney") { songNameText = new Text2(song.title, { size: 44, fill: "#fff", font: "Arial" }); } else if (song.id === "NewEraMalayStyle") { var displayTitle = selectedLanguage === 'zh' ? song.title_zh : song.title; songNameText = new Text2(displayTitle, { size: 48, fill: "#fff", font: "Arial" }); } else { songNameText = new Text2(song.title, { size: 64, fill: "#fff", font: "Arial" }); } songNameText.anchor.set(0.5, 0.5); songNameText.x = 0; songNameText.y = song.id === "NewEraMalayStyle" ? cardY - 20 : cardY - 30; homeScreen.addChild(songNameText); // Add subtitle (optional: show "Sky Journey", "Night Pulse", "Sunrise Waltz", "Unknown Journey") var subtitleZh = ""; if (selectedLanguage === 'zh') { if (song.title === "Sky Journey") subtitleZh = "天空之旅";else if (song.title === "Night Pulse") subtitleZh = "夜之律动";else if (song.title === "Sunrise Waltz") subtitleZh = "日出圆舞曲";else if (song.title === "Unknown Journey") subtitleZh = "莫问前程";else if (song.title === "New Era Malay Style") subtitleZh = "新纪元马来风"; } var subtitleText; if (song.id === "UnknownJourney") { subtitleText = new Text2(subtitleZh, { size: 28, fill: 0xE0E0E0, font: "Arial" }); } else if (song.id === "NewEraMalayStyle" && selectedLanguage === 'zh') { // Only show one instance of 新纪元马来风 (no duplicate small subtitle) subtitleText = new Text2("New Era Malay Style", { size: 36, fill: 0xE0E0E0, font: "Arial" }); } else { subtitleText = new Text2(subtitleZh, { 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 // For Chinese, ensure paragraph form and improved word wrap var storyParagraphs = []; if (selectedLanguage === 'zh') { // Remove all whitespace and line breaks var cleaned = story.replace(/[\r\n]/g, ''); // Split at Chinese fullstop (。), Chinese comma (,), normal comma (,), normal fullstop (.), exclamation (!,!), question (?,?) var parts = cleaned.split(/(。|,|,|\.|!|!|\?|?)/); var temp = ""; for (var i = 0; i < parts.length; i++) { temp += parts[i]; // If the part is a punctuation, treat as paragraph end if (parts[i] === "。" || parts[i] === "," || parts[i] === "," || parts[i] === "." || parts[i] === "!" || parts[i] === "!" || parts[i] === "?" || parts[i] === "?") { if (temp.trim().length > 0) { storyParagraphs.push(temp.trim()); } temp = ""; } } // If any text remains (no ending punctuation), add as last paragraph if (temp.trim().length > 0) { storyParagraphs.push(temp.trim()); } // Further split any paragraph that is too long to fit, to prevent off-screen/overlap var maxCharsPerParagraph = 13; // Tighter for best fit with cover var adjustedParagraphs = []; for (var i = 0; i < storyParagraphs.length; ++i) { var para = storyParagraphs[i]; // If paragraph is too long, split into multiple while (para.length > maxCharsPerParagraph) { adjustedParagraphs.push(para.substring(0, maxCharsPerParagraph)); para = para.substring(maxCharsPerParagraph); } if (para.length > 0) { adjustedParagraphs.push(para); } } storyParagraphs = adjustedParagraphs; } else { storyParagraphs = [story]; } // Calculate Y start so text never overlaps cover and never goes off box var storyTextY = boxTopY + boxH * 0.13; for (var i = 0; i < storyParagraphs.length; ++i) { var para = storyParagraphs[i]; var paraText = new Text2(para, { size: 55, fill: "#fff", wordWrap: true, wordWrapWidth: selectedLanguage === 'zh' ? boxW * 0.54 : boxW * 0.45, align: "left" }); if (selectedLanguage === 'zh') { paraText.anchor.set(0, 0); // Left align for Chinese paraText.x = boxLeftX + boxW * 0.48; // Start further right for Chinese, away from cover } else { paraText.anchor.set(0, 0); paraText.x = boxLeftX + boxW * 0.52; // Start at 52% from left edge } paraText.y = storyTextY; storyBox.addChild(paraText); // Move down for next paragraph, add extra spacing for Chinese storyTextY += paraText.height + (selectedLanguage === 'zh' ? 18 : 16); } // 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(); // Clear previous backgrounds // Add new background for gameplay and track it 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); } else if (selectedSong && selectedSong.id === 'UnknownJourney') { gameplayBackground = LK.getAsset('BgUnknownJourney', { 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 === 'NewEraMalayStyle') { gameplayBackground = LK.getAsset('BgNewEraMalayStyle', { anchorX: 0.5, anchorY: 0, x: 2048 / 2, y: 0, scaleX: 2048 / 2048, scaleY: 2732 / 3066.01 }); gameplayBackground.width = 2048; gameplayBackground.height = 2732; game.addChildAt(gameplayBackground, 0); } 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 var scoreboardFontColor = selectedSong && selectedSong.id === 'UnknownJourney' ? "#000" : "#fff"; scoreText = new Text2("Score: 0", { size: 70, fill: scoreboardFontColor }); scoreText.anchor.set(0.5, 0); scoreText.x = 0; scoreText.y = 20; gui.top.addChild(scoreText); comboText = new Text2("Combo: 0", { size: 70, fill: scoreboardFontColor }); comboText.anchor.set(0.5, 0); comboText.x = 0; comboText.y = 70; gui.top.addChild(comboText); accuracyText = new Text2("Accuracy: 100%", { size: 70, fill: scoreboardFontColor }); 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, but never allow scale to exceed original tween(hitArea, { scaleX: Math.max(hitArea.originalScaleX, hitArea.originalScaleX * 1.3), scaleY: Math.max(hitArea.originalScaleY, 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: Math.max(hitArea.originalScaleX, hitArea.originalScaleX * 1.2), scaleY: Math.max(hitArea.originalScaleY, 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, but never allow scale to exceed original var delay = v * 50; tween(hitArea, { scaleX: Math.max(hitArea.originalScaleX, hitArea.originalScaleX * 1.4), scaleY: Math.max(hitArea.originalScaleY, hitArea.originalScaleY * 1.4) }, { duration: 200, delay: delay, easing: tween.easeOut, onFinish: function onFinish() { tween(hitArea, { scaleX: hitArea.originalScaleX, scaleY: hitArea.originalScaleY }, { 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, but never allow scale to exceed original tween(hitArea, { scaleX: Math.max(hitArea.originalScaleX, hitArea.originalScaleX * 1.5), scaleY: Math.max(hitArea.originalScaleY, hitArea.originalScaleY * 1.5) }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { tween(hitArea, { scaleX: hitArea.originalScaleX, scaleY: hitArea.originalScaleY }, { 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: Math.max(hitArea.originalScaleX, hitArea.originalScaleX * 1.2), scaleY: Math.max(hitArea.originalScaleY, hitArea.originalScaleY * 1.2) }, { duration: 150, easing: tween.easeOut, onFinish: function onFinish() { tween(hitArea, { scaleX: hitArea.originalScaleX, scaleY: hitArea.originalScaleY }, { 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 (gameplayBackground && gameplayBackground.parent) { gameplayBackground.parent.removeChild(gameplayBackground); gameplayBackground = null; } } // --- RESULT SCREEN --- function showResult() { removeGameplayBackgrounds(); clearGameObjects(); // Remove gameplay backgrounds again to ensure they do not appear on result screen removeGameplayBackgrounds(); // Remove gameplay backgrounds immediately after scoreboard appears (triple call to guarantee removal in all result transitions) removeGameplayBackgrounds(); var acc = totalNotes > 0 ? Math.floor(100 * hitNotes / totalNotes) : 100; var screenWidth = LK.width || 2048; var screenHeight = LK.height || 2732; // 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; var resultFontColor = "#fff"; resultText = new Text2(resultStr, { size: 50, fill: resultFontColor, 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(); removeGameplayBackgrounds(); 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
@@ -913,10 +913,10 @@
fill: 0xE0E0E0,
font: "Arial"
});
} else if (song.id === "NewEraMalayStyle" && selectedLanguage === 'zh') {
- // Show both Chinese and English title for New Era Malay Style in Chinese version
- subtitleText = new Text2(subtitleZh + "\nNew Era Malay Style", {
+ // Only show one instance of 新纪元马来风 (no duplicate small subtitle)
+ subtitleText = new Text2("New Era Malay Style", {
size: 36,
fill: 0xE0E0E0,
font: "Arial"
});
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