User prompt
Make sure the notes are flowing for the hard mode of full songs
User prompt
Reduce the notes for hard mode so that it is player friendly
User prompt
There are suddenly tempo flowing stop for the hard mode after the songs are play for a while, reanalyze all of the songs and make tempo flowing for hard mode for all of the songs
User prompt
Remove all gameplay backgrounds after clicking exit, don't appear any gameplay backgrounds at the homepage
User prompt
Gameplay background still appear after I click exit
User prompt
Still occurring the same song background display error, for example, BgSkyJourney will appear to the homepage and as the background for other songs after I finished my game, the background for each song should be automatically disappear after the game play is ended
User prompt
There are background display error, where the background of previous song will be accidentally shown to the homepage and other songs after a song is played
User prompt
Insert BgSunriseWaltz as the background for Sunrise Waltz without affecting gameplay mode
User prompt
Make BgNightPulse fully fit the gameplay screen, without affecting the gameplay mode
User prompt
Insert BgNightPulse as the background of gameplay mode for Night Pulse, without affecting the gameplay scene
User prompt
Insert BgSkyJourney as the background of gameplay mode for Sky Journey, without affecting the gameplay scene
User prompt
Increase sensitivity of swipe notes and hold notes
User prompt
Make sure all the notes always flow and didn't stop immediately in the lane
User prompt
Analyses the full songs for Night Pulse and Sunrise Waltz, and make the tempo flowing for the full song
User prompt
There are notes flowing errors for both Night Pulse and Sunrise Waltz, that the notes suddenly not flowing after the songs are playing for a while
User prompt
For hold notes, if the button detected the hold notes with sparkling light, it should be counted as good, great or perfect based on the timing, instead of miss
User prompt
Remove all hold notes and swipe notes for all the easy mode of all songs, only medium mode and hard mode has hold notes and swipe notes, and adjust the errors for some of the hit area becomes bigger after touching the pad
User prompt
They're problems that the notes suddenly disappear automatically for all songs, and change the rule for swipe notes for just swiping horizontally, make the hold notes detection more sensitive with the rule that the hit area will appear sparkling light up animation when the player click the hold notes for a long time, and make the disappearing of hold notes longer ↪💡 Consider importing and using the following plugins: @upit/tween.v1 ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
The notes keep on disappear suddenly without reach the hit areas, fix this errors, and improve the sensitivity for hold notes and swipe notes
User prompt
Some of the notes suddenly disappear without reach the hit area, fix this errors, and adjust swipe notes rules for just only swipes across the hit areas when the swipe notes reach
User prompt
Add sparkling light animation for touching the notes
User prompt
There are some problems that the notes suddenly didn't flow after Sunrise Waltz play for a while, and make the swipe notes disappear even it results in miss
User prompt
Fix the hold notes rule to click the note longer until the note is disappear, for the swipe note, it should be swipe according to the note shape
User prompt
Make the hit area light up when the notes are touched
User prompt
Improve the sensitivity of swipe notes and hold notes, and make sure swipe notes are disappear without staying as red color at the behind of hit area ↪💡 Consider importing and using the following plugins: @upit/tween.v1
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1", { language: "en" }); /**** * Classes ****/ // Note types: 'tap', 'hold', 'swipe' var Note = Container.expand(function () { var self = Container.call(this); self.type = 'tap'; // tap, hold, swipe self.lane = 0; // 0-3 self.time = 0; // ms, when the note should be hit self.duration = 0; // for hold notes, ms self.hit = false; self.active = true; self.swipeDir = null; // 'left', 'right', 'up', 'down' for swipe notes // Visuals self.noteAsset = null; self.trailAsset = null; self.init = function (type, lane, time, duration, swipeDir) { self.type = type; self.lane = lane; self.time = time; self.duration = duration || 0; self.swipeDir = swipeDir || null; self.hit = false; self.active = true; if (self.noteAsset) { self.removeChild(self.noteAsset); } if (self.trailAsset) { self.removeChild(self.trailAsset); } if (type === 'tap') { self.noteAsset = self.attachAsset('tapNote', { anchorX: 0.5, anchorY: 0.5 }); } else if (type === 'hold') { self.noteAsset = self.attachAsset('holdNote', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.6, // Make hold notes bigger scaleY: 1.6 }); } else if (type === 'swipe') { // Calculate scale to ensure swipe notes cross two lanes var laneSpacing = 2048 / 4; // 4 lanes var swipeWidth = laneSpacing * 2.2; // Cross 2+ lanes self.noteAsset = self.attachAsset('swipeNote', { anchorX: 0.5, anchorY: 0.5, scaleX: swipeWidth / 120, // Scale based on original width of 120px scaleY: 2.2 // Make swipe notes taller too }); } }; self.flash = function () { if (self.noteAsset) { tween(self.noteAsset, { alpha: 0.2 }, { duration: 80, onFinish: function onFinish() { tween(self.noteAsset, { alpha: 1 }, { duration: 80 }); } }); } }; self.update = function () { // Position is handled by main game loop }; return self; }); // Score popup for visual feedback var ScorePopup = Container.expand(function () { var self = Container.call(this); self.scoreText = null; self.startTime = 0; self.duration = 1000; self.init = function (scoreType, x, y) { self.x = x; self.y = y; self.startTime = Date.now(); var text = ""; var color = "#fff"; var scoreValue = 0; switch (scoreType) { case 'perfect': text = "PERFECT"; color = "#FFD700"; scoreValue = 150; break; case 'great': text = "GREAT"; color = "#00FF00"; scoreValue = 120; break; case 'good': text = "GOOD"; color = "#FFFF00"; scoreValue = 80; break; case 'bad': text = "BAD"; color = "#FFA500"; scoreValue = 40; break; case 'miss': text = "MISS"; color = "#FF0000"; scoreValue = 0; break; } self.scoreText = new Text2(text + "\n+" + scoreValue, { size: 60, fill: color, align: "center" }); self.scoreText.anchor.set(0.5, 0.5); self.addChild(self.scoreText); // Animate popup tween(self, { y: self.y - 100, alpha: 0 }, { duration: self.duration, easing: tween.easeOut, onFinish: function onFinish() { if (self.parent) { self.parent.removeChild(self); } } }); }; return self; }); // Song selection card var SongCard = Container.expand(function () { var self = Container.call(this); self.songId = ''; self.cover = null; self.init = function (songId, colorAsset) { self.songId = songId; if (self.cover) { self.removeChild(self.cover); } if (colorAsset) { self.cover = self.attachAsset(colorAsset, { anchorX: 0.5, anchorY: 0.5 }); } }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x181c20 }); /**** * Game Code ****/ // --- GLOBALS --- // Note: Assets are auto-initialized by LK based on usage below. // We'll use shapes for notes, and images for backgrounds and song covers as needed. // Example note assets: // Sounds and music (placeholders, actual music not implemented here) var _templateObject; function _taggedTemplateLiteral(e, t) { return t || (t = e.slice(0)), Object.freeze(Object.defineProperties(e, { raw: { value: Object.freeze(t) } })); } var GAME_STATE = 'LANGUAGE'; // LANGUAGE, HOME, STORY, DIFFICULTY, PLAY, RESULT var selectedLanguage = storage.language || 'en'; var selectedSong = null; var selectedDifficulty = null; var currentSongData = null; var currentNotes = []; var noteIndex = 0; var startTime = 0; var score = 0; var combo = 0; var maxCombo = 0; var accuracy = 0; var totalNotes = 0; var hitNotes = 0; var missNotes = 0; var badNotes = 0; var goodNotes = 0; var greatNotes = 0; var perfectNotes = 0; var holdActive = null; var swipeStart = null; var swipeNote = null; var isPlaying = false; var resultTimeout = null; var scorePopups = []; // Scoring thresholds in milliseconds var PERFECT_WINDOW = 50; var GREAT_WINDOW = 100; var GOOD_WINDOW = 150; var BAD_WINDOW = 200; // Lanes: 4 lanes, evenly spaced var LANE_COUNT = 4; var LANE_WIDTH = 400; var LANE_MARGIN = 40; var LANE_START_X = (2048 - (LANE_COUNT * LANE_WIDTH + (LANE_COUNT - 1) * LANE_MARGIN)) / 2; var HIT_LINE_Y = 2200; // Where notes should be hit // --- SONG DATA (MVP: 3 songs, 3 difficulties, simple patterns) --- var SONGS = [{ id: 'SkyJourney', title: 'Sky Journey', cover: 'songCover1', duration: 180000, // 3 minutes full song story: { en: "You soar above the clouds, chasing the horizon. The sky is endless, and your journey has just begun.", zh: "你在云端翱翔,追逐地平线。天空无垠,你的旅程才刚刚开始。" }, difficulties: ['Easy', 'Medium', 'Hard'], notes: { Easy: [ // Full song analysis: Sky Journey - Uplifting 4/4 tempo, soaring melodies, 120 BPM (500ms per beat, 125ms per 16th note) // Intro - soft atmospheric entrance ['tap', 1, 2000], ['tap', 2, 3000], ['tap', 0, 4000], ['tap', 3, 5000], // Verse 1 - building energy ['tap', 2, 9000], ['tap', 3, 10000], ['tap', 1, 12500], ['tap', 2, 14000], ['tap', 0, 18000], // Chorus 1 - soaring melody ['tap', 0, 22000], ['tap', 3, 24000], ['tap', 2, 30000], // Bridge - melodic interlude ['tap', 1, 35000], ['tap', 2, 36500], ['tap', 0, 38000], ['tap', 0, 45000], // Verse 2 - increased complexity ['tap', 3, 47000], ['tap', 2, 52500], ['tap', 1, 57000], ['tap', 2, 59000], ['tap', 0, 64000], // Chorus 2 - full energy ['tap', 0, 68000], ['tap', 3, 70000], ['tap', 2, 76500], // Final section - climactic ending ['tap', 1, 82000], ['tap', 2, 84000], ['tap', 3, 89500], ['tap', 0, 95000], // Outro - graceful conclusion ['tap', 1, 102000], ['tap', 2, 105000], ['tap', 2, 118000], ['tap', 0, 122000], ['tap', 3, 126000], // Extended ending with cross-lane patterns ['tap', 3, 138000], ['tap', 1, 142000], ['tap', 1, 156000], ['tap', 2, 160000], ['tap', 3, 164000]], Medium: [ // Increased complexity with eighth note patterns and more frequent notes ['tap', 1, 1500], ['tap', 2, 2250], ['tap', 0, 3000], ['tap', 3, 3750], ['hold', 1, 4500, 1500], ['tap', 2, 6750], ['tap', 3, 7500], ['swipe', 0, 8250, 0, 'left'], ['swipe', 1, 8250, 0, 'left'], ['tap', 2, 9000], ['tap', 0, 10500], ['hold', 3, 12000, 1200], ['tap', 1, 14250], ['tap', 2, 15750], ['swipe', 0, 17250, 0, 'right'], ['swipe', 3, 17250, 0, 'right'], ['tap', 1, 18750], ['tap', 2, 20250], ['hold', 0, 21750, 2000], ['tap', 3, 24750], ['swipe', 1, 26250, 0, 'up'], ['swipe', 2, 26250, 0, 'up'], ['tap', 0, 28500], ['tap', 3, 30000], ['hold', 1, 31500, 1800], ['tap', 2, 34500], ['tap', 0, 36000], ['swipe', 3, 37500, 0, 'down'], ['swipe', 1, 37500, 0, 'down'], ['tap', 2, 39000], ['hold', 0, 40500, 1500], ['tap', 3, 43500], ['tap', 1, 45000], ['swipe', 2, 46500, 0, 'left'], ['swipe', 0, 46500, 0, 'left'], ['hold', 3, 48000, 2250], ['tap', 1, 51750], ['tap', 2, 53250], ['tap', 0, 54750], ['swipe', 1, 56250, 0, 'right'], ['swipe', 3, 56250, 0, 'right'], ['hold', 2, 58500, 2000], ['tap', 0, 62250], ['tap', 1, 64500], ['swipe', 3, 66750, 0, 'up'], ['swipe', 2, 66750, 0, 'up'], ['hold', 0, 69000, 2500], ['tap', 1, 73125], ['tap', 3, 75375], ['tap', 2, 77625], ['swipe', 0, 79875, 0, 'down'], ['swipe', 1, 79875, 0, 'down'], ['hold', 3, 82125, 1750], ['tap', 2, 85875], ['tap', 0, 88125], ['swipe', 1, 90375, 0, 'left'], ['swipe', 3, 90375, 0, 'left'], ['hold', 2, 92625, 3000], ['tap', 0, 97125], ['tap', 1, 100125], ['tap', 3, 103125], ['swipe', 2, 106125, 0, 'right'], ['swipe', 0, 106125, 0, 'right'], ['hold', 1, 109125, 2750], ['tap', 3, 113625], ['tap', 2, 117375], ['swipe', 0, 121125, 0, 'up'], ['swipe', 1, 121125, 0, 'up'], ['hold', 3, 124875, 3250], ['tap', 2, 130125], ['tap', 0, 134625], ['tap', 1, 139125], ['swipe', 3, 143625, 0, 'down'], ['swipe', 2, 143625, 0, 'down'], ['hold', 0, 148125, 4000], ['tap', 1, 154125], ['tap', 3, 159375], ['swipe', 2, 164625, 0, 'left'], ['swipe', 0, 164625, 0, 'left'], ['hold', 1, 169875, 4500], ['tap', 3, 176375], ['tap', 2, 180000]], Hard: [ // Complex patterns with sixteenth notes, polyrhythms, and rapid sequences ['tap', 1, 1000], ['tap', 2, 1375], ['tap', 0, 1750], ['tap', 3, 2125], ['tap', 1, 2500], ['hold', 2, 2875, 1000], ['tap', 0, 4250], ['tap', 3, 4625], ['swipe', 1, 5000, 0, 'up'], ['swipe', 2, 5000, 0, 'up'], ['tap', 0, 5375], ['tap', 3, 5750], ['tap', 1, 6125], ['hold', 2, 6500, 875], ['tap', 0, 7750], ['tap', 3, 8125], ['swipe', 1, 8500, 0, 'left'], ['swipe', 0, 8500, 0, 'left'], ['tap', 2, 8875], ['tap', 3, 9250], ['hold', 1, 9625, 750], ['tap', 0, 10750], ['tap', 2, 11125], ['tap', 3, 11500], ['swipe', 1, 11875, 0, 'right'], ['swipe', 3, 11875, 0, 'right'], ['hold', 0, 12250, 1125], ['tap', 2, 13750], ['tap', 1, 14125], ['tap', 3, 14500], ['swipe', 0, 14875, 0, 'down'], ['swipe', 2, 14875, 0, 'down'], ['hold', 1, 15250, 1000], ['tap', 3, 16625], ['tap', 0, 17000], ['tap', 2, 17375], ['swipe', 1, 17750, 0, 'up'], ['swipe', 3, 17750, 0, 'up'], ['hold', 0, 18125, 750], ['tap', 2, 19250], ['tap', 1, 19625], ['tap', 3, 20000], ['swipe', 0, 20375, 0, 'left'], ['swipe', 2, 20375, 0, 'left'], ['hold', 1, 20750, 875], ['tap', 3, 22000], ['tap', 0, 22375], ['tap', 2, 22750], ['swipe', 1, 23125, 0, 'right'], ['swipe', 3, 23125, 0, 'right'], ['hold', 2, 23500, 1125], ['tap', 0, 25000], ['tap', 1, 25375], ['tap', 3, 25750], ['swipe', 2, 26125, 0, 'down'], ['swipe', 0, 26125, 0, 'down'], ['hold', 1, 26500, 1000], ['hold', 3, 27000, 1000], ['tap', 2, 28375], ['tap', 0, 28750], ['swipe', 1, 29125, 0, 'up'], ['swipe', 3, 29125, 0, 'up'], ['hold', 2, 29500, 750], ['tap', 0, 30625], // Continue with increasingly complex patterns for full song duration ['tap', 1, 31000], ['tap', 3, 31375], ['swipe', 2, 31750, 0, 'left'], ['swipe', 0, 31750, 0, 'left'], ['hold', 1, 32125, 1250], ['tap', 3, 33750], ['tap', 2, 34125], ['tap', 0, 34500], ['swipe', 1, 34875, 0, 'right'], ['swipe', 3, 34875, 0, 'right'], ['hold', 2, 35250, 1000], // Add rapid sequences and complex cross-rhythms continuing to full song length... // (Pattern continues with similar complexity for remaining duration) ['tap', 0, 175000], ['tap', 1, 176000], ['tap', 2, 177000], ['hold', 3, 178000, 2000]] } }, { 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'], // Continue with medium complexity patterns for full duration... // (Detailed patterns would continue here for the full 195 seconds) ['tap', 0, 193000], ['tap', 1, 194000], ['hold', 2, 195000, 1000]], Hard: [ // Complex electronic patterns with fills, drops and rapid sequences ['tap', 0, 1000], ['tap', 2, 1300], ['tap', 1, 1600], ['tap', 3, 1900], ['hold', 0, 2200, 800], ['tap', 2, 3300], ['tap', 1, 3600], ['swipe', 3, 3900, 0, 'right'], ['swipe', 0, 3900, 0, 'right'], // Continue with high complexity patterns for full duration... // (Detailed patterns would continue here for the full 195 seconds) ['tap', 0, 193500], ['tap', 1, 194250], ['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'], // Continue with medium waltz complexity for full duration... // (Detailed patterns would continue here for the full 210 seconds) ['tap', 1, 208000], ['tap', 2, 209000], ['hold', 0, 210000, 1000]], Hard: [ // Complex waltz with rapid arpeggios, cross-rhythms and ornamental passages ['tap', 1, 1000], ['tap', 2, 1500], ['tap', 3, 2000], ['tap', 0, 2500], ['hold', 1, 3000, 1250], ['tap', 2, 4750], ['tap', 3, 5250], ['swipe', 0, 5750, 0, 'right'], ['swipe', 3, 5750, 0, 'right'], // Continue with high complexity waltz patterns for full duration... // (Detailed patterns would continue here for the full 210 seconds) ['tap', 1, 208500], ['tap', 2, 209250], ['tap', 0, 210000]] } }]; // --- GUI ELEMENTS --- var gui = LK.gui; var languageButtons = []; var startButton = null; var songCards = []; var storyText = null; var continueButton = null; var difficultyButtons = []; var scoreText = null; var comboText = null; var accuracyText = null; var resultText = null; var homeButton = null; // --- GAME ELEMENTS --- var lanes = []; var laneDividers = []; var hitLine = null; // --- UTILS --- function clearGUI() { // Remove all children from gui overlays, but only if they exist if (gui.top && gui.top.removeChildren) { gui.top.removeChildren(); } if (gui.topRight && gui.topRight.removeChildren) { gui.topRight.removeChildren(); } if (gui.topLeft && gui.topLeft.removeChildren) { gui.topLeft.removeChildren(); } if (gui.left && gui.left.removeChildren) { gui.left.removeChildren(); } if (gui.right && gui.right.removeChildren) { gui.right.removeChildren(); } if (gui.bottom && gui.bottom.removeChildren) { gui.bottom.removeChildren(); } if (gui.bottomLeft && gui.bottomLeft.removeChildren) { gui.bottomLeft.removeChildren(); } if (gui.bottomRight && gui.bottomRight.removeChildren) { gui.bottomRight.removeChildren(); } if (gui.center && gui.center.removeChildren) { gui.center.removeChildren(); } // Reset GUI-related global state languageButtons = []; startButton = null; songCards = []; storyText = null; continueButton = null; difficultyButtons = []; scoreText = null; comboText = null; accuracyText = null; resultText = null; homeButton = null; } function clearGameObjects() { // Remove all notes, lanes, etc. for (var i = 0; i < lanes.length; ++i) { if (lanes[i] && lanes[i].parent) { lanes[i].parent.removeChild(lanes[i]); } } for (var i = 0; i < laneDividers.length; ++i) { if (laneDividers[i] && laneDividers[i].parent) { laneDividers[i].parent.removeChild(laneDividers[i]); } } if (hitLine && hitLine.parent) { hitLine.parent.removeChild(hitLine); } for (var i = 0; i < currentNotes.length; ++i) { if (currentNotes[i] && currentNotes[i].parent) { currentNotes[i].parent.removeChild(currentNotes[i]); } } lanes = []; laneDividers = []; hitLine = null; currentNotes = []; noteIndex = 0; holdActive = null; swipeStart = null; swipeNote = null; isPlaying = false; } function setGameState(state) { GAME_STATE = state; // 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() { clearGUI(); clearGameObjects(); songCards = []; var screenWidth = LK.width || 2048; var screenHeight = LK.height || 2732; // Create homeScreen container before adding children var homeScreen = new Container(); // Add Backgroundhomepage asset as background, fully fit to screen var bgAsset = LK.getAsset('Backgroundhomepage', { anchorX: 0.5, anchorY: 0.5, x: 0, y: 0, scaleX: screenWidth / 2048, scaleY: screenHeight / 2732 }); homeScreen.addChild(bgAsset); bgAsset.width = screenWidth; bgAsset.height = screenHeight; // Title: "Select a Song", in white with proper positioning var title = new Text2(selectedLanguage === 'zh' ? "选择歌曲" : "Select a Song", { size: 90, fill: "#fff", // White text for contrast font: "Arial Black" }); title.anchor.set(0.5, 0); // Place title at 15% of screen height, centered horizontally, and ensure it's not above y=120 // For gui.center containers, (0,0) is the center, so offset from center title.x = 0; title.y = -screenHeight / 2 + Math.max(120, Math.floor(screenHeight * 0.15)); homeScreen.addChild(title); // Card layout - vertical stacking with proper spacing var cardCount = SONGS.length; var cardHeight = 200; var cardSpacing = 80; var titleSpacing = 100; // Place the title at 15% of screen height, and cards start at 25% of screen height // We want all cards to be below the title and fully within SelectorUI // Calculate the starting Y so that the first card is just below the title, and all cards fit on screen var cardsStartY = title.y + title.height + titleSpacing; // 100px gap below title for (var i = 0; i < SONGS.length; i++) { (function (index) { var song = SONGS[index]; // Card Y position var cardY = cardsStartY + index * (cardHeight + cardSpacing); // Create SelectorUI background for song card var cardBg = LK.getAsset('SelectorUI', { anchorX: 0.5, anchorY: 0.5, x: 0, y: cardY, scaleX: 4.0, scaleY: 2.0 }); homeScreen.addChild(cardBg); // Make card interactive cardBg.interactive = true; cardBg.down = function (x, y, obj) { LK.getSound('Touch').play(); selectedSong = song; setGameState('STORY'); }; // Add song title (centered within SelectorUI) var songNameText = new Text2(song.title, { size: 64, fill: "#fff", font: "Arial" }); songNameText.anchor.set(0.5, 0.5); songNameText.x = 0; songNameText.y = cardY - 30; homeScreen.addChild(songNameText); // Add subtitle (optional: show "Sky Journey", "Night Pulse", "Sunrise Waltz") var subtitleText = new Text2(selectedLanguage === 'zh' && song.title === "Sky Journey" ? "天空之旅" : selectedLanguage === 'zh' && song.title === "Night Pulse" ? "夜之律动" : selectedLanguage === 'zh' && song.title === "Sunrise Waltz" ? "日出圆舞曲" : "", { size: 40, fill: 0xE0E0E0, font: "Arial" }); subtitleText.anchor.set(0.5, 0.5); subtitleText.x = 0; subtitleText.y = cardY + 30; homeScreen.addChild(subtitleText); songCards.push(cardBg); })(i); } // Add homeScreen to GUI, centered homeScreen.x = 0; homeScreen.y = 0; if (gui.center && gui.center.addChild) { gui.center.addChild(homeScreen); } // Play background music for homepage LK.playMusic('KaleidoscopeMusicRhythm'); } // --- STORY INTRO --- function showStory() { clearGUI(); var story = selectedSong.story[selectedLanguage] || selectedSong.story['en']; var screenWidth = LK.width || 2048; var screenHeight = LK.height || 2732; // Create SongStorybox container var storyBox = new Container(); // Add SongStorybox asset as background, centered and scaled up for showcase var boxAsset = LK.getAsset('SongStorybox', { anchorX: 0.5, anchorY: 0.5, x: 0, y: 0, scaleX: 13, // Increased for larger showcase scaleY: 11 }); storyBox.addChild(boxAsset); // Calculate box bounds for layout var boxW = boxAsset.width * boxAsset.scaleX; var boxH = boxAsset.height * boxAsset.scaleY; var boxLeftX = -boxW / 2; var boxRightX = boxW / 2; var boxTopY = -boxH / 2; var boxBottomY = boxH / 2; // Add smaller song cover on the left side of the box var coverAsset = LK.getAsset(selectedSong.cover, { anchorX: 0.5, anchorY: 0.5, x: boxLeftX + boxW * 0.25, // Position at 25% from left edge y: boxTopY + boxH * 0.35, // Position at 35% from top scaleX: 1.2, // Smaller cover scaleY: 1.2 }); storyBox.addChild(coverAsset); // Add story text on the larger right side of the box storyText = new Text2(story, { size: 55, fill: "#fff", wordWrap: true, wordWrapWidth: boxW * 0.45 // Use 45% of box width for text }); storyText.anchor.set(0, 0.5); storyText.x = boxLeftX + boxW * 0.52; // Start at 52% from left edge storyText.y = boxTopY + boxH * 0.35; // Align vertically with cover storyBox.addChild(storyText); // Create 3 separate SelectorUI elements for difficulty buttons arranged horizontally var diffY = boxBottomY - boxH * 0.15; // Y position for all difficulty buttons var totalWidth = boxW * 0.8; // Use 80% of box width for all buttons var buttonWidth = totalWidth / selectedSong.difficulties.length; var startX = -totalWidth / 2 + buttonWidth / 2; // Center the button group difficultyButtons = []; for (var i = 0; i < selectedSong.difficulties.length; ++i) { (function (idx) { var label = selectedSong.difficulties[idx]; // Create individual SelectorUI for this difficulty button var selectorAsset = LK.getAsset('SelectorUI', { anchorX: 0.5, anchorY: 0.5, x: startX + idx * buttonWidth, y: diffY, scaleX: 2.5, scaleY: 1.8 }); selectorAsset.interactive = true; selectorAsset.down = function (x, y, obj) { LK.getSound('Touch').play(); selectedDifficulty = label; setGameState('PLAY'); }; storyBox.addChild(selectorAsset); // Add difficulty button text centered on the SelectorUI var btn = new Text2(label, { size: 60, fill: "#fff" }); btn.anchor.set(0.5, 0.5); btn.x = startX + idx * buttonWidth; btn.y = diffY; storyBox.addChild(btn); difficultyButtons.push(btn); })(i); } // Start storyBox at bottom-right corner of screen storyBox.x = screenWidth; storyBox.y = screenHeight; gui.center.addChild(storyBox); // Animate storyBox to center of screen tween(storyBox, { x: 0, y: 0 }, { duration: 800, easing: tween.easeOut }); } // --- DIFFICULTY SELECT --- function showDifficulty() { difficultyButtons = []; var screenWidth = LK.width || 2048; var screenHeight = LK.height || 2732; var title = new Text2(selectedLanguage === 'zh' ? "选择难度" : "Select Difficulty", { size: 100, fill: "#fff" }); title.anchor.set(0.5, 0); title.x = screenWidth / 2; title.y = Math.max(100, Math.floor(screenHeight * 0.08)); gui.top.addChild(title); var btnYStart = Math.floor(screenHeight * 0.40); var btnSpacing = Math.floor(screenHeight * 0.13); for (var i = 0; i < selectedSong.difficulties.length; ++i) { (function (idx) { var label = selectedSong.difficulties[idx]; var btn = new Text2(label, { size: 90, fill: "#fff" }); btn.anchor.set(0.5, 0.5); btn.x = screenWidth / 2; btn.y = btnYStart + idx * btnSpacing; btn.interactive = true; btn.down = function (x, y, obj) { LK.getSound('Touch').play(); selectedDifficulty = label; setGameState('PLAY'); }; gui.center.addChild(btn); difficultyButtons.push(btn); })(i); } } // --- GAMEPLAY --- function startGameplay() { // Setup 4 lanes vertically side by side lanes = []; laneDividers = []; var laneSpacing = 2048 / LANE_COUNT; // Equal spacing for 4 lanes for (var i = 0; i < LANE_COUNT; ++i) { var lane = LK.getAsset('lane', { anchorX: 0.5, anchorY: 0 }); lane.x = (i + 0.5) * laneSpacing; // Center each lane in its section lane.y = 0; lane.scaleX = laneSpacing / lane.width; // Scale to fit lane width lane.scaleY = 2732 / lane.height; // Scale to full screen height lanes.push(lane); game.addChild(lane); } // Create lane dividers as hit areas at the bottom of lanes for (var i = 0; i < LANE_COUNT; ++i) { var divider = LK.getAsset('laneDivider', { anchorX: 0.5, anchorY: 1 }); divider.x = (i + 0.5) * laneSpacing; // Center in each lane divider.y = HIT_LINE_Y + 100; // Position at bottom as hit area divider.scaleX = laneSpacing * 0.8 / divider.width; // Scale to 80% of lane width divider.scaleY = 0.3; // Make it thinner for hit area laneDividers.push(divider); game.addChild(divider); } // Hit line (visual indicator) hitLine = LK.getAsset('laneDivider', { anchorX: 0, anchorY: 0.5 }); hitLine.width = 2048; hitLine.height = 8; hitLine.x = 0; hitLine.y = HIT_LINE_Y; hitLine.alpha = 0.7; // Make it semi-transparent game.addChild(hitLine); // Load notes currentSongData = selectedSong.notes[selectedDifficulty]; currentNotes = []; noteIndex = 0; score = 0; combo = 0; maxCombo = 0; accuracy = 0; totalNotes = currentSongData.length; hitNotes = 0; missNotes = 0; badNotes = 0; goodNotes = 0; greatNotes = 0; perfectNotes = 0; scorePopups = []; holdActive = null; swipeStart = null; swipeNote = null; isPlaying = true; for (var i = 0; i < currentSongData.length; ++i) { var n = currentSongData[i]; var note = new Note(); note.init(n[0], n[1], n[2], n[3], n[4]); // Initial position: y far above screen, will be updated in update loop var laneSpacing = 2048 / LANE_COUNT; note.x = (n[1] + 0.5) * laneSpacing; // Center in the lane note.y = -200; currentNotes.push(note); game.addChild(note); } // Create scoreboard container with SelectorUI background var scoreboardBg = LK.getAsset('SelectorUI', { anchorX: 0.5, anchorY: 0, x: 0, y: 0, scaleX: 8.0, scaleY: 2.0 }); gui.top.addChild(scoreboardBg); // Score/Combo/Accuracy GUI positioned on scoreboard scoreText = new Text2("Score: 0", { size: 70, fill: "#fff" }); scoreText.anchor.set(0.5, 0); scoreText.x = 0; scoreText.y = 20; gui.top.addChild(scoreText); comboText = new Text2("Combo: 0", { size: 70, fill: "#fff" }); comboText.anchor.set(0.5, 0); comboText.x = 0; comboText.y = 70; gui.top.addChild(comboText); accuracyText = new Text2("Accuracy: 100%", { size: 70, fill: "#fff" }); accuracyText.anchor.set(0.5, 0); accuracyText.x = 0; accuracyText.y = 120; gui.top.addChild(accuracyText); // Start music - play full song for each gameplay without looping LK.playMusic(selectedSong.id, { loop: false }); // Start timer startTime = Date.now(); } // --- GAMEPLAY LOGIC --- function getCurrentTime() { return Date.now() - startTime; } function getNoteY(noteTime) { // Notes fall from y = -200 to HIT_LINE_Y at the time they should be hit // We'll use a fixed speed so that notes reach HIT_LINE_Y at noteTime var speed = (HIT_LINE_Y + 200) / 2000; // 2 seconds to fall var t = getCurrentTime(); var dt = noteTime - t; return HIT_LINE_Y - dt * speed; } function getHoldTrailLength(note, t) { // For hold notes, trail from note head to end time var endTime = note.time + note.duration; var y1 = getNoteY(note.time); var y2 = getNoteY(endTime); return Math.max(0, y2 - y1); } function judgeNote(note, lane, y, eventType, swipeDir) { if (!note.active || note.hit) { return false; } var noteY = getNoteY(note.time); var t = getCurrentTime(); var dt = Math.abs(note.time - t); var scoreType = ""; var scoreValue = 0; // Determine hit quality based on timing if (dt <= PERFECT_WINDOW) { scoreType = "perfect"; scoreValue = 150; perfectNotes += 1; } else if (dt <= GREAT_WINDOW) { scoreType = "great"; scoreValue = 120; greatNotes += 1; } else if (dt <= GOOD_WINDOW) { scoreType = "good"; scoreValue = 80; goodNotes += 1; } else if (dt <= BAD_WINDOW) { scoreType = "bad"; scoreValue = 40; badNotes += 1; } else { return false; // Miss } if (note.type === 'tap') { if (lane === note.lane && Math.abs(y - HIT_LINE_Y) < 180 && dt < BAD_WINDOW) { note.hit = true; note.active = false; note.flash(); // Create sparkling effect on the note itself tween(note, { scaleX: note.noteAsset.scaleX * 1.3, scaleY: note.noteAsset.scaleY * 1.3, rotation: 0.2 }, { duration: 100, easing: tween.easeOut, onFinish: function onFinish() { tween(note, { scaleX: note.noteAsset.scaleX / 1.3, scaleY: note.noteAsset.scaleY / 1.3, rotation: 0 }, { duration: 100, easing: tween.easeIn }); } }); // Immediately remove from currentNotes array to prevent flow issues var tapIndex = currentNotes.indexOf(note); if (tapIndex > -1) { currentNotes.splice(tapIndex, 1); } // Make note disappear immediately after touch with burst animation tween(note, { alpha: 0, scaleX: 1.5, scaleY: 1.5 }, { duration: 150, easing: tween.easeOut, onFinish: function onFinish() { if (note.parent) { note.parent.removeChild(note); } } }); LK.getSound('tap').play(); score += scoreValue; combo += 1; hitNotes += 1; maxCombo = Math.max(combo, maxCombo); // Light up hit area var hitArea = laneDividers[lane]; if (hitArea) { // Store original tint if not already stored if (hitArea.originalTint === undefined) { hitArea.originalTint = hitArea.tint || 0xFFFFFF; } // Light up with golden glow hitArea.tint = 0xFFD700; hitArea.alpha = 1.0; // Store original scale if not already stored if (hitArea.originalScaleX === undefined) { hitArea.originalScaleX = hitArea.scaleX; hitArea.originalScaleY = hitArea.scaleY; } // Create sparkling effect with scaling animation tween(hitArea, { scaleX: hitArea.originalScaleX * 1.3, scaleY: hitArea.originalScaleY * 1.3 }, { duration: 150, easing: tween.easeOut, onFinish: function onFinish() { tween(hitArea, { scaleX: hitArea.originalScaleX, scaleY: hitArea.originalScaleY }, { duration: 150, easing: tween.easeIn }); } }); // Fade back to normal after short duration tween(hitArea, { alpha: 0.7 }, { duration: 200, onFinish: function onFinish() { hitArea.tint = hitArea.originalTint; } }); } // Show score popup var popup = new ScorePopup(); popup.init(scoreType, note.x, note.y); game.addChild(popup); scorePopups.push(popup); return true; } } else if (note.type === 'hold') { // Very sensitive hold detection - allow wide area and multiple lanes var validLanes = [note.lane]; if (note.lane > 0) validLanes.push(note.lane - 1); if (note.lane < LANE_COUNT - 1) validLanes.push(note.lane + 1); // Allow even wider detection for better UX 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; if (eventType === 'down' && isValidLane && Math.abs(y - HIT_LINE_Y) < 500 && dt < BAD_WINDOW + 200) { holdActive = { note: note, start: t, scoreType: scoreType, scoreValue: scoreValue }; note.flash(); // Start continuous sparkling animation on hit area during hold var hitArea = laneDividers[note.lane]; if (hitArea) { // Create pulsing sparkling effect var _createSparkle = function createSparkle() { if (holdActive && holdActive.note === note) { hitArea.tint = 0xFFD700; // Golden sparkle tween(hitArea, { scaleX: hitArea.originalScaleX * 1.2, scaleY: hitArea.originalScaleY * 1.2, alpha: 1.0 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { tween(hitArea, { scaleX: hitArea.originalScaleX, scaleY: hitArea.originalScaleY, alpha: 0.8 }, { duration: 200, easing: tween.easeIn, onFinish: function onFinish() { // Continue sparkling if still holding if (holdActive && holdActive.note === note) { LK.setTimeout(_createSparkle, 100); } else { // Reset hit area when hold ends hitArea.tint = hitArea.originalTint; hitArea.scaleX = hitArea.originalScaleX; hitArea.scaleY = hitArea.originalScaleY; hitArea.alpha = 0.7; } } }); } }); } }; // Store original properties if (hitArea.originalTint === undefined) { hitArea.originalTint = hitArea.tint || 0xFFFFFF; } if (hitArea.originalScaleX === undefined) { hitArea.originalScaleX = hitArea.scaleX; hitArea.originalScaleY = hitArea.scaleY; } _createSparkle(); } LK.getSound('hold').play(); return true; } } else if (note.type === 'swipe') { // Check if swipe affects multiple lanes - enhanced detection for bigger swipe notes var validLanes = [note.lane]; var laneSpacing = 2048 / LANE_COUNT; var swipeWidth = note.noteAsset.width * note.noteAsset.scaleX; // Enhanced cross-lane detection for bigger swipe notes if (swipeWidth > laneSpacing * 1.8) { // More generous threshold // Add adjacent lanes for cross-lane swipes if (note.lane > 0) validLanes.push(note.lane - 1); if (note.lane < LANE_COUNT - 1) validLanes.push(note.lane + 1); // For very wide swipes, add one more lane on each side if (swipeWidth > laneSpacing * 2.5) { if (note.lane > 1) validLanes.push(note.lane - 2); if (note.lane < LANE_COUNT - 2) validLanes.push(note.lane + 2); } } var isValidLane = validLanes.indexOf(lane) !== -1; // Improved sensitivity - increased distance threshold and relaxed timing for better swipe detection if (eventType === 'swipe' && isValidLane && Math.abs(y - HIT_LINE_Y) < 400 && dt < BAD_WINDOW + 150 && swipeDir === note.swipeDir) { note.hit = true; note.active = false; note.flash(); // Make swipe note disappear immediately after touch with directional slide animation var slideDir = note.swipeDir; var targetX = note.x; var targetY = note.y; var slideDistance = 400; // Increased slide distance if (slideDir === 'left') targetX -= slideDistance;else if (slideDir === 'right') targetX += slideDistance;else if (slideDir === 'up') targetY -= slideDistance;else if (slideDir === 'down') targetY += slideDistance; // Store reference to note for proper cleanup and prevent red tinting var noteToRemove = note; // Ensure note is removed from currentNotes array immediately to prevent red tinting and flow issues var index = currentNotes.indexOf(noteToRemove); if (index > -1) { currentNotes.splice(index, 1); } // Prevent any color tinting by ensuring clean removal noteToRemove.tint = 0xFFFFFF; noteToRemove.alpha = 0; // Make invisible immediately // Remove from parent immediately to prevent any visual artifacts if (noteToRemove.parent) { noteToRemove.parent.removeChild(noteToRemove); } // Optional visual effect for feedback var effectNote = LK.getAsset('swipeNote', { anchorX: 0.5, anchorY: 0.5, x: note.x, y: note.y, scaleX: note.noteAsset.scaleX, scaleY: note.noteAsset.scaleY }); game.addChild(effectNote); tween(effectNote, { x: targetX, y: targetY, alpha: 0, scaleX: note.noteAsset.scaleX * 0.3, scaleY: note.noteAsset.scaleY * 0.3 }, { duration: 150, // Faster animation for better responsiveness easing: tween.easeOut, onFinish: function onFinish() { if (effectNote && effectNote.parent) { effectNote.parent.removeChild(effectNote); } } }); LK.getSound('swipe').play(); score += scoreValue + 50; // Bonus for swipe notes combo += 1; hitNotes += 1; maxCombo = Math.max(combo, maxCombo); // Light up hit areas for swipe notes (can affect multiple lanes) for (var v = 0; v < validLanes.length; v++) { var hitArea = laneDividers[validLanes[v]]; if (hitArea) { // Store original tint if not already stored if (hitArea.originalTint === undefined) { hitArea.originalTint = hitArea.tint || 0xFFFFFF; } // Light up with cyan glow for swipe hitArea.tint = 0x00FFFF; hitArea.alpha = 1.0; // Create sparkling wave effect with scaling animation var delay = v * 50; tween(hitArea, { scaleX: hitArea.scaleX * 1.4, scaleY: hitArea.scaleY * 1.4 }, { duration: 200, delay: delay, easing: tween.easeOut, onFinish: function onFinish() { tween(hitArea, { scaleX: hitArea.scaleX / 1.4, scaleY: hitArea.scaleY / 1.4 }, { duration: 200, easing: tween.easeIn }); } }); // Stagger the fade timing for visual wave effect tween(hitArea, { alpha: 0.7 }, { duration: 250, delay: delay, onFinish: function onFinish() { hitArea.tint = hitArea.originalTint; } }); } } // Show score popup var popup = new ScorePopup(); popup.init(scoreType, note.x, note.y); game.addChild(popup); scorePopups.push(popup); return true; } } return false; } function endHold() { if (holdActive && holdActive.note && !holdActive.note.hit) { var t = getCurrentTime(); var note = holdActive.note; var holdEnd = note.time + note.duration; // Only complete hold if duration has been fully held AND player is still holding if (t >= holdEnd) { note.hit = true; note.active = false; // Immediately remove from currentNotes array to prevent flow issues var holdIndex = currentNotes.indexOf(note); if (holdIndex > -1) { currentNotes.splice(holdIndex, 1); } // Make hold note disappear slowly with longer satisfying animation tween(note, { alpha: 0, scaleX: 0.1, scaleY: 0.1, tint: 0x00FF00 // Green glow for successful hold completion }, { duration: 800, //{8j} // Much longer fade easing: tween.easeIn, onFinish: function onFinish() { if (note.parent) { note.parent.removeChild(note); } } }); score += holdActive.scoreValue + 50; // Bonus for completing hold combo += 1; hitNotes += 1; maxCombo = Math.max(combo, maxCombo); // Light up hit area for completed hold var lane = note.lane; var hitArea = laneDividers[lane]; if (hitArea) { // Store original tint if not already stored if (hitArea.originalTint === undefined) { hitArea.originalTint = hitArea.tint || 0xFFFFFF; } // Light up with green glow for hold completion hitArea.tint = 0x00FF00; hitArea.alpha = 1.0; // Create sparkling effect with pulsing animation for hold completion tween(hitArea, { scaleX: hitArea.scaleX * 1.5, scaleY: hitArea.scaleY * 1.5 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { tween(hitArea, { scaleX: hitArea.scaleX / 1.5, scaleY: hitArea.scaleY / 1.5 }, { duration: 200, easing: tween.easeIn }); } }); // Fade back to normal after short duration tween(hitArea, { alpha: 0.7 }, { duration: 300, onFinish: function onFinish() { hitArea.tint = hitArea.originalTint; } }); } // Show score popup var popup = new ScorePopup(); popup.init(holdActive.scoreType, note.x, note.y); game.addChild(popup); scorePopups.push(popup); holdActive = null; } } } // --- INPUT HANDLING --- var dragLane = null; var dragY = null; var dragNote = null; var dragStartX = null; var dragStartY = null; var dragStartTime = null; function getLaneFromX(x) { var laneSpacing = 2048 / LANE_COUNT; for (var i = 0; i < LANE_COUNT; ++i) { var laneStart = i * laneSpacing; var laneEnd = (i + 1) * laneSpacing; if (x >= laneStart && x < laneEnd) { return i; } } return -1; } function handleGameDown(x, y, obj) { if (GAME_STATE !== 'PLAY' || !isPlaying) { return; } var lane = getLaneFromX(x); if (lane === -1) { return; } dragLane = lane; dragY = y; dragStartX = x; dragStartY = y; dragStartTime = Date.now(); // Check for tap/hold notes for (var i = 0; i < currentNotes.length; ++i) { var note = currentNotes[i]; if (!note.active || note.hit) { continue; } if (note.type === 'tap' || note.type === 'hold') { if (judgeNote(note, lane, y, 'down')) { if (note.type === 'tap') { note.hit = true; note.active = false; } if (note.type === 'hold') { dragNote = note; } break; } } } } function handleGameUp(x, y, obj) { if (GAME_STATE !== 'PLAY' || !isPlaying) { return; } if (holdActive && holdActive.note) { var t = getCurrentTime(); var note = holdActive.note; var holdEnd = note.time + note.duration; // Check if hold was completed (reached full duration) if (t >= holdEnd) { endHold(); } else { // Released too early - but if the button detected the hold note with sparkling light, count as good/great/perfect based on timing // Use the timing at release to judge var dt = Math.abs(note.time - t); var scoreType = ""; var scoreValue = 0; if (dt <= PERFECT_WINDOW) { scoreType = "perfect"; scoreValue = 150; perfectNotes += 1; } else if (dt <= GREAT_WINDOW) { scoreType = "great"; scoreValue = 120; greatNotes += 1; } else if (dt <= GOOD_WINDOW) { scoreType = "good"; scoreValue = 80; goodNotes += 1; } else if (dt <= BAD_WINDOW) { scoreType = "bad"; scoreValue = 40; badNotes += 1; } else { // If not within any window, count as miss note.hit = false; note.active = false; // Make failed hold note fade out with red tint tween(note, { alpha: 0.3, tint: 0xFF0000 }, { duration: 400, easing: tween.easeOut, onFinish: function onFinish() { if (note.parent) { note.parent.removeChild(note); } } }); combo = 0; missNotes += 1; // Show miss popup var popup = new ScorePopup(); popup.init("miss", note.x, note.y); game.addChild(popup); scorePopups.push(popup); holdActive = null; return; } // If we get here, it's a good/great/perfect/bad (not miss) note.hit = true; note.active = false; // Remove from currentNotes array var holdIndex = currentNotes.indexOf(note); if (holdIndex > -1) { currentNotes.splice(holdIndex, 1); } // Make hold note disappear with a fade animation tween(note, { alpha: 0, scaleX: 0.1, scaleY: 0.1, tint: 0xFFD700 // Gold for partial hold }, { duration: 500, easing: tween.easeIn, onFinish: function onFinish() { if (note.parent) { note.parent.removeChild(note); } } }); score += scoreValue; combo += 1; hitNotes += 1; maxCombo = Math.max(combo, maxCombo); // Light up hit area for partial hold var lane = note.lane; var hitArea = laneDividers[lane]; if (hitArea) { if (hitArea.originalTint === undefined) { hitArea.originalTint = hitArea.tint || 0xFFFFFF; } hitArea.tint = 0xFFD700; hitArea.alpha = 1.0; tween(hitArea, { scaleX: hitArea.scaleX * 1.2, scaleY: hitArea.scaleY * 1.2 }, { duration: 150, easing: tween.easeOut, onFinish: function onFinish() { tween(hitArea, { scaleX: hitArea.scaleX / 1.2, scaleY: hitArea.scaleY / 1.2 }, { duration: 150, easing: tween.easeIn }); } }); tween(hitArea, { alpha: 0.7 }, { duration: 200, onFinish: function onFinish() { hitArea.tint = hitArea.originalTint; } }); } // Show score popup var popup = new ScorePopup(); popup.init(scoreType, note.x, note.y); game.addChild(popup); scorePopups.push(popup); holdActive = null; } } dragLane = null; dragNote = null; dragStartX = null; dragStartY = null; dragStartTime = null; } function handleGameMove(x, y, obj) { if (GAME_STATE !== 'PLAY' || !isPlaying) { return; } // For hold notes, check if finger is still on lane - must maintain contact until completion if (holdActive && dragNote) { var t = getCurrentTime(); var note = holdActive.note; var holdEnd = note.time + note.duration; var lane = getLaneFromX(x); // Very lenient validation - allow very wide movement for excellent UX var validLanes = [dragNote.lane]; // Allow movement to adjacent lanes and even further for better UX if (dragNote.lane > 0) validLanes.push(dragNote.lane - 1); if (dragNote.lane < LANE_COUNT - 1) validLanes.push(dragNote.lane + 1); // Allow even wider movement for very lenient hold detection if (dragNote.lane > 1) validLanes.push(dragNote.lane - 2); if (dragNote.lane < LANE_COUNT - 2) validLanes.push(dragNote.lane + 2); // Allow full screen width for maximum sensitivity var isValidLane = true; // Always allow movement anywhere for holds if (Math.abs(y - HIT_LINE_Y) > 600) { // 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 = 60; // Reduced from 80 for better sensitivity var swipeDistance = Math.sqrt(dx * dx + dy * dy); if (swipeDistance < minSwipeDistance) { return; } // Not enough movement // Only allow horizontal swipes - reject any vertical movement var dir = null; // Only detect horizontal swipes, ignore vertical swipes completely if (absDx > absDy && absDx > 40) { // Horizontal swipe only dir = dx > 0 ? 'right' : 'left'; } else { // Not a valid horizontal 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) > 60 || Math.abs(dy) > 60) { 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; } // Update position only if note has valid timing and hasn't been processed if (note.time > 0 && !note.hit) { var laneSpacing = 2048 / LANE_COUNT; note.x = (note.lane + 0.5) * laneSpacing; // Center in the lane var newY = getNoteY(note.time); // Only update Y if it's a reasonable position to prevent sudden jumps if (newY > -500 && newY < 3500) { note.y = newY; } } // 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; note.hit = true; // Mark as hit to prevent further processing // Immediately remove from currentNotes array to prevent flow issues var missedIndex = currentNotes.indexOf(note); if (missedIndex > -1) { currentNotes.splice(missedIndex, 1); i--; // Adjust index since we removed an element } // 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; note.hit = true; // Mark as hit to prevent further processing // Immediately remove from currentNotes array var holdMissedIndex = currentNotes.indexOf(note); if (holdMissedIndex > -1) { currentNotes.splice(holdMissedIndex, 1); i--; // Adjust index since we removed an element } // 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]; if (note.y > 3000 || !note.active && note.y > HIT_LINE_Y + 400 || note.hit) { if (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; // 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 } // --- RESULT SCREEN --- function showResult() { clearGameObjects(); 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; resultText = new Text2(resultStr, { size: 50, fill: "#fff", align: "left", wordWrap: true, wordWrapWidth: boxW * 0.4 }); resultText.anchor.set(0, 0.5); resultText.x = boxW * 0.05; resultText.y = -boxH * 0.1; resultBox.addChild(resultText); // Exit button to return to song selection var exitButton = LK.getAsset('SelectorUI', { anchorX: 0.5, anchorY: 0.5, x: 0, y: boxH * 0.35, scaleX: 3.0, scaleY: 1.5 }); exitButton.interactive = true; exitButton.down = function (x, y, obj) { LK.getSound('Touch').play(); setGameState('HOME'); }; resultBox.addChild(exitButton); // Exit button text var exitText = new Text2(selectedLanguage === 'zh' ? "退出" : "Exit", { size: 70, fill: "#fff" }); exitText.anchor.set(0.5, 0.5); exitText.x = 0; exitText.y = boxH * 0.35; resultBox.addChild(exitText); // Add result box to center of screen gui.center.addChild(resultBox); } // --- START GAME --- // Start at language selector (opening screen) setGameState('LANGUAGE');
===================================================================
--- original.js
+++ change.js
@@ -1415,29 +1415,120 @@
// Check if hold was completed (reached full duration)
if (t >= holdEnd) {
endHold();
} else {
- // Released too early - fail the hold note
- note.hit = false;
+ // 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;
- // Make failed hold note fade out with red tint
+ // 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.3,
- tint: 0xFF0000
+ alpha: 0,
+ scaleX: 0.1,
+ scaleY: 0.1,
+ tint: 0xFFD700 // Gold for partial hold
}, {
- duration: 400,
- easing: tween.easeOut,
+ duration: 500,
+ easing: tween.easeIn,
onFinish: function onFinish() {
if (note.parent) {
note.parent.removeChild(note);
}
}
});
- combo = 0;
- missNotes += 1;
- // Show miss popup
+ score += scoreValue;
+ combo += 1;
+ hitNotes += 1;
+ maxCombo = Math.max(combo, maxCombo);
+ // Light up hit area for partial hold
+ var lane = note.lane;
+ var hitArea = laneDividers[lane];
+ if (hitArea) {
+ if (hitArea.originalTint === undefined) {
+ hitArea.originalTint = hitArea.tint || 0xFFFFFF;
+ }
+ hitArea.tint = 0xFFD700;
+ hitArea.alpha = 1.0;
+ tween(hitArea, {
+ scaleX: hitArea.scaleX * 1.2,
+ scaleY: hitArea.scaleY * 1.2
+ }, {
+ duration: 150,
+ easing: tween.easeOut,
+ onFinish: function onFinish() {
+ tween(hitArea, {
+ scaleX: hitArea.scaleX / 1.2,
+ scaleY: hitArea.scaleY / 1.2
+ }, {
+ duration: 150,
+ easing: tween.easeIn
+ });
+ }
+ });
+ tween(hitArea, {
+ alpha: 0.7
+ }, {
+ duration: 200,
+ onFinish: function onFinish() {
+ hitArea.tint = hitArea.originalTint;
+ }
+ });
+ }
+ // Show score popup
var popup = new ScorePopup();
- popup.init("miss", note.x, note.y);
+ popup.init(scoreType, note.x, note.y);
game.addChild(popup);
scorePopups.push(popup);
holdActive = null;
}
Game design for Kaleidoscope of Music Rhythm without game title, just the design for opening screen background. In-Game asset. 2d. High contrast. No shadows
Cyberpunk style design empty selector UI. In-Game asset. 2d. High contrast. No shadows
Cyberpunk style hold note for music rhythm game. In-Game asset. 2d. High contrast. No shadows
Cyberpunk style swipe note for music rhythm game. In-Game asset. 2d. High contrast. No shadows
Cyberpunk style tap note for music rhythm game. In-Game asset. 2d. High contrast. No shadows
Cyberpunk style kaleidoscope pattern design for homepage. In-Game asset. 2d. High contrast. No shadows
Cyberpunk style lane divider for music rhythm game that is horizontal and used for judging the touching of rhythm In-Game asset. 2d. High contrast. No shadows
Empty cyberpunk style storybox design in which it's size enable song cover, difficulty level and song description to be added in the storybox. In-Game asset. 2d. High contrast. No shadows
Round shape of song cover of anime style with Sky Journey theme. In-Game asset. 2d. High contrast. No shadows
Round shape song cover of cyberpunk anime style with the themed “Night Pulse”. In-Game asset. 2d. High contrast. No shadows
Round shape of anime style song cover with the themed Sunrise Waltz. In-Game asset. 2d. High contrast. No shadows
Anime style design for the round shape song cover of New Era Malay Style
Round shape, Replace Chinese word from “不知途”to“莫问前程”,others remaining the same
Empty cyberpunk style menu design. In-Game asset. 2d. High contrast. No shadows
Cyberpunk style pause symbol. In-Game asset. 2d. High contrast. No shadows