local Numbers = require("common.numbers") local DiffRectangle = require("components.diff_rectangle") -- Horizontal alignment TEXT_ALIGN_LEFT = 1 TEXT_ALIGN_CENTER = 2 TEXT_ALIGN_RIGHT = 4 -- Vertical alignment TEXT_ALIGN_TOP = 8 TEXT_ALIGN_MIDDLE = 16 TEXT_ALIGN_BOTTOM = 32 TEXT_ALIGN_BASELINE = 64 local jacket = nil; local selectedIndex = 1 local selectedDiff = 1 local challengeCache = {} local doffset = 0 local soffset = 0 local diffColors = { { 0, 0, 255 }, { 0, 255, 0 }, { 255, 0, 0 }, { 255, 0, 255 } } local timer = 0 local scrollmul = 0 local scrollmulOffset = 0 -- bc we have min/max the game doesn't know we have to account for extra local effector = 0 local searchText = gfx.CreateLabel("", 5, 0) local searchIndex = 1 local backgroundImage = gfx.CreateSkinImage("bg_pattern.png", gfx.IMAGE_REPEATX | gfx.IMAGE_REPEATY) local challengeBGImage = gfx.CreateSkinImage("challenge_select/bg.png", 0) local challengeCardBGImage = gfx.CreateSkinImage("challenge_select/small_box.png", 0) local jacketFallback = gfx.CreateSkinImage("song_select/loading.png", 0) local showGuide = game.GetSkinSetting("show_guide") local legendTable = { { ["labelSingleLine"] = gfx.CreateLabel("SCROLL INFO", 16, 0), ["labelMultiLine"] = gfx.CreateLabel("SCROLL\nINFO", 16, 0), ["image"] = gfx.CreateSkinImage("legend/knob-left.png", 0) }, { ["labelSingleLine"] = gfx.CreateLabel("CHALL SELECT", 16, 0), ["labelMultiLine"] = gfx.CreateLabel("CHALLENGE\nSELECT", 16, 0), ["image"] = gfx.CreateSkinImage("legend/knob-right.png", 0) }, { ["labelSingleLine"] = gfx.CreateLabel("FILTER CHALLS", 16, 0), ["labelMultiLine"] = gfx.CreateLabel("FILTER\nCHALLENGES", 16, 0), ["image"] = gfx.CreateSkinImage("legend/FX-L.png", 0) }, { ["labelSingleLine"] = gfx.CreateLabel("SORT CHALLS", 16, 0), ["labelMultiLine"] = gfx.CreateLabel("SORT\nCHALLENGES", 16, 0), ["image"] = gfx.CreateSkinImage("legend/FX-R.png", 0) }, { ["labelSingleLine"] = gfx.CreateLabel("GAME SETTINGS", 16, 0), ["labelMultiLine"] = gfx.CreateLabel("GAME\nSETTINGS", 16, 0), ["image"] = gfx.CreateSkinImage("legend/FX-LR.png", 0) }, { ["labelSingleLine"] = gfx.CreateLabel("PLAY", 16, 0), ["labelMultiLine"] = gfx.CreateLabel("PLAY", 16, 0), ["image"] = gfx.CreateSkinImage("legend/start.png", 0) } } local grades = { { ["max"] = 6999999, ["image"] = gfx.CreateSkinImage("common/grades/D.png", 0) }, { ["max"] = 7999999, ["image"] = gfx.CreateSkinImage("common/grades/C.png", 0) }, { ["max"] = 8699999, ["image"] = gfx.CreateSkinImage("common/grades/B.png", 0) }, { ["max"] = 8999999, ["image"] = gfx.CreateSkinImage("common/grades/A.png", 0) }, { ["max"] = 9299999, ["image"] = gfx.CreateSkinImage("common/grades/A+.png", 0) }, { ["max"] = 9499999, ["image"] = gfx.CreateSkinImage("common/grades/AA.png", 0) }, { ["max"] = 9699999, ["image"] = gfx.CreateSkinImage("common/grades/AA+.png", 0) }, { ["max"] = 9799999, ["image"] = gfx.CreateSkinImage("common/grades/AAA.png", 0) }, { ["max"] = 9899999, ["image"] = gfx.CreateSkinImage("common/grades/AAA+.png", 0) }, { ["max"] = 99999999, ["image"] = gfx.CreateSkinImage("common/grades/S.png", 0) } } local badges = { 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 passStates = { gfx.CreateSkinImage("challenge_select/pass_states/not_played.png", 0), gfx.CreateSkinImage("challenge_select/pass_states/failed.png", 0), gfx.CreateSkinImage("challenge_select/pass_states/cleared.png", 0) } local scoreNumber = Numbers.load_number_image("score_num") local difficultyLabelImages = { gfx.CreateSkinImage("diff/1 novice.png", 0), gfx.CreateSkinImage("diff/2 advanced.png", 0), gfx.CreateSkinImage("diff/3 exhaust.png", 0), gfx.CreateSkinImage("diff/4 maximum.png", 0), gfx.CreateSkinImage("diff/5 infinite.png", 0), gfx.CreateSkinImage("diff/6 gravity.png", 0), gfx.CreateSkinImage("diff/7 heavenly.png", 0), gfx.CreateSkinImage("diff/8 vivid.png", 0) } local difficultyLabelText = { "NOV", "ADV", "EXH", "MXM", "INF", "GRV", "HVN", "VVD" } gfx.LoadSkinFont("divlit_custom.ttf") gfx.LoadSkinFont("dfmarugoth.ttf"); game.LoadSkinSample("menu_click") game.LoadSkinSample("click-02") game.LoadSkinSample("woosh") local wheelSize = 5 get_page_size = function() return math.floor(wheelSize / 2) end -- 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 resolutionChange = function(x, y) resX = x resY = y fullX = portraitWidescreenRatio * y fullY = y end local update_cache_labels = function(challenge, titleFontSize) if challengeCache[challenge.id] then local _, fontsize = gfx.LabelSize(challengeCache[challenge.id]["title"]) if fontsize ~= titleFontSize then gfx.UpdateLabel(challengeCache[challenge.id]["title"], challengeCache[challenge.id]["title_raw"], titleFontSize) for _, chart in ipairs(challengeCache[challenge.id]["charts"]) do gfx.UpdateLabel(chart["title"], chart["title_raw"], titleFontSize) end end end end local check_or_create_cache = function(challenge) local defaultLabelSize = 16 if not challengeCache[challenge.id] then challengeCache[challenge.id] = {} end if not challengeCache[challenge.id]["title"] then challengeCache[challenge.id]["title"] = gfx.CreateLabel(challenge.title, defaultLabelSize, 0) challengeCache[challenge.id]["title_raw"] = challenge.title end if not challengeCache[challenge.id]["charts"] then if challenge.missing_chart then local missing_text = "*COULD NOT FIND ALL CHARTS!*" challengeCache[challenge.id]["charts"] = { { ["title"] = gfx.CreateLabel(missing_text, defaultLabelSize, 0), ["title_raw"] = missing_text, ["level"] = 0, ["difficulty"] = 0 } } end local charts = {} for _, chart in ipairs(challenge.charts) do table.insert(charts, { ["title"] = gfx.CreateLabel(chart.title, defaultLabelSize, 0), ["title_raw"] = chart.title, ["level"] = chart.level, ["difficulty"] = chart.difficulty }) end challengeCache[challenge.id]["charts"] = charts end if (not challengeCache[challenge.id]["percent"] or not challengeCache[challenge.id]["total_score"] or challengeCache[challenge.id]["total_score"] ~= challenge.bestScore) then challengeCache[challenge.id]["percent"] = math.max(0, (challenge.bestScore - 8000000) // 10000) challengeCache[challenge.id]["total_score"] = challenge.bestScore end if not challengeCache[challenge.id]["pass_state"] then local passState = math.min(challenge.topBadge, 2) + 1 -- challenge.topBadge -> [1, 3] challengeCache[challenge.id]["pass_state"] = passStates[passState] end if not challengeCache[challenge.id]["jackets"] then local jackets = {} for i, chart in ipairs(challenge.charts) do jackets[i] = gfx.LoadImageJob(chart.jacketPath, jacketFallback, 200, 200) end if #jackets == 0 then jackets[1] = jacketFallback end challengeCache[challenge.id]["jackets"] = jackets end end draw_challenge = function(challenge, x, y, w, h, selected) if not challenge then return end check_or_create_cache(challenge) local _draw_card_bg = function() gfx.BeginPath() gfx.ImageRect(x, y, w, h, challengeCardBGImage, 1, 0) end local _draw_info = function() local stateLabel = challengeCache[challenge.id]["pass_state"] local stateLabelWidth, stateLabelHeight = gfx.ImageSize(stateLabel) local stateLabelAspect = stateLabelWidth / stateLabelHeight local stateWidth = w / 5 local stateHeight = stateWidth / stateLabelAspect local stateOffsetX = x + w / 32 local stateOffsetY = y + h / 32 local titleMargin = 6 local titleFontSize = math.floor(0.11 * h) -- must be an integer local titleMaxWidth = 6 / 9 * w local titleCenterX = x + w - titleMargin - titleMaxWidth / 2 -- align right local titleBaselineY = y + 0.025 * h -- align baseline if selected then stateWidth = stateWidth / 0.9 stateHeight = stateHeight / 0.9 stateOffsetY = y + h / 16 titleFontSize = math.floor(0.075 * h) -- must be an integer titleMaxWidth = 3 / 5 * w titleCenterX = x + w - titleMargin - titleMaxWidth / 2 -- align right titleBaselineY = y + 0.09 * h -- align baseline end update_cache_labels(challenge, titleFontSize) gfx.BeginPath() gfx.ImageRect(stateOffsetX, stateOffsetY, stateWidth, stateHeight, stateLabel, 1, 0) gfx.FontFace("divlit_custom.ttf") gfx.TextAlign(gfx.TEXT_ALIGN_CENTER | gfx.TEXT_ALIGN_BASELINE) gfx.DrawLabel(challengeCache[challenge.id]["title"], titleCenterX, titleBaselineY, titleMaxWidth) end local _draw_jacket = function() local size = h * 0.68 local offsetX = x + w / 32 local offsetY = y + h - size - h * 0.05 if not selected then size = h * 0.66 offsetX = x + w * 0.058 offsetY = y + h - size - h * 0.066 end gfx.BeginPath() gfx.ImageRect(offsetX, offsetY, size, size, challengeCache[challenge.id]["jackets"][1], 1, 0) end local _draw_stats = function() local textSizeCorrection = h / gfx.ImageSize(scoreNumber[1]) local percentOffsetX = x + 6 / 12 * w local percentOffsetY = y + 0.87 * h local percentSize = 0.17 * textSizeCorrection local scoreUpperOffsetX = 0 local scoreUpperOffsetY = 0 local scoreOffsetX = x + w * 0.74 local scoreOffsetY = y + h * 0.9 local scoreUpperSize = 0.2 * textSizeCorrection local scoreSize = 0.125 * textSizeCorrection local percent = challengeCache[challenge.id]["percent"] local scoreUpper = math.floor(challengeCache[challenge.id]["total_score"] / 10000) local score = challengeCache[challenge.id]["total_score"] if selected then percentOffsetX = x + 11 / 24 * w percentOffsetY = y + 49 / 64 * h percentSize = 0.12 * textSizeCorrection scoreUpperOffsetX = x + w * 0.63 scoreUpperOffsetY = y + h * 0.82 scoreOffsetX = x + w * 0.755 scoreOffsetY = y + h * 0.835 scoreUpperSize = 0.12 * textSizeCorrection scoreSize = 0.09 * textSizeCorrection end Numbers.draw_number(percentOffsetX, percentOffsetY, 1, percent, 3, scoreNumber, true, percentSize, 1) -- TODO: Missing percentage character if selected then Numbers.draw_number( scoreUpperOffsetX, scoreUpperOffsetY, 1, scoreUpper, 4, scoreNumber, true, scoreUpperSize, 1 ) Numbers.draw_number(scoreOffsetX, scoreOffsetY, 1, score, 4, scoreNumber, true, scoreSize, 1) else Numbers.draw_number(scoreOffsetX, scoreOffsetY, 1, score, 8, scoreNumber, true, scoreSize, 1) end -- TODO: Missing completion bar -- TODO: Missing completion badge (COMP, HARD, UC, PUC) -- TODO: Missing completion grade (S+, S, AAA+, etc.) end local _draw_charts = function() local diffIconHeight = 0.124 * h local diffIconWidth = 140 / 31 * diffIconHeight -- only scaling, no stretching textures -- diffIconWidth = 0.8 * diffIconWidth -- okay, maybe a little squishing must be done local paddingY = 0.18 * h local offsetX = x + 0.252 * w local offsetY = y + 0.246 * h local titleMargin = 6 local titleMaxWidth = 6 / 9 * w local titleCenterX = x + w - titleMargin - titleMaxWidth / 2 -- align right if selected then diffIconHeight = h / 12 diffIconWidth = 140 / 31 * diffIconHeight -- only scaling, no stretching textures -- diffIconWidth = 0.8 * diffIconWidth -- okay, maybe a little squishing must be done paddingY = 0.122 * h offsetX = x + 0.268 * w offsetY = y + 0.256 * h titleFontSize = math.floor(0.075 * h) -- must be an integer titleMaxWidth = 3 / 5 * w titleCenterX = x + w - titleMargin - titleMaxWidth / 2 -- align right end for i, chart in ipairs(challengeCache[challenge.id]["charts"]) do local ypos = offsetY + paddingY * (i - 1) draw_diff_icon(chart, offsetX, ypos, diffIconWidth, diffIconHeight, selected) gfx.TextAlign(gfx.TEXT_ALIGN_CENTER | gfx.TEXT_ALIGN_MIDDLE) gfx.DrawLabel(chart.title, titleCenterX, ypos + diffIconHeight / 2, titleMaxWidth) end end if not selected then _draw_card_bg() end _draw_info() _draw_jacket() _draw_stats() _draw_charts() gfx.ForceRender() end draw_diff_icon = function(diff, x, y, w, h) -- cut out the transparent part of the diff background image local boxWidth = 140 / 122 * w local boxHeight = 31 / 27 * h local boxOffsetX = x - (boxWidth - w) / 2 local boxOffsetY = y - (boxHeight - h) / 2 local labelOffsetX = w / 12 local labelTextOffsetY = h / 12 gfx.BeginPath() gfx.ImageRect(boxOffsetX, boxOffsetY, boxWidth, boxHeight, difficultyLabelImages[diff.difficulty + 1], 1, 0) gfx.FontFace("dfmarugoth.ttf") gfx.FontSize(math.floor(0.6 * h)) gfx.TextAlign(gfx.TEXT_ALIGN_LEFT | gfx.TEXT_ALIGN_MIDDLE) gfx.FastText(difficultyLabelText[diff.difficulty + 1], x + labelOffsetX, y + (h / 2) - labelTextOffsetY) gfx.FontSize(math.floor(0.8 * h)) gfx.TextAlign(gfx.TEXT_ALIGN_RIGHT | gfx.TEXT_ALIGN_MIDDLE) gfx.FastText(tostring(diff.level), x + w - labelOffsetX, y + (h / 2) - labelTextOffsetY) end draw_selected = function(challenge, x, y, w, h) if not challenge then return end check_or_create_cache(challenge) draw_challenge(challenge, x, y, w, h, true) end draw_chalwheel = function(x, y, w, h) local challengeAspect = 4.367 local selectedChallengeAspect = 3.305 local width = math.floor(w * 0.839) local height = math.floor(width / challengeAspect) local selectedWidth = math.floor(w * 0.944) local selectedHeight = math.floor(selectedWidth / selectedChallengeAspect) local offsetX = w / 2 - width / 2 -- center local centerY = h / 2 - height / 2 local selectedOffsetX = w / 2 - selectedWidth / 2 local selectedCenterY = h / 2 - selectedHeight / 2 local margin = h / 128 local centerMargin = h / 100 local imin = math.ceil(selectedIndex - wheelSize / 2) local imax = math.floor(selectedIndex + wheelSize / 2) for i = math.max(imin, 1), math.min(imax, #chalwheel.challenges) do local current = selectedIndex - i if not (current == 0) then local challenge = chalwheel.challenges[i] local xpos = x + offsetX -- local offsetY = current * (height - (wheelSize / 2 * (current * aspectFloat))) local offsetY = math.abs(current) * (height + margin) + (selectedHeight - height) / 2 local ypos = y + centerY if current < 0 then ypos = ypos + centerMargin + offsetY else -- if current > 0 then ypos = ypos - centerMargin - offsetY end draw_challenge(challenge, xpos, ypos, width, height) end end -- render selected song information local xpos = x + selectedOffsetX local ypos = y + selectedCenterY draw_selected(chalwheel.challenges[selectedIndex], xpos, ypos, selectedWidth, selectedHeight) end draw_legend_pane = function(x, y, w, h, obj) local xpos = x + 5 local ypos = y local imageSize = h gfx.BeginPath() gfx.TextAlign(gfx.TEXT_ALIGN_MIDDLE + gfx.TEXT_ALIGN_LEFT) gfx.ImageRect(x, y, imageSize, imageSize, obj.image, 1, 0) xpos = xpos + imageSize + 5 gfx.FontSize(16); if h < (w - (10 + imageSize)) / 2 then gfx.DrawLabel(obj.labelSingleLine, xpos, y + (h / 2), w - (10 + imageSize)) else gfx.DrawLabel(obj.labelMultiLine, xpos, y + (h / 2), w - (10 + imageSize)) end gfx.ForceRender() end draw_legend = function(x, y, w, h) gfx.TextAlign(gfx.TEXT_ALIGN_MIDDLE + gfx.TEXT_ALIGN_LEFT); gfx.BeginPath() gfx.FillColor(0, 0, 0, 170) gfx.Rect(x, y, w, h) gfx.Fill() local xpos = 10; local legendWidth = math.floor((w - 20) / #legendTable) for i, v in ipairs(legendTable) do local xOffset = draw_legend_pane(xpos + (legendWidth * (i - 1)), y + 5, legendWidth, h - 10, legendTable[i]) end end draw_search = function(x, y, w, h) soffset = soffset + (searchIndex) - (chalwheel.searchInputActive and 0 or 1) if searchIndex ~= (chalwheel.searchInputActive and 0 or 1) then game.PlaySample("woosh") end searchIndex = chalwheel.searchInputActive and 0 or 1 gfx.BeginPath() local bgfade = 1 - (searchIndex + soffset) -- if not chalwheel.searchInputActive then bgfade = soffset end gfx.FillColor(0, 0, 0, math.floor(200 * bgfade)) gfx.Rect(0, 0, resx, resy) gfx.Fill() gfx.ForceRender() local xpos = x + (searchIndex + soffset) * w gfx.UpdateLabel(searchText, string.format("Search: %s", chalwheel.searchText), 30) gfx.BeginPath() gfx.RoundedRect(xpos, y, w, h, h / 2) gfx.FillColor(30, 30, 30) gfx.StrokeColor(0, 128, 255) gfx.StrokeWidth(1) gfx.Fill() gfx.Stroke() gfx.BeginPath(); gfx.LoadSkinFont("dfmarugoth.ttf"); gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_MIDDLE); gfx.DrawLabel(searchText, xpos + 10, y + (h / 2), w - 20) end render = function(deltaTime) -- detect resolution change local resx, resy = game.GetResolution(); if resx ~= resX or resy ~= resY then resolutionChange(resx, resy) end -- draw background image gfx.BeginPath() bgImageWidth, bgImageHeight = gfx.ImageSize(backgroundImage) gfx.Rect(0, 0, resX, resY) gfx.FillPaint(gfx.ImagePattern(0, 0, bgImageWidth, bgImageHeight, 0, backgroundImage, 0.2)) gfx.Fill() if chalwheel.challenges and chalwheel.challenges[1] then -- draw surface background gfx.BeginPath() gfx.ImageRect((resX - fullX) / 2, 0, fullX, fullY, challengeBGImage, 1, 0) -- draw chalwheel gfx.BeginPath(); draw_chalwheel((resX - fullX) / 2, 0, fullX, fullY) -- Draw Legend Information --[[ if showGuide then draw_legend(0, (fifthY / 3) * 14, fullX, (fifthY / 3) * 1) end --]] -- draw text search --[[ draw_search(fifthX * 2, 5, fifthX * 3, fifthY / 5) doffset = doffset * 0.9 soffset = soffset * 0.8 if chalwheel.searchStatus then gfx.BeginPath() gfx.FillColor(255, 255, 255) gfx.FontSize(20); gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_TOP) gfx.Text(chalwheel.searchStatus, 3, 3) end gfx.LoadSkinFont("dfmarugoth.ttf"); gfx.ResetTransform() gfx.ForceRender() --]] end end set_index = function(newIndex, scrollamt) if newIndex ~= selectedIndex then game.PlaySample("menu_click") scrollmulOffset = 0 end selectedIndex = newIndex scrollmul = scrollamt + scrollmulOffset end; local badgeRates = { 0.5, -- Played 1.0, -- Cleared 1.02, -- Hard clear 1.04, -- UC 1.1 -- PUC } local gradeRates = { { ["min"] = 9900000, ["rate"] = 1.05 }, -- S { ["min"] = 9800000, ["rate"] = 1.02 }, -- AAA+ { ["min"] = 9700000, ["rate"] = 1 }, -- AAA { ["min"] = 9500000, ["rate"] = 0.97 }, -- AA+ { ["min"] = 9300000, ["rate"] = 0.94 }, -- AA { ["min"] = 9000000, ["rate"] = 0.91 }, -- A+ { ["min"] = 8700000, ["rate"] = 0.88 }, -- A { ["min"] = 7500000, ["rate"] = 0.85 }, -- B { ["min"] = 6500000, ["rate"] = 0.82 }, -- C { ["min"] = 0, ["rate"] = 0.8 } -- D } challenges_changed = function(withAll) if not withAll then return end end