require('common') local Easing = require('common.easings'); local VolforceCalc = require('components.volforceCalc'); local backgroundImage = gfx.CreateSkinImage("song_select/bg.png", 1) local dataPanelImage = gfx.CreateSkinImage("song_select/data_bg_overlay.png", 1) local dataGlowOverlayImage = gfx.CreateSkinImage("song_select/data_panel/data_glow_overlay.png", 1) local gradeBgImage = gfx.CreateSkinImage("song_select/data_panel/grade_bg.png", 1) local badgeBgImage = gfx.CreateSkinImage("song_select/data_panel/clear_badge_bg.png", 1) local effectedBgImage = gfx.CreateSkinImage("song_select/data_panel/effected_bg.png", 1) local illustratedBgImage = gfx.CreateSkinImage("song_select/data_panel/illust_bg.png", 1) local songPlateBg = gfx.CreateSkinImage("song_select/plate/bg.png", 1) local songPlateBottomBarOverlayImage = gfx.CreateSkinImage("song_select/plate/bottom_bar_overlay.png", 1) local cursorImages = { gfx.CreateSkinImage("song_select/cursor.png", 1), -- Effective rate or fallback gfx.CreateSkinImage("song_select/cursor_exc.png", 1), -- Excessive rate gfx.CreateSkinImage("song_select/cursor_perm.png", 1), -- Premissive rate gfx.CreateSkinImage("song_select/cursor_blast.png", 1), -- Blastive rate } local diffCursorImage = gfx.CreateSkinImage("song_select/level_cursor.png", 1) local searchBgImage = gfx.CreateSkinImage("song_select/search_bg.png", 1) local headerTitleImage = gfx.CreateSkinImage("song_select/header/title.png", 1) local headerGlowTitleImage = gfx.CreateSkinImage("song_select/header/title_glow.png", 1) local defaultJacketImage = gfx.CreateSkinImage("song_select/loading.png", 0) local difficultyLabelImages = { gfx.CreateSkinImage("song_select/plate/difficulty_labels/novice.png", 1), gfx.CreateSkinImage("song_select/plate/difficulty_labels/advanced.png", 1), gfx.CreateSkinImage("song_select/plate/difficulty_labels/exhaust.png", 1), gfx.CreateSkinImage("song_select/plate/difficulty_labels/maximum.png", 1), gfx.CreateSkinImage("song_select/plate/difficulty_labels/infinite.png", 1), gfx.CreateSkinImage("song_select/plate/difficulty_labels/gravity.png", 1), gfx.CreateSkinImage("song_select/plate/difficulty_labels/heavenly.png", 1), gfx.CreateSkinImage("song_select/plate/difficulty_labels/vivid.png", 1), } local badgeImages = { gfx.CreateSkinImage("song_select/medal/nomedal.png", 1), gfx.CreateSkinImage("song_select/medal/played.png", 1), gfx.CreateSkinImage("song_select/medal/clear.png", 1), gfx.CreateSkinImage("song_select/medal/hard.png", 1), gfx.CreateSkinImage("song_select/medal/uc.png", 1), gfx.CreateSkinImage("song_select/medal/puc.png", 1), } local gradeCutoffs = { D = 0000000, C = 7000000, B = 8000000, A = 8700000, A_P = 9000000, AA = 9300000, AA_P = 9500000, AAA = 9700000, AAA_P = 9800000, S = 9900000, } local gradeImages = { S = gfx.CreateSkinImage("common/grades/S.png", 0), AAA_P = gfx.CreateSkinImage("common/grades/AAA+.png", 0), AAA = gfx.CreateSkinImage("common/grades/AAA.png", 0), AA_P = gfx.CreateSkinImage("common/grades/AA+.png", 0), AA = gfx.CreateSkinImage("common/grades/AA.png", 0), A_P = gfx.CreateSkinImage("common/grades/A+.png", 0), A = gfx.CreateSkinImage("common/grades/A.png", 0), B = gfx.CreateSkinImage("common/grades/B.png", 0), C = gfx.CreateSkinImage("common/grades/C.png", 0), D = gfx.CreateSkinImage("common/grades/D.png", 0), none = gfx.CreateSkinImage("common/grades/none.png", 0), } local difficultyLabelUnderImages = { gfx.CreateSkinImage("songtransition/difficulty_labels/nov.png", 0), gfx.CreateSkinImage("songtransition/difficulty_labels/adv.png", 0), gfx.CreateSkinImage("songtransition/difficulty_labels/exh.png", 0), gfx.CreateSkinImage("songtransition/difficulty_labels/mxm.png", 0), gfx.CreateSkinImage("songtransition/difficulty_labels/inf.png", 0), gfx.CreateSkinImage("songtransition/difficulty_labels/grv.png", 0), gfx.CreateSkinImage("songtransition/difficulty_labels/hvn.png", 0), gfx.CreateSkinImage("songtransition/difficulty_labels/vvd.png", 0), } -- ANIMS local laserAnimation = gfx.LoadSkinAnimation('song_select/laser_anim', 1/60, 0, false); game.LoadSkinSample('song_wheel/cursor_change.wav'); game.LoadSkinSample('song_wheel/diff_change.wav'); local difficultyNumbers; local scoreNumbers; local resx, resy = game.GetResolution() local desw, desh; local scale; local songPlateHeight = 172; local selectedIndex = 1; local selectedDifficulty = 1; local jacketCache = {} local transitionScrollScale = 0; local transitionScrollOffsetY = 0; local scrollingUp = false; local transitionAfterscrollScale = 0; local transitionAfterscrollDataOverlayAlpha = 0; local transitionAfterscrollGradeAlpha = 0; local transitionAfterscrollBadgeAlpha = 0; local transitionAfterscrollTextSongTitle = 0; local transitionAfterscrollTextSongArtist = 0; local transitionAfterscrollDifficultiesAlpha = 0; local transitionAfterscrollJacketBgAlpha = 0; local transitionLeaveReverse = false; local transitionLeaveScale = 0; local transitionLeaveOffsetY = 0; function resetLayoutInformation() resx, resy = game.GetResolution() desw = 1080 desh = 1920 scale = resx / desw end function load_number_image(path) local images = {} for i = 0, 9 do images[i + 1] = gfx.CreateSkinImage(string.format("%s/%d.png", path, i), 0) end return images end function draw_number(x, y, alpha, num, digits, images, is_dim, scale, kern) scale = scale or 1; kern = kern or 1; local tw, th = gfx.ImageSize(images[1]) tw = tw * scale; th = th * scale; x = x + (tw * (digits - 1)) / 2 y = y - th / 2 for i = 1, digits do local mul = 10 ^ (i - 1) local digit = math.floor(num / mul) % 10 local a = alpha if is_dim and num < mul then a = 0.4 end gfx.BeginPath() gfx.ImageRect(x, y, tw, th, images[digit + 1], a, 0) x = x - (tw * kern) end end function getCorrectedIndex(from, offset) total = #songwheel.songs; if (math.abs(offset) > total) then if (offset < 0) then offset = offset + total*math.floor(math.abs(offset)/total); else offset = offset - total*math.floor(math.abs(offset)/total); end end index = from + offset; if index < 1 then index = total + (from+offset) -- this only happens if the offset is negative end; if index > total then indexesUntilEnd = total - from; index = offset - indexesUntilEnd -- this only happens if the offset is positive end; return index; end; function getJacketImage(song) if not jacketCache[song.id] or jacketCache[song.id]==defaultJacketImage then jacketCache[song.id] = gfx.LoadImageJob(song.difficulties[1].jacketPath, defaultJacketImage, 165, 165); end return jacketCache[song.id]; end function getGradeImageForScore(score) local gradeImage = gradeImages.none; local bestGradeCutoff = 0; for gradeName, scoreCutoff in pairs(gradeCutoffs) do if scoreCutoff <= score then if scoreCutoff > bestGradeCutoff then gradeImage = gradeImages[gradeName]; bestGradeCutoff = scoreCutoff; end end end return gradeImage; end function drawBackground(deltaTime) gfx.BeginPath() gfx.ImageRect(0, 0, desw, desh, backgroundImage, 1, 0) -- If the score for song exists local song = songwheel.songs[selectedIndex]; local diff = song and song.difficulties[selectedDifficulty] or false; if song and diff then local jacketImage = getJacketImage(song); gfx.BeginPath() gfx.ImageRect(0+transitionAfterscrollScale*-300, 0, 900, 900, jacketImage or defaultJacketImage, transitionAfterscrollJacketBgAlpha, 0) gfx.BeginPath(); gfx.FillColor(0,0,0,math.floor(transitionAfterscrollJacketBgAlpha*64)); gfx.Rect(0,0,900,900); gfx.Fill(); gfx.ClosePath(); gfx.BeginPath(); end gfx.ImageRect(0, 0, desw, desh, dataPanelImage, 1, 0) local laserAnimTickRes = gfx.TickAnimation(laserAnimation, deltaTime); if laserAnimTickRes == 1 then gfx.BeginPath() gfx.ImageRect(0, 0, desw, desh, laserAnimation, 1, 0); gfx.GlobalAlpha(1); end if song and diff then gfx.BeginPath() gfx.ImageRect(0, 0, desw, desh, dataGlowOverlayImage, transitionAfterscrollDataOverlayAlpha, 0) gfx.BeginPath() gfx.ImageRect(341, 754, 85, 85, gradeBgImage, transitionAfterscrollDataOverlayAlpha, 0) gfx.BeginPath() gfx.ImageRect(391, 687, 180*0.85, 226*0.85, badgeBgImage, transitionAfterscrollDataOverlayAlpha, 0) gfx.BeginPath() gfx.ImageRect(95, 1165, 433, 30, effectedBgImage, transitionAfterscrollDataOverlayAlpha, 0) gfx.BeginPath() gfx.ImageRect(95, 1195, 433, 30, illustratedBgImage, transitionAfterscrollDataOverlayAlpha, 0) end end function drawSong(song, y) if (not song) then return end; local songX = desw/2+28 local selectedSongDifficulty = song.difficulties[math.min(selectedDifficulty, #song.difficulties)] -- Limit selecting difficulty that is above the number that the song has if not selectedSongDifficulty then return; end local bestScore; if selectedSongDifficulty.scores then bestScore = selectedSongDifficulty.scores[1]; end -- Draw the bg for the song plate gfx.BeginPath() gfx.ImageRect(songX, y, 515, 172, songPlateBg, 1, 0) -- Draw jacket local jacketImage = getJacketImage(song); gfx.BeginPath() gfx.ImageRect(songX+4, y+4, 163, 163, jacketImage or defaultJacketImage, 1, 0) -- Draw the overlay for the song plate (that bottom black bar) gfx.BeginPath() gfx.ImageRect(songX, y, 515, 172, songPlateBottomBarOverlayImage, 1, 0) -- Draw the difficulty notch background gfx.BeginPath() local diffIndex = GetDisplayDifficulty(selectedSongDifficulty.jacketPath, selectedSongDifficulty.difficulty) gfx.ImageRect(songX, y+95, 83, 74, difficultyLabelImages[diffIndex], 1, 0) -- Draw the difficulty level number gfx.BeginPath() draw_number(songX+30, y+125, 1.0, selectedSongDifficulty.level, 2, difficultyNumbers, false, 0.65, 1) -- Draw song title gfx.FontSize(24) gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_MIDDLE) gfx.Text(song.title, songX+90, y+155); -- Draw score badge local badgeImage = badgeImages[1]; if selectedSongDifficulty.topBadge then badgeImage = badgeImages[selectedSongDifficulty.topBadge+1]; end gfx.BeginPath() gfx.ImageRect(songX+282, y+44, 79, 69, badgeImage, 1, 0) -- Draw grade local gradeImage = gradeImages.none; if bestScore then gradeImage = getGradeImageForScore(bestScore.score) end gfx.BeginPath(); gfx.ImageRect(songX+390, y+47, 60, 60, gradeImage, 1, 0); end function drawSongList() local numOfSongsAround = 7; -- How many songs should be up and how many should be down of the selected one local yOffset = transitionLeaveOffsetY + transitionScrollOffsetY; local i=1; while (i <= numOfSongsAround) do local songIndex = getCorrectedIndex(selectedIndex, -i) drawSong(songwheel.songs[songIndex], desh/2-songPlateHeight/2-songPlateHeight*i + yOffset) i=i+1; end; -- Draw the selected song drawSong(songwheel.songs[selectedIndex], desh/2-songPlateHeight/2 + yOffset) i=1; while (i <= numOfSongsAround) do local songIndex = getCorrectedIndex(selectedIndex, i) drawSong(songwheel.songs[songIndex], desh/2-songPlateHeight/2+songPlateHeight*i + yOffset) i=i+1; end; end function drawCursor() gfx.BeginPath() local cursorImageIndex = game.GetSkinSetting('_gaugeType') local cursorImage = cursorImages[cursorImageIndex or 1]; gfx.ImageRect(desw/2-14, desh/2-213/2, 555, 213, cursorImage, 1, 0) end local scoreNumbers = load_number_image("score_num"); function drawData() -- Draws the song data on the left panel local song = songwheel.songs[selectedIndex]; local diff = song and song.difficulties[selectedDifficulty] or false; local bestScore = diff and diff.scores[1]; if not song then return false end local jacketImage = getJacketImage(song); gfx.BeginPath() gfx.ImageRect(96, 324, 348, 348, jacketImage or defaultJacketImage, 1, 0) gfx.Save() if bestScore then -- Draw best score gfx.BeginPath() draw_number(100, 793, 1.0, math.floor(bestScore.score / 10000), 4, scoreNumbers, true, 0.3, 1.12) draw_number(245, 797, 1.0, bestScore.score, 4, scoreNumbers, true, 0.22, 1.12) -- Draw grade local gradeImage = gradeImages.none; if bestScore then gradeImage = getGradeImageForScore(bestScore.score) end gfx.BeginPath(); gfx.ImageRect(360, 773, 45, 45, gradeImage, transitionAfterscrollGradeAlpha, 0); -- Draw badge badgeImage = badgeImages[diff.topBadge+1]; gfx.BeginPath() gfx.ImageRect(425, 724, 93/1.1, 81/1.1, badgeImage, transitionAfterscrollBadgeAlpha, 0) end gfx.Restore() -- Draw BPM gfx.BeginPath(); gfx.FontSize(24) gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_MIDDLE) gfx.Save() gfx.LoadSkinFont('Digital-Serial-Bold.ttf') gfx.GlobalAlpha(transitionAfterscrollDataOverlayAlpha) -- TODO: split this out gfx.Text(song.bpm, 85, 920); gfx.Restore() -- Draw song title gfx.FontSize(28) gfx.GlobalAlpha(transitionAfterscrollTextSongTitle); gfx.Text(song.title, 30+(1-transitionAfterscrollTextSongTitle)*20, 955); -- Draw artist gfx.GlobalAlpha(transitionAfterscrollTextSongArtist); gfx.Text(song.artist, 30+(1-transitionAfterscrollTextSongArtist)*30, 997); gfx.GlobalAlpha(1); -- Draw difficulties local DIFF_X_START = 98.5 local DIFF_GAP = 114.8; gfx.GlobalAlpha(transitionAfterscrollDifficultiesAlpha); for i, diff in ipairs(song.difficulties) do gfx.BeginPath() local index = diff.difficulty+1 if i == selectedDifficulty then gfx.ImageRect(DIFF_X_START+(index-1)*DIFF_GAP-(163*0.8)/2, 1028, 163*0.8, 163*0.8, diffCursorImage, 1, 0) end draw_number(85+(index-1)*DIFF_GAP, 1085, 1.0, diff.level, 2, difficultyNumbers, false, 0.8, 1) local diffLabelImage = difficultyLabelUnderImages[ GetDisplayDifficulty(diff.jacketPath, diff.difficulty) ]; local tw, th = gfx.ImageSize(diffLabelImage) tw=tw*0.9 th=th*0.9 gfx.BeginPath() gfx.ImageRect(DIFF_X_START+(index-1)*DIFF_GAP-tw/2, 1050, tw, th, diffLabelImage, 1, 0) end gfx.GlobalAlpha(1); gfx.FontSize(22) gfx.GlobalAlpha(transitionAfterscrollDataOverlayAlpha); gfx.Text(diff.effector, 270, 1180); gfx.Text(diff.illustrator, 270, 1210); gfx.GlobalAlpha(1); end function drawSearch() if (not songwheel.searchInputActive) then return; end gfx.BeginPath(); local tw, th = gfx.ImageSize(searchBgImage) gfx.ImageRect(desw-tw/2, 0, tw/2, th/2, searchBgImage, 1, 0) gfx.FontSize(28); gfx.Text(songwheel.searchText, desw-200, 30); end function tickTransitions(deltaTime) if transitionScrollScale < 1 then transitionScrollScale = transitionScrollScale + deltaTime / 0.1 -- transition should last for that time in seconds else transitionScrollScale = 1 end if transitionAfterscrollScale < 1 then if transitionScrollScale == 1 then -- Only start the after scroll transition when the scroll transition is finished transitionAfterscrollScale = transitionAfterscrollScale + deltaTime / 15 end else transitionAfterscrollScale = 1; end if scrollingUp then transitionScrollOffsetY = Easing.inQuad(1-transitionScrollScale) * songPlateHeight; else transitionScrollOffsetY = Easing.inQuad(1-transitionScrollScale) * -songPlateHeight; end if transitionAfterscrollScale < 0.02 then transitionAfterscrollDataOverlayAlpha = math.min(1, transitionAfterscrollScale / 0.02) else transitionAfterscrollDataOverlayAlpha = 1; end -- Grade alpha if transitionAfterscrollScale >= 0.03 and transitionAfterscrollScale < 0.033 then transitionAfterscrollGradeAlpha = 0.5; elseif transitionAfterscrollScale >= 0.04 then transitionAfterscrollGradeAlpha = 1; else transitionAfterscrollGradeAlpha = 0; end -- Badge alpha if transitionAfterscrollScale >= 0.032 and transitionAfterscrollScale < 0.035 then transitionAfterscrollBadgeAlpha = 0.5; elseif transitionAfterscrollScale >= 0.042 then transitionAfterscrollBadgeAlpha = 1; else transitionAfterscrollBadgeAlpha = 0; end -- Song title alpha and pos if transitionAfterscrollScale < 0.025 then transitionAfterscrollTextSongTitle = Easing.outQuad(math.min(1, (transitionAfterscrollScale) / 0.025)); else transitionAfterscrollTextSongTitle = 1 end -- Song artist alpha and pos if transitionAfterscrollScale < 0.025 then transitionAfterscrollTextSongArtist = Easing.outQuad(math.min(1, (transitionAfterscrollScale) / 0.025)); else transitionAfterscrollTextSongArtist = 1 end -- Difficulties alpha if transitionAfterscrollScale < 0.025 then transitionAfterscrollDifficultiesAlpha = math.min(1, transitionAfterscrollScale / 0.025) else transitionAfterscrollDifficultiesAlpha = 1; end -- Jacket background alpha if transitionAfterscrollScale < 0.05 or transitionAfterscrollScale >= 1 then transitionAfterscrollJacketBgAlpha = 0; elseif transitionAfterscrollScale >= 0.05 and transitionAfterscrollScale < 0.1 then transitionAfterscrollJacketBgAlpha = math.min(1, (transitionAfterscrollScale-0.05) / 0.05); elseif transitionAfterscrollScale >= 0.95 and transitionAfterscrollScale < 1 then transitionAfterscrollJacketBgAlpha = math.min(1, 1-((transitionAfterscrollScale-0.95) / 0.05)); else transitionAfterscrollJacketBgAlpha = 1; end -- Leave transition if (transitionLeaveReverse) then if transitionLeaveScale > 0 then transitionLeaveScale = transitionLeaveScale - deltaTime / 0.5 -- transition should last for that time in seconds else transitionLeaveScale = 0 end else if transitionLeaveScale < 1 then transitionLeaveScale = transitionLeaveScale + deltaTime / 0.5 -- transition should last for that time in seconds else transitionLeaveScale = 1 end end transitionLeaveOffsetY = Easing.inOutQuad(transitionLeaveScale) * (desh+songPlateHeight*2+145); end render = function (deltaTime) resetLayoutInformation(); tickTransitions(deltaTime); gfx.Scale(scale, scale); if not difficultyNumbers then difficultyNumbers = load_number_image('diff_num') end drawBackground(deltaTime); drawSongList() drawCursor() drawData() drawSearch(); if (game.GetSkinSetting('_songWheelOverlayActive') ~= 1) then transitionLeaveReverse = true; else transitionLeaveReverse = false; end gfx.BeginPath(); gfx.FontSize(18) gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_TOP) local debugScrollingUp= "FALSE" if scrollingUp then debugScrollingUp = "TRUE" end; gfx.Text('S_I: ' .. selectedIndex .. ' // S_D: ' .. selectedDifficulty .. ' // S_UP: ' .. debugScrollingUp .. ' // AC_TS: ' .. transitionAfterscrollScale, 8, 8); end songs_changed = function (withAll) if not withAll then return end local diffs = {} for i = 1, #songwheel.allSongs do local song = songwheel.allSongs[i] for j = 1, #song.difficulties do local diff = song.difficulties[j] diff.force = VolforceCalc.calc(diff) table.insert(diffs, diff) end end table.sort(diffs, function (l, r) return l.force > r.force end) totalForce = 0 for i = 1, 50 do if diffs[i] then totalForce = totalForce + diffs[i].force end end game.SetSkinSetting('_volforce', totalForce) end set_index = function(newIndex) transitionScrollScale = 0; transitionAfterscrollScale = 0; scrollingUp = false; if ((newIndex > selectedIndex and not (newIndex == #songwheel.songs and selectedIndex == 1)) or (newIndex == 1 and selectedIndex == #songwheel.songs)) then scrollingUp = true; end; game.PlaySample('song_wheel/cursor_change.wav'); selectedIndex = newIndex; end; set_diff = function(newDiff) if newDiff ~= selectedDifficulty then game.PlaySample('song_wheel/diff_change.wav'); end selectedDifficulty = newDiff; end;