ExperimentalGear/scripts/songselect/songwheel.lua

1226 lines
41 KiB
Lua

local Charting = require('common.charting')
local Easing = require('common.easing')
local Background = require('components.background')
local Dim = require("common.dimensions")
local Wallpaper = require("components.wallpaper")
local common = require('common.util')
local Sound = require("common.sound")
local Numbers = require('components.numbers')
local Footer = require('components.footer')
local VolforceCalc = require('components.volforceCalc')
require("api.point2d")
require("components.radar")
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 scoreBoardBarBgImage = gfx.CreateSkinImage("song_select/textboard.png", 1)
local crownImage = gfx.CreateSkinImage("song_select/crown.png", 1)
local laserAnimBaseImage = gfx.CreateSkinImage("song_select/laser_anim.png", 1)
local top50OverlayImage = gfx.CreateSkinImage("song_select/top50.png", 1)
local top50JacketOverlayImage = gfx.CreateSkinImage("song_select/top50_jacket.png", 1)
local diffCursorImage = gfx.CreateSkinImage("song_select/level_cursor.png", 1)
local scrollBarBackgroundImage = gfx.CreateSkinImage("song_select/scrollbar/bg.png", 1)
local scrollBarFillImage = gfx.CreateSkinImage("song_select/scrollbar/fill.png", 1)
local filterInfoBgImage = gfx.CreateSkinImage("song_select/filter_info_bg.png", 1)
local sortInfoBgImage = gfx.CreateSkinImage("song_select/sort_info_bg.png", 1)
local searchBgImage = gfx.CreateSkinImage("song_select/search_bg.png", 1)
local searchActiveImage = gfx.CreateSkinImage("song_select/search_active.png", 1)
local searchInfoPanelImage = gfx.CreateSkinImage("song_select/search_info_panel.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),
gfx.CreateSkinImage("song_select/plate/difficulty_labels/exceed.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 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 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),
gfx.CreateSkinImage("songtransition/difficulty_labels/xcd.png", 0),
}
game.LoadSkinSample('song_wheel/cursor_change.wav')
game.LoadSkinSample('song_wheel/diff_change.wav')
local scoreNumbers = Numbers.load_number_image("score_num")
local difficultyNumbers = Numbers.load_number_image("diff_num")
local showEffectRadar = game.GetSkinSetting("songselect_showEffectRadar") or false
local LEADERBOARD_PLACE_NAMES = {
'1st',
'2nd',
'3rd',
'4th',
}
local songPlateHeight = 172
local selectedIndex = 1
local selectedDifficulty = 1
local radar = Radar.new(Point2D.new(0, 0))
local updateRadar = true
local jacketCache = {}
local top50diffs = {}
local irRequestStatus = 1 -- 0=unused, 1=not requested, 2=loading, others are status codes
local irRequestTimeout = 2
local irLeaderboard = {} ---@type ServerScore[]|{}
local irLeaderboardsCache = {}
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 transitionJacketBgScrollScale = 0
local transitionJacketBgScrollAlpha = 0
local transitionJacketBgScrollPosX = 0
--search
local searchPreviousActiveState = false
local searchInfoPreviousActiveState = false
local transitionSearchEnterScale = 0
local transitionSearchInfoEnterScale = 0
local transitionSearchBackgroundAlpha = 0
local transitionSearchbarOffsetY = 0
local transitionSearchInfoOffsetY = 0
local transitionSearchBackgroundInfoAlpha = Easing.inOutQuad(transitionSearchInfoEnterScale)
local transitionLaserScale = 0
local transitionLaserY = 0
-- Flash transition (animation)
-- Used for flashing the badges
-- 0 = minimum brightness; 0.5 = maximum brightness; 1 = minimum brightness again
local transitionFlashScale = 0
local transitionFlashAlpha = 1
local isFilterWheelActive = false
local transitionLeaveScale = 0
local transitionLeaveReappearTimer = 0
local TRANSITION_LEAVE_DURATION = 0.1
-- Window variables
local resX, resY
-- Aspect Ratios
local landscapeWidescreenRatio = 16 / 9
local landscapeStandardRatio = 4 / 3
local portraitWidescreenRatio = 9 / 16
-- Portrait sizes
local fullX, fullY
local desw = 1080
local desh = 1920
local resolutionChange = function(x, y)
resX = x
resY = y
fullX = portraitWidescreenRatio * y
fullY = y
game.Log('resX:' .. resX .. ' // resY:' .. resY .. ' // fullX:' .. fullX .. ' // fullY:' .. fullY, game.LOGGER_ERROR)
end
local function getCorrectedIndex(from, offset)
local 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
local index = from + offset
if index < 1 then
index = total + (from+offset) -- this only happens if the offset is negative
end
if index > total then
local indexesUntilEnd = total - from
index = offset - indexesUntilEnd -- this only happens if the offset is positive
end
return index
end
local function getJacketImage(song)
if not jacketCache[song.id] or jacketCache[song.id]==defaultJacketImage then
jacketCache[song.id] = gfx.LoadImageJob(song.difficulties[
math.min(selectedDifficulty, #song.difficulties)
].jacketPath, defaultJacketImage, 500, 500)
end
return jacketCache[song.id]
end
local 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
local function drawLaserAnim()
gfx.Save()
gfx.BeginPath()
gfx.Scissor(0, transitionLaserY, desw, 100)
gfx.ImageRect(0, 0, desw, desh, laserAnimBaseImage, 1, 0)
gfx.Restore()
end
local function drawBackground(deltaTime)
Background.draw(deltaTime)
local song = songwheel.songs[selectedIndex]
local diff = song and song.difficulties[selectedDifficulty] or false
if (not isFilterWheelActive and transitionLeaveReappearTimer == 0) then
-- If the score for song exists
if song and diff then
local jacketImage = getJacketImage(song)
gfx.BeginPath()
gfx.ImageRect(transitionJacketBgScrollPosX, 0, 900, 900, jacketImage or defaultJacketImage, transitionJacketBgScrollAlpha, 0)
gfx.BeginPath()
gfx.FillColor(0,0,0,math.floor(transitionJacketBgScrollAlpha*64))
gfx.Rect(0,0,900,900)
gfx.Fill()
gfx.ClosePath()
end
end
gfx.BeginPath()
gfx.ImageRect(0, 0, desw, desh, dataPanelImage, 1, 0)
drawLaserAnim()
if song and diff and (not isFilterWheelActive and transitionLeaveReappearTimer == 0) 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
---@param song SongWheelSong
---@param y number
local 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 = Charting.GetDisplayDifficulty(selectedSongDifficulty.jacketPath, selectedSongDifficulty.difficulty)
gfx.ImageRect(songX, y+95, 83, 74, difficultyLabelImages[diffIndex], 1, 0)
-- Draw the difficulty level number
gfx.BeginPath()
Numbers.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
local badgeAlpha = 1
if (selectedSongDifficulty.topBadge >= 3) then
badgeAlpha = transitionFlashAlpha -- If hard clear or above, flash the badge
end
gfx.BeginPath()
gfx.ImageRect(songX+282, y+44, 79, 69, badgeImage, badgeAlpha, 0)
-- Draw grade
local gradeImage = gradeImages.none
local gradeAlpha = 1
if bestScore then
gradeImage = getGradeImageForScore(bestScore.score)
if (bestScore.score >= gradeCutoffs.S) then
gradeAlpha = transitionFlashAlpha -- If S, flash the badge
end
end
gfx.BeginPath()
gfx.ImageRect(songX+391, y+47, 60, 60, gradeImage, gradeAlpha, 0)
-- Draw top 50 label if applicable
if (top50diffs[selectedSongDifficulty.hash]) then
gfx.BeginPath()
gfx.ImageRect(songX+82, y+109, 506*0.85, 26*0.85, top50OverlayImage, 1, 0)
end
end
local function drawSongList()
gfx.GlobalAlpha(1-transitionLeaveScale)
local numOfSongsAround = 7 -- How many songs should be up and how many should be down of the selected one
local yOffset = 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
gfx.GlobalAlpha(1)
end
---@param diff SongWheelDifficulty
local function drawLocalLeaderboard(diff)
gfx.LoadSkinFont('Digital-Serial-Bold.ttf')
gfx.FontSize(26)
local scoreBoardX = 75
local scoreBoardY = 1250
local sbBarWidth = 336*1.2
local sbBarHeight = 33
local sbBarContentLeftX = scoreBoardX + 80
local sbBarContentRightX = scoreBoardX + sbBarWidth/2 + 30
-- Draw the header
gfx.BeginPath()
gfx.ImageRect(scoreBoardX, scoreBoardY, sbBarWidth, sbBarHeight, scoreBoardBarBgImage, 1, 0)
gfx.BeginPath()
gfx.ImageRect(205, 1252.5, 800*0.045, 600*0.045, crownImage, 1, 0)
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_MIDDLE)
gfx.BeginPath()
gfx.Text("LOCAL TOP", sbBarContentRightX, scoreBoardY + sbBarHeight/2)
for i = 1, 5, 1 do
local scoreTable = diff.scores[i]
local score = scoreTable and scoreTable.score
local username = scoreTable and scoreTable.playerName
-- if for some reason there's a score but no associated username, fall back to skin setting
if score and username == "" then
username = game.GetSkinSetting("username")
end
gfx.BeginPath()
gfx.ImageRect(scoreBoardX, scoreBoardY + i*sbBarHeight, sbBarWidth, sbBarHeight, scoreBoardBarBgImage, 1, 0)
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_MIDDLE)
gfx.BeginPath()
gfx.Text(username or "-", sbBarContentLeftX, scoreBoardY + sbBarHeight/2 + i*sbBarHeight)
gfx.BeginPath()
local scoreText = score and tostring(score) or "- - - - - - - -"
gfx.Text(scoreText, sbBarContentRightX, scoreBoardY + sbBarHeight/2 + i*sbBarHeight)
end
end
local function drawIrLeaderboard()
if not IRData.Active then
return
end
gfx.LoadSkinFont('Digital-Serial-Bold.ttf')
gfx.FontSize(26)
local scoreBoardX = 75
local scoreBoardY = 1500
local sbBarWidth = 336*1.2
local sbBarHeight = 33
local sbBarContentLeftX = scoreBoardX + 80
local sbBarContentRightX = scoreBoardX + sbBarWidth/2 + 30
-- Draw the header
gfx.BeginPath()
gfx.ImageRect(scoreBoardX, scoreBoardY, sbBarWidth, sbBarHeight, scoreBoardBarBgImage, 1, 0)
gfx.TextAlign(gfx.TEXT_ALIGN_CENTER + gfx.TEXT_ALIGN_MIDDLE)
gfx.BeginPath()
if irRequestStatus == 1 or irRequestStatus == 2 then
gfx.Text("Loading ranking...", scoreBoardX + (sbBarWidth / 2), scoreBoardY + sbBarHeight/2)
return
end
if irRequestStatus == IRData.States.ChartRefused then
gfx.Text("This chart is blacklisted", scoreBoardX + (sbBarWidth / 2), scoreBoardY + sbBarHeight/2)
return
end
if irRequestStatus == IRData.States.NotFound then
gfx.Text("This chart is not tracked", scoreBoardX + (sbBarWidth / 2), scoreBoardY + sbBarHeight/2)
return
end
if #irLeaderboard == 0 then
gfx.Text("This chart has no scores", scoreBoardX + (sbBarWidth / 2), scoreBoardY + sbBarHeight/2)
return
end
gfx.TextAlign(gfx.TEXT_ALIGN_CENTER + gfx.TEXT_ALIGN_MIDDLE)
gfx.BeginPath()
gfx.Text("IR TOP", scoreBoardX + (sbBarWidth / 2), scoreBoardY + sbBarHeight/2)
for i = 1, 4, 1 do
gfx.BeginPath()
gfx.ImageRect(scoreBoardX, scoreBoardY + i*sbBarHeight, sbBarWidth, sbBarHeight, scoreBoardBarBgImage, 1, 0)
end
-- Becuase the scores are in "random order", we have to do this
for index, irScore in ipairs(irLeaderboard) do
-- local irScore = irLeaderboard[i]
if irScore then
local rank = index
gfx.TextAlign(gfx.TEXT_ALIGN_CENTER + gfx.TEXT_ALIGN_MIDDLE)
gfx.BeginPath()
gfx.Text(LEADERBOARD_PLACE_NAMES[rank], sbBarContentLeftX-40, scoreBoardY + sbBarHeight/2 + rank*sbBarHeight)
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_MIDDLE)
gfx.BeginPath()
gfx.Text(string.upper(irScore.username), sbBarContentLeftX, scoreBoardY + sbBarHeight/2 + rank*sbBarHeight)
gfx.BeginPath()
gfx.Text(string.format("%d", irScore.score), sbBarContentRightX, scoreBoardY + sbBarHeight/2 + rank*sbBarHeight)
local badgeImage = badgeImages[irScore.lamp+1]
gfx.BeginPath()
gfx.ImageRect(scoreBoardX + sbBarWidth - 50, scoreBoardY + sbBarHeight/2 + rank*sbBarHeight - 12.5, 31.6, 27.6, badgeImage, 1, 0)
end
end
end
local function drawData() -- Draws the song data on the left panel
if isFilterWheelActive or transitionLeaveReappearTimer ~= 0 then return false end
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)
if (top50diffs[diff.hash]) then
gfx.BeginPath()
gfx.ImageRect(96, 529, 410*0.85, 168*0.85, top50JacketOverlayImage, 1, 0)
end
-- Draw best score
gfx.Save()
gfx.BeginPath()
local scoreNumber = 0
if bestScore then
scoreNumber = bestScore.score
end
Numbers.draw_number(100, 793, 1.0, math.floor(scoreNumber / 10000), 4, scoreNumbers, true, 0.3, 1.12)
Numbers.draw_number(253, 798, 1.0, scoreNumber, 4, scoreNumbers, true, 0.22, 1.12)
-- Draw grade
local gradeImage = gradeImages.none
local gradeAlpha = transitionAfterscrollGradeAlpha
if bestScore then
gradeImage = getGradeImageForScore(bestScore.score)
if (transitionAfterscrollGradeAlpha == 1 and bestScore.score >= gradeCutoffs.S) then
gradeAlpha = transitionFlashAlpha -- If S, flash the badge
end
end
gfx.BeginPath()
gfx.ImageRect(360, 773, 45, 45, gradeImage, gradeAlpha, 0)
-- Draw badge
local badgeImage = badgeImages[diff.topBadge+1]
local badgeAlpha = transitionAfterscrollBadgeAlpha
if (transitionAfterscrollBadgeAlpha == 1 and diff.topBadge >= 3) then
badgeAlpha = transitionFlashAlpha -- If hard clear or above, flash the badge, but only after the initial transition
end
gfx.BeginPath()
gfx.ImageRect(425, 724, 93/1.1, 81/1.1, badgeImage, badgeAlpha, 0)
gfx.Restore()
-- Draw BPM
gfx.Save()
gfx.BeginPath()
gfx.FontSize(24)
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_MIDDLE)
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.Save()
gfx.FontSize(28)
gfx.GlobalAlpha(transitionAfterscrollTextSongTitle)
gfx.Text(song.title, 30+(1-transitionAfterscrollTextSongTitle)*20, 955)
gfx.Restore()
-- Draw artist
gfx.Save()
gfx.GlobalAlpha(transitionAfterscrollTextSongArtist)
gfx.Text(song.artist, 30+(1-transitionAfterscrollTextSongArtist)*30, 997)
gfx.Restore()
-- Draw difficulties
local DIFF_X_START = 98.5
local DIFF_GAP = 114.8
gfx.Save()
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
Numbers.draw_number(85+(index-1)*DIFF_GAP, 1085, 1.0, diff.level, 2, difficultyNumbers, false, 0.8, 1)
local diffLabelImage = difficultyLabelUnderImages[
Charting.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.Restore()
-- Scoreboard
drawLocalLeaderboard(diff)
drawIrLeaderboard()
gfx.Save()
gfx.FontSize(22)
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_MIDDLE)
gfx.GlobalAlpha(transitionAfterscrollDataOverlayAlpha)
gfx.Text(diff.effector, 270, 1180) -- effected by
gfx.Text(diff.illustrator, 270, 1210) -- illustrated by
gfx.Restore()
end
local function drawFilterInfo(deltatime)
gfx.LoadSkinFont('NotoSans-Regular.ttf')
if (songwheel.searchInputActive) then
--return
end
gfx.BeginPath()
gfx.ImageRect(5, 95, 417*0.85, 163*0.85, filterInfoBgImage, 1, 0)
local folderLabel = game.GetSkinSetting('_songWheelActiveFolderLabel')
local subFolderLabel = game.GetSkinSetting('_songWheelActiveSubFolderLabel')
local sortOptionLabel = game.GetSkinSetting('_songWheelActiveSortOptionLabel')
gfx.FontSize(24)
gfx.TextAlign(gfx.TEXT_ALIGN_CENTER + gfx.TEXT_ALIGN_MIDDLE)
gfx.BeginPath()
gfx.Text(folderLabel or '', 167, 131)
gfx.BeginPath()
gfx.Text(subFolderLabel or '', 195, 166)
gfx.BeginPath()
gfx.ImageRect(desw - 310 - 5, 108, 310, 75, sortInfoBgImage, 1, 0)
gfx.BeginPath()
gfx.Text(sortOptionLabel or '', desw-150, 130)
end
local function drawCursor()
if isFilterWheelActive or transitionLeaveScale ~= 0 then return false end
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 function drawSearch()
if (not songwheel.searchInputActive and searchPreviousActiveState) then
searchPreviousActiveState = false
game.PlaySample('sort_wheel/enter.wav')
elseif (songwheel.searchInputActive and not searchPreviousActiveState) then
searchPreviousActiveState = true
game.PlaySample('sort_wheel/leave.wav')
end
if (songwheel.searchText ~= '' and searchInfoPreviousActiveState == true) then
searchInfoPreviousActiveState = false
elseif (songwheel.searchText == '' and searchInfoPreviousActiveState == false) then
searchInfoPreviousActiveState = true
end
if (transitionSearchEnterScale == 0) then
return
end
-- Draw dark overlay over Songwheel
gfx.BeginPath()
gfx.FillColor(0, 0, 0, math.floor(transitionSearchBackgroundAlpha * 192))
gfx.Rect(0, 0, 1080, 1920)
gfx.Fill()
-- Draw search info panel
gfx.BeginPath()
local infoResize = 0.855
local sw, sh = gfx.ImageSize(searchInfoPanelImage)
sw = sw * infoResize
sh = sh * infoResize
local infoXPos = 0
local infoYStartPos = desh - sh - 772 + 242
local infoYPos = infoYStartPos + transitionSearchInfoOffsetY
if (game.GetSkinSetting('gameplay_showSearchControls')) then
gfx.ImageRect(infoXPos, infoYPos, sw, sh, searchInfoPanelImage, transitionSearchBackgroundInfoAlpha, 0)
end
-- Draw Search is Active text
gfx.BeginPath()
local activeResize = 0.855
local activew, activeh = gfx.ImageSize(searchActiveImage)
activew = activew * activeResize
activeh = activeh * activeResize
local activeXPos = 0
local activeYStartPos = desh - sh - 722
local activeYPos = activeYStartPos + transitionSearchInfoOffsetY
gfx.ImageRect(activeXPos, activeYPos, activew, activeh, searchActiveImage, 1, 0)
-- Draw Searchbox
gfx.BeginPath()
local searchResize = 0.8
local tw, th = gfx.ImageSize(searchBgImage)
tw = tw * searchResize
th = th * searchResize
local xPos = (desw-tw)/2
local yStartPos = 170
local yPos = yStartPos - transitionSearchbarOffsetY
gfx.ImageRect(xPos, yPos, tw, th, searchBgImage, 1, 0)
gfx.FontSize(48)
gfx.LoadSkinFont('Digital-Serial-Bold.ttf')
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_MIDDLE)
gfx.Text(songwheel.searchText, xPos + 160, yPos + 83.2)
end
local function drawScrollbar()
if isFilterWheelActive or transitionLeaveScale ~= 0 then return end
-- Scrollbar Background
gfx.BeginPath()
local resize = 0.85
local lw, lh = gfx.ImageSize(scrollBarBackgroundImage)
local lw = lw * resize
local lh = lh * resize
local xPos = desw-20
local backgroundYPos = desh/2 - lh/2
gfx.ImageRect(xPos, backgroundYPos, lw, lh, scrollBarBackgroundImage, 1, 0)
-- Scrollbar Fill
gfx.BeginPath()
local sw, sh = gfx.ImageSize(scrollBarFillImage)
local sw = sw * resize
local sh = sh * resize
local fillXPos = xPos - 6
local minScrollYPos = backgroundYPos
local maxScrollYPos = backgroundYPos + lh - sh
local scrollStep = (maxScrollYPos - minScrollYPos) / (#songwheel.songs - 1)
local scrollbarYOffset = (selectedIndex - 1) * scrollStep
local scrollbarYPos = minScrollYPos + scrollbarYOffset
gfx.ImageRect(fillXPos, scrollbarYPos, sw, sh, scrollBarFillImage, 1, 0)
-- 1st letter of song title on scroll
gfx.BeginPath()
gfx.FontSize(16)
gfx.LoadSkinFont('Digital-Serial-Bold.ttf')
gfx.Rect(fillXPos-18, scrollbarYPos - 5, 16, 16)
gfx.FillColor(0,0,0,170)
gfx.Fill()
gfx.FillColor(255,255,255)
gfx.TextAlign(gfx.TEXT_ALIGN_MIDDLE + gfx.TEXT_ALIGN_CENTER)
if (songwheel.songs[selectedIndex] ~= nil) then
local title = songwheel.songs[selectedIndex].title;
local letter = string.upper(common.firstAlphaNum(title))
gfx.Text(letter, fillXPos-10, scrollbarYPos + 5)
end
end
---Called on IR Leaderboard fetch complete
---@param res IRLeaderboardResponse
local function onIrLeaderboardFetched(res)
irRequestStatus = res.statusCode
local song = songwheel.songs[selectedIndex]
local diff = song and song.difficulties[selectedDifficulty] or false
if res.statusCode == IRData.States.Success then
local tempIrLB = res.body
table.sort(tempIrLB, function (a,b)
return a.score > b.score
end)
irLeaderboard = tempIrLB
irLeaderboardsCache[diff.hash] = irLeaderboard
else
local httpStatus = (res.statusCode // 10) * 100 + res.statusCode % 10 -- convert to 100 range
game.Log("IR error (" .. httpStatus .. "): " .. res.description, game.LOGGER_ERROR)
if res.body then
game.Log(common.dump(res.body), game.LOGGER_ERROR)
end
end
end
local function refreshIrLeaderboard(deltaTime)
if not IRData.Active then
return
end
if irRequestStatus ~= 1 then -- Only continue if the leaderboard is requesteded, but not loading or loaded.
return
end
irLeaderboard = {}
local song = songwheel.songs[selectedIndex]
local diff = song and song.difficulties[selectedDifficulty] or false
if (not diff) then
return
end
if (irLeaderboardsCache[diff.hash]) then
irLeaderboard = irLeaderboardsCache[diff.hash]
irRequestStatus = 20
return
end
if (irRequestTimeout > 0) then
irRequestTimeout = irRequestTimeout - deltaTime
return
end
irRequestStatus = 2 -- Loading
IR.Leaderboard(diff.hash, 'best', 4, onIrLeaderboardFetched)
end
local 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
-- Searchbar offsets and alpha
if not searchPreviousActiveState then
if transitionSearchEnterScale > 0 then
transitionSearchEnterScale = transitionSearchEnterScale - deltaTime / 0.5 -- transition should last for that time in seconds
else
transitionSearchEnterScale = 0
end
else
if transitionSearchEnterScale < 1 then
transitionSearchEnterScale = transitionSearchEnterScale + deltaTime / 0.5 -- transition should last for that time in seconds
else
transitionSearchEnterScale = 1
end
end
transitionSearchInfoOffsetY = Easing.inOutQuad(1 - transitionSearchEnterScale) * 1680
transitionSearchbarOffsetY = Easing.inOutQuad(1 - transitionSearchEnterScale) * 300
transitionSearchBackgroundAlpha = Easing.inOutQuad(transitionSearchEnterScale)
if not searchInfoPreviousActiveState then
if transitionSearchInfoEnterScale > 0 then
transitionSearchInfoEnterScale = transitionSearchInfoEnterScale - deltaTime / 0.25 -- transition should last for that time in seconds
else
transitionSearchInfoEnterScale = 0
end
else
if transitionSearchInfoEnterScale < 1 then
transitionSearchInfoEnterScale = transitionSearchInfoEnterScale + deltaTime / 0.25 -- transition should last for that time in seconds
else
transitionSearchInfoEnterScale = 1
end
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 bg animation
if transitionJacketBgScrollScale < 1 then
transitionJacketBgScrollScale = transitionJacketBgScrollScale + deltaTime / 20 -- transition should last for that time in seconds
else
transitionJacketBgScrollScale = 0
end
if transitionJacketBgScrollScale < 0.05 or transitionJacketBgScrollScale >= 1 then
transitionJacketBgScrollAlpha = 0
elseif transitionJacketBgScrollScale >= 0.05 and transitionJacketBgScrollScale < 0.1 then
transitionJacketBgScrollAlpha = math.min(1, (transitionJacketBgScrollScale-0.05) / 0.05)
elseif transitionJacketBgScrollScale >= 0.8 and transitionJacketBgScrollScale < 1 then
transitionJacketBgScrollAlpha = math.max(0,
math.min(1, 1-((transitionJacketBgScrollScale-0.8) / 0.05))
)
else
transitionJacketBgScrollAlpha = 1
end
transitionJacketBgScrollPosX = 0+(transitionJacketBgScrollScale*(0.8/1))*-300
-- Laser anim
if transitionLaserScale < 1 then
transitionLaserScale = transitionLaserScale + deltaTime / 2 -- transition should last for that time in seconds
else
transitionLaserScale = 0
end
transitionLaserY = desh - math.min(transitionLaserScale * 2 * desh, desh)
-- Flash transition
if transitionFlashScale < 1 then
---@type number|string
local songBpm = 120
if (songwheel.songs[selectedIndex] and game.GetSkinSetting('animations_affectWithBPM')) then
local songBpmStr = songwheel.songs[selectedIndex].bpm
local songBpmStrs = common.split(songBpmStr, '-')
local minBpm = tonumber(songBpmStrs[1]) -- Lowest bpm value
songBpm = minBpm or songBpm
end
transitionFlashScale = transitionFlashScale + deltaTime / (60/songBpm) -- transition should last for that time in seconds
else
transitionFlashScale = 0
end
if transitionFlashScale < 0.5 then
transitionFlashAlpha = transitionFlashScale * 2
else
transitionFlashAlpha = 1-((transitionFlashScale-0.5) * 2)
end
transitionFlashAlpha = 1+transitionFlashAlpha*0.5
-- Leave transition
if (isFilterWheelActive) then
if transitionLeaveScale < 1 then
transitionLeaveScale = transitionLeaveScale + deltaTime / TRANSITION_LEAVE_DURATION -- transition should last for that time in seconds
else
transitionLeaveScale = 1
end
transitionLeaveReappearTimer = 1
transitionAfterscrollScale = 0 -- Keep songwheel in the "afterscroll" state while the filterwheel is active
transitionJacketBgScrollScale = 0 -- Same thing here, just with the jacket bg
else
if (transitionLeaveReappearTimer ~= 0) then
transitionAfterscrollScale = 0 -- Keep songwheel in the "afterscroll" state while we're waiting on filter wheel to fade out
transitionJacketBgScrollScale = 0 -- Same thing here, just with the jacket bg
end
transitionLeaveReappearTimer = transitionLeaveReappearTimer - deltaTime / (TRANSITION_LEAVE_DURATION + 0.05) -- 0.05s is a few frames between the completetion of the fade out and songs reappearing in the AC
if (transitionLeaveReappearTimer <= 0) then
transitionLeaveScale = 0
transitionLeaveReappearTimer = 0
end
end
end
---This function is basically a workaround for the ForceRender call
local function drawRadar()
if not showEffectRadar then return end
if isFilterWheelActive or transitionLeaveScale ~= 0 then return end
local x, y = 375, 650
local scale = 0.666
gfx.FontSize(28)
gfx.Translate(x, y)
gfx.Scale(scale, scale)
local strokeColor = ColorRGBA.new(255, 255, 255, 128)
local fillColor = ColorRGBA.new(0, 0, 0, 191)
gfx.ResetScissor()
radar:drawBackground(fillColor)
radar:drawOutline(3, strokeColor)
--Bug: ForceRender resets every transformation, need to re-setup view transform afterwards
--ForceRender also resets gfx stack, USC will crash if you try to call gfx.Restore(),
--make sure the gfx stack is clean before calling radar:drawRadarMesh()
radar:drawRadarMesh()
Dim.transformToScreenSpace()
gfx.Save()
gfx.Translate(x, y)
gfx.Scale(scale, scale)
radar:drawRadialTicks(strokeColor)
radar:drawAttributes()
gfx.Restore()
end
local draw_songwheel = function(deltaTime)
drawBackground(deltaTime)
drawSongList()
isFilterWheelActive = game.GetSkinSetting('_songWheelOverlayActive') == 1
drawData()
drawRadar()
drawCursor()
drawFilterInfo(deltaTime)
drawSearch()
drawScrollbar()
gfx.BeginPath()
gfx.FontSize(18)
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_TOP)
local debugScrollingUp= "FALSE"
if scrollingUp then debugScrollingUp = "TRUE" end
if game.GetSkinSetting('debug_showInformation') then
gfx.Text('S_I: ' .. selectedIndex .. ' // S_D: ' .. selectedDifficulty .. ' // S_UP: ' .. debugScrollingUp .. ' // AC_TS: ' .. transitionAfterscrollScale .. ' // L_TS: ' .. transitionLeaveScale .. ' // IR_CODE: ' .. irRequestStatus .. ' // IR_T: ' .. irRequestTimeout, 8, 8)
end
Footer.draw(deltaTime)
gfx.ResetTransform()
end
---@diagnostic disable-next-line:lowercase-global
render = function (deltaTime)
tickTransitions(deltaTime)
game.SetSkinSetting('_currentScreen', 'songwheel')
Sound.stopMusic()
if showEffectRadar and updateRadar then
local difficultyNames = {"nov","adv","exh","mxm","inf","grv","hvn","vvd","exc"}
local diff = songwheel.songs[selectedIndex].difficulties[selectedDifficulty].difficulty + 1
radar:updateGraph(songwheel.songs[selectedIndex].path, difficultyNames[diff])
updateRadar = false
end
Dim.updateResolution()
Wallpaper.render()
Dim.transformToScreenSpace()
draw_songwheel(deltaTime)
refreshIrLeaderboard(deltaTime)
end
---@diagnostic disable-next-line:lowercase-global
songs_changed = function (withAll)
irLeaderboardsCache = {} -- Reset LB cache
if not withAll then return end
game.SetSkinSetting('_songWheelScrollbarTotal', #songwheel.songs)
game.SetSkinSetting('_songWheelScrollbarIndex', selectedIndex)
local diffs = {}
for i = 1, #songwheel.allSongs do
local song = songwheel.allSongs[i]
for j = 1, #song.difficulties do
local diff = song.difficulties[j]
table.insert(diffs, {hash = diff.hash, force = VolforceCalc.calc(diff)})
end
end
table.sort(diffs, function (l, r)
return l.force > r.force
end)
local totalForce = 0
for i = 1, 50 do
local diff = diffs[i]
if not diff then
break
end
top50diffs[diff.hash] = true
totalForce = totalForce + diff.force
end
game.SetSkinSetting('_volforce', totalForce)
end
---@diagnostic disable-next-line:lowercase-global
set_index = function(newIndex)
transitionScrollScale = 0
transitionAfterscrollScale = 0
transitionJacketBgScrollScale = 0
Footer.resetTimer()
game.SetSkinSetting('_songWheelScrollbarTotal', #songwheel.songs)
game.SetSkinSetting('_songWheelScrollbarIndex', newIndex)
scrollingUp = false
if ((newIndex > selectedIndex and not (newIndex == #songwheel.songs and selectedIndex == 1)) or (newIndex == 1 and selectedIndex == #songwheel.songs)) then
scrollingUp = true
end
updateRadar = true
game.PlaySample('song_wheel/cursor_change.wav')
selectedIndex = newIndex
end
local json = require("common.json")
---@diagnostic disable-next-line:lowercase-global
set_diff = function(newDiff)
if newDiff ~= selectedDifficulty then
jacketCache = {} -- Clear the jacket cache for the new diff jackets
game.PlaySample('song_wheel/diff_change.wav')
end
Footer.resetTimer()
updateRadar = true
selectedDifficulty = newDiff
irLeaderboard = {}
irRequestStatus = 1
irRequestTimeout = 2
end