From 7768ae21751bc2bf8dde9dbd6a6108342c2288b1 Mon Sep 17 00:00:00 2001 From: Hersi Date: Mon, 13 Nov 2023 01:41:52 +0100 Subject: [PATCH 01/12] initial radar implementation --- scripts/api/color.lua | 74 ++++++ scripts/api/point2d.lua | 28 ++ scripts/common/util.lua | 5 + scripts/components/radar.lua | 443 +++++++++++++++++++++++++++++++ scripts/songselect/songwheel.lua | 66 ++++- shaders/radar.fs | 66 +++++ shaders/radar.vs | 22 ++ shaders/radarVertex.fs | 11 + shaders/radarVertex.vs | 94 +++++++ 9 files changed, 800 insertions(+), 9 deletions(-) create mode 100644 scripts/api/color.lua create mode 100644 scripts/api/point2d.lua create mode 100644 scripts/components/radar.lua create mode 100644 shaders/radar.fs create mode 100644 shaders/radar.vs create mode 100644 shaders/radarVertex.fs create mode 100644 shaders/radarVertex.vs diff --git a/scripts/api/color.lua b/scripts/api/color.lua new file mode 100644 index 0000000..5424e70 --- /dev/null +++ b/scripts/api/color.lua @@ -0,0 +1,74 @@ +local util = require("common.util") + +---@class CColorRGBA +ColorRGBA = { + ---Create a new Color instance + ---@param r integer # red or monochrome value + ---@param g? integer # green value + ---@param b? integer # blue value + ---@param a? integer # alpha value, default 255 + ---@return ColorRGBA + new = function (r, g , b, a) + ---@class ColorRGBA : CColorRGBA + ---@field r integer + ---@field g integer + ---@field b integer + ---@field a integer + local o = { + r = r or 0, + g = g or r, + b = b or r, + a = a or 255, + } + + setmetatable(o, ColorRGBA) + return o + end, + + ---Mix two colors + ---@param color1 ColorRGBA + ---@param color2 ColorRGBA + ---@param factor number + ---@return ColorRGBA + mix = function (color1, color2, factor) + local r = math.floor(util.mix(color1.r, color2.r, factor)) + local g = math.floor(util.mix(color1.g, color2.g, factor)) + local b = math.floor(util.mix(color1.b, color2.b, factor)) + local a = math.floor(util.mix(color1.a, color2.a, factor)) + return ColorRGBA.new(r, g, b, a) + end +} + +ColorRGBA.__index = ColorRGBA +ColorRGBA.BLACK = ColorRGBA.new(0) +ColorRGBA.GREY = ColorRGBA.new(128) +ColorRGBA.WHITE = ColorRGBA.new(255) +ColorRGBA.RED = ColorRGBA.new(255, 0, 0) +ColorRGBA.GREEN = ColorRGBA.new(0, 255, 0) +ColorRGBA.BLUE = ColorRGBA.new(0, 0, 255) +ColorRGBA.YELLOW = ColorRGBA.new(255, 255, 0) +ColorRGBA.CYAN = ColorRGBA.new(0, 255, 255) +ColorRGBA.MAGENTA = ColorRGBA.new(255, 0, 255) + +---Split to components +---@return integer # red +---@return integer # green +---@return integer # blue +---@return integer # alpha +function ColorRGBA:components() + ---@cast self ColorRGBA + + return self.r, self.g, self.b, self.a +end + +---Split to components scaled to [0.0, 1.0] +---@return number # red +---@return number # green +---@return number # blue +---@return number # alpha +function ColorRGBA:componentsFloat() + ---@cast self ColorRGBA + local scale = 255 + + return self.r / scale, self.g / scale, self.b / scale, self.a / scale +end diff --git a/scripts/api/point2d.lua b/scripts/api/point2d.lua new file mode 100644 index 0000000..84cfdf1 --- /dev/null +++ b/scripts/api/point2d.lua @@ -0,0 +1,28 @@ +---@class CPoint2D +Point2D = { + ---Create a Point2D instance + ---@param x? number # default 0.0 + ---@param y? number # default 0.0 + ---@return Point2D + new = function(x, y) + ---@class Point2D : CPoint2D + ---@field x number + ---@field y number + local o = { + x = x + .0 or .0, + y = y + .0 or .0, + } + + setmetatable(o, Point2D) + return o + end +} + +Point2D.__index = Point2D +Point2D.ZERO = Point2D.new(0, 0) + +function Point2D:coords() + ---@cast self Point2D + + return self.x, self.y +end diff --git a/scripts/common/util.lua b/scripts/common/util.lua index e7ab595..7f10481 100644 --- a/scripts/common/util.lua +++ b/scripts/common/util.lua @@ -59,6 +59,10 @@ local function lerp(x, x0, y0, x1, y1) return y0 + (x - x0) * (y1 - y0) / (x1 - x0) end +local function mix(x, y, a) + return (1 - a) * x + a * y +end + --modulo operation for index value local function modIndex(index, mod) return (index - 1) % mod + 1 @@ -84,6 +88,7 @@ return { roundToZero = roundToZero, areaOverlap = areaOverlap, lerp = lerp, + mix = mix, modIndex = modIndex, firstAlphaNum = firstAlphaNum, } diff --git a/scripts/components/radar.lua b/scripts/components/radar.lua new file mode 100644 index 0000000..75f77ae --- /dev/null +++ b/scripts/components/radar.lua @@ -0,0 +1,443 @@ +--[[ +S2 song attribute radar component +Original code thanks to RealFD, he's a real homie +]] + +require("common.globals") +require("api.point2d") +require("api.color") + +local Dim = require("common.dimensions") + +Dim.updateResolution() + +local RADAR_PURPLE = ColorRGBA.new(238, 130, 238) +local RADAR_MAGENTA = ColorRGBA.new(191, 70, 235) +local RADAR_GREEN = ColorRGBA.new(0, 255, 100) + +---@param p1 Point2D +---@param p2 Point2D +---@param width number +---@param color ColorRGBA +local function drawLine(p1, p2, width, color) + gfx.BeginPath() + gfx.MoveTo(p1:coords()) + gfx.LineTo(p2:coords()) + gfx.StrokeColor(color:components()) + gfx.StrokeWidth(width) + gfx.Stroke() +end + +---@param pos Point2D +---@param text string +---@param outlineWidth number +---@param color ColorRGBA +local function renderOutlinedText(pos, text, outlineWidth, color) + local x, y = pos:coords() + + gfx.FillColor(ColorRGBA.BLACK:components()); + gfx.Text(text, x - outlineWidth, y + outlineWidth); + gfx.Text(text, x - outlineWidth, y - outlineWidth); + gfx.Text(text, x + outlineWidth, y + outlineWidth); + gfx.Text(text, x + outlineWidth, y - outlineWidth); + + gfx.FillColor(color:components()); + gfx.Text(text, x, y); +end + +---@param pos Point2D +---@param graphdata table +local function drawDebugText(pos, graphdata) + local color = ColorRGBA.WHITE + gfx.Save() + gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_CENTER) + --renderOutlinedText(x, 20, '"' .. txtFilePath .. '"', 1, 255, 255, 255) + renderOutlinedText(pos, "NOTES = " .. graphdata.notes, 1, color) + renderOutlinedText(pos, "PEAK = " .. graphdata.peak, 1, color) + renderOutlinedText(pos, "TSUMAMI = " .. graphdata.tsumami, 1, color) + renderOutlinedText(pos, "TRICKY = " .. graphdata.tricky, 1, color) + renderOutlinedText(pos, "ONE-HAND = " .. graphdata.onehand, 1, color) + renderOutlinedText(pos, "HAND-TRIP = " .. graphdata.handtrip, 1, color) + --renderOutlinedText(pos, "NOTES (Relative) = " .. graphdata.notes_relative, 1, color) + --renderOutlinedText(pos, "TOTAL-MESURES = " .. graphdata.measures, 1, color) + gfx.Restore() +end + +---@class CRadarAttributes +RadarAttributes = { + ---Create RadarAttributes instance + ---@param text? string # default "" + ---@param pos? Point2D # default (0, 0) + ---@param color? ColorRGBA # default BLACK + ---@return RadarAttributes + new = function (text, pos, color) + ---@class RadarAttributes + ---@field text string + ---@field pos Point2D + ---@field color ColorRGBA + local o = { + text = text or "", + pos = pos or Point2D.ZERO, + color = color or ColorRGBA.BLACK + } + + setmetatable(o, RadarAttributes) + return o + end +} +RadarAttributes.__index = RadarAttributes + + +---@class CRadar +Radar = { + ---@type RadarAttributes[][] + ATTRIBUTES = { + {RadarAttributes.new("notes", Point2D.new(0, -30), ColorRGBA.CYAN),}, + {RadarAttributes.new("peak", Point2D.new(40, -20), ColorRGBA.RED),}, + {RadarAttributes.new("tsumami", Point2D.new(40, 20), RADAR_PURPLE),}, + {RadarAttributes.new("tricky", Point2D.new(0, 30), ColorRGBA.YELLOW),}, + { + RadarAttributes.new("hand", Point2D.new(-40, 20), RADAR_MAGENTA), + RadarAttributes.new("trip", Point2D.new(5, 18), RADAR_MAGENTA), + }, + { + RadarAttributes.new("one", Point2D.new(-40, -20), RADAR_GREEN), + RadarAttributes.new("hand", Point2D.new(-5, 18), RADAR_GREEN), + } + }, + RADIUS = 100.0, + + ---Create Radar instance + ---@param pos Point2D + ---@param radius? number + ---@return Radar + new = function (pos, radius) + ---@class Radar : CRadar + local o = { + _graphdata = { + notes = 0, + peak = 0, + tsumami = 0, + tricky = 0, + handtrip = 0, + onehand = 0, + }, + _hexagonMesh = gfx.CreateShadedMesh("radar"), + outlineVertices = {}, + attributePositions = {}, + pos = pos or Point2D.ZERO, + scale = radius and radius / Radar.RADIUS or 1.0, + } + + local sides = #Radar.ATTRIBUTES + local angleStep = (2 * math.pi) / sides + local rotationAngle = angleStep / 2 + + local outlineRadius = Radar.RADIUS + local attributeRadius = Radar.RADIUS + 20 + + for i = 0, sides - 1 do + local attrIdx = i + 1 + local angle = i * angleStep - rotationAngle + local cosAngle = math.cos(angle) + local sinAngle = math.sin(angle) + -- cache outline vertices + table.insert(o.outlineVertices, Point2D.new(outlineRadius * cosAngle, outlineRadius * sinAngle)) + -- cache attribute positions + table.insert(o.attributePositions, {}) + for j = 1, #Radar.ATTRIBUTES[attrIdx] do + local attr = Radar.ATTRIBUTES[attrIdx][j] + local attributePos = Point2D.new(attributeRadius * cosAngle, attributeRadius * sinAngle) + attributePos.x = attributePos.x + attr.pos.x + attributePos.y = attributePos.y + attr.pos.y + table.insert(o.attributePositions, j, attributePos) + end + end + + setmetatable(o, Radar) + return o + end, +} +Radar.__index = Radar + +---@param w number +---@param color ColorRGBA +function Radar:drawOutline(w, color) + ---@cast self Radar + + for i = 1, #self.outlineVertices do + local j = i % #self.outlineVertices + 1 + drawLine(self.outlineVertices[i], self.outlineVertices[j], w, color) + end +end + +---@param color ColorRGBA +---@param ticks? integer +function Radar:drawRadialTicks(color, ticks) + ---@cast self Radar + + ticks = ticks or 3 + + gfx.Save() + gfx.StrokeColor(color:components()) + + for i, vertex in ipairs(self.outlineVertices) do + gfx.BeginPath() + gfx.MoveTo(0, 0) + gfx.LineTo(vertex.x, vertex.y) + gfx.Stroke() + + local lineLength = math.sqrt(vertex.x * vertex.x + vertex.y * vertex.y) + local tinyLineLength = 10 + + local tinyLineAngle = math.atan(vertex.y / vertex.x) + if vertex.x < 0 then + tinyLineAngle = tinyLineAngle + math.pi + end + + local halfTinyLineLength = tinyLineLength / 2 + + for j = 1, ticks do + local distanceFromCenter = j * lineLength / (ticks + 1) -- Adjusted for 3 middle lines + + local offsetX = distanceFromCenter * (vertex.x / lineLength) + local offsetY = distanceFromCenter * (vertex.y / lineLength) + + local endX = halfTinyLineLength * math.cos(tinyLineAngle - math.pi / 2) -- Rotate by -90 degrees + local endY = halfTinyLineLength * math.sin(tinyLineAngle - math.pi / 2) -- Rotate by -90 degrees + + local offsetX2 = halfTinyLineLength * math.cos(tinyLineAngle + math.pi / 2) + local offsetY2 = halfTinyLineLength * math.sin(tinyLineAngle + math.pi / 2) + + gfx.BeginPath() + gfx.MoveTo(offsetX - offsetX2, offsetY - offsetY2) + gfx.LineTo(endX + offsetX + offsetX2 + offsetX2, endY + offsetY + offsetY2 + offsetY2) + gfx.Stroke() + end + end + + gfx.Restore() +end + +---@param fillColor ColorRGBA +function Radar:drawBackground(fillColor) + ---@cast self Radar + + gfx.Save() + + gfx.BeginPath() + gfx.MoveTo(self.outlineVertices[1].x, self.outlineVertices[1].y) + for i = 2, #self.outlineVertices do + gfx.LineTo(self.outlineVertices[i].x, self.outlineVertices[i].y) + end + gfx.ClosePath() + + gfx.FillColor(fillColor:components()) + gfx.Fill() + + gfx.Restore() +end + +function Radar:drawAttributes() + ---@cast self Radar + + gfx.Save() + + gfx.FontSize(20) + for i = 1, #self.attributePositions do + local attrPos = self.attributePositions[i] + for j = 1, #attrPos do + local pos = attrPos[j] + local attr = Radar.ATTRIBUTES[i][j] + gfx.TextAlign(gfx.TEXT_ALIGN_MIDDLE + gfx.TEXT_ALIGN_CENTER) + renderOutlinedText(pos, string.upper(attr.text), 1, attr.color) + end + end + + gfx.Restore() +end + +function Radar:drawRadarMesh() + ---@cast self Radar + + local scaleFact = { + self._graphdata.notes, + self._graphdata.peak, + self._graphdata.tsumami, + self._graphdata.tricky, + self._graphdata.handtrip, + self._graphdata.onehand, + } + + local color1 = ColorRGBA.new(255, 12, 48, 230) -- magenta-ish + local color2 = ColorRGBA.new(112, 119, 255, 230) -- light blue-ish purple + + local currentTime = 0 + + -- Calculate the maximum size based on the constraint + local maxSize = self.RADIUS * self.scale + 10 + local maxLineLength = maxSize + (maxSize / 2) + self._hexagonMesh:SetParam("maxSize", maxLineLength + .0) + + -- Set the color of the hexagon + self._hexagonMesh:SetParamVec4("colorMax", color1:componentsFloat()) + self._hexagonMesh:SetParamVec4("colorCenter", color2:componentsFloat()) + + -- Set the primitive type to triangles + self._hexagonMesh:SetPrimitiveType(self._hexagonMesh.PRIM_TRIFAN) + + -- Calculate the vertices of the hexagon + local sides = #Radar.ATTRIBUTES + local angleStep = (2 * math.pi) / sides + local rotationAngle = angleStep / 2 + --local rotationAngle = -math.pi / 2 + local vertices = {} + + table.insert(vertices, {{0, 0}, {0, 0}}) + for i = 0, sides do + local j = i % sides + 1 + local angle = i * angleStep - rotationAngle + + --local angle = math.rad(60 * (i-1)) + rotationAngle + local scale = scaleFact[j] + local lineLength = maxLineLength * scale + lineLength = math.min(lineLength, maxLineLength) -- Cap the length + local px = lineLength * math.cos(angle) + local py = lineLength * math.sin(angle) + table.insert(vertices, {{px, py}, {0, 0}}) + end + + -- Set the hexagon's vertices + self._hexagonMesh:SetData(vertices) + --self._hexagonMesh:SetPosition(-maxSize, maxSize) + self._hexagonMesh:Draw() + + --NOTE: Bug: forcerender resets every transformation, need to re-setup view transform afterwards + gfx.ForceRender() +end + +function Radar:drawGraph() + ---@cast self Radar + + gfx.Save() + gfx.Reset() + gfx.ResetScissor() + + Dim.updateResolution() + Dim.transformToScreenSpace() + + gfx.FontSize(28) + gfx.Translate(self.pos.x, self.pos.y) + gfx.Scale(self.scale, self.scale) + + local strokeColor = ColorRGBA.new(255, 255, 255, 100) + local fillColor = ColorRGBA.new(0, 0, 0, 191) + + self:drawBackground(strokeColor, fillColor) + self:drawOutline(3, ColorRGBA.WHITE) + self:drawRadarMesh() + self:drawRadialTicks() + self:drawAttributes() + + local pos = Point2D.new(self.pos:coords()) + pos.y = pos.y - self.RADIUS + --drawDebugText(pos, self._graphdata) + + gfx.Restore() + + --NOTE: Bug workaround: forcerender resets every transformation, re-setup view transform + Dim.transformToScreenSpace() +end + +---Compute radar attribute values from ksh +---@param info string # chart directory path +---@param dif string # chart name without extension +function Radar:updateGraph(info, dif) + ---@cast self Radar + + --local pattern = "(.*[\\/])" + --local extractedSubstring = info:match(pattern) + --local txtFilePath = extractedSubstring .. "radar\\" .. dif .. ".txt" + + --local song = io.open(txtFilePath, "r") + local song = io.open(info.."/"..dif..".ksh") + game.Log(info.."/"..dif..".ksh", game.LOGGER_DEBUG) + game.Log(song and "file open" or "file not found", game.LOGGER_DEBUG) + if song then + local chartData = song:read("*all") + song:close() + + local notesCount, knobCount, oneHandCount, handTripCount = 0, 0, 0, 0 + local trickyValue = 0 + local totalMeasures = 0 + local totalSongLength = 0 + local tsumamiValue = 0 + + for line in chartData:gmatch("[^\r\n]+") do + local noteType, fxType, laserType = line:match("(%d%d%d%d)|(%d%d)|([%-%a ]+)") + + if noteType and fxType then + local noteCount = noteType:match("1") and 1 or 0 + + notesCount = notesCount + noteCount + knobCount = knobCount + (fxType == "02" and 1 or 0) + + oneHandCount = oneHandCount + (laserType:match("[79A-D:]") and 1 or 0) + handTripCount = handTripCount + (laserType:match("[HKPUV:]") and 1 or 0) + if laserType ~= "--" then + tsumamiValue = tsumamiValue + 0.5 + end + end + + if line == "--" then + totalMeasures = totalMeasures + 1 + elseif line:match("t=(%d+)") then + local bpm = tonumber(line:match("t=(%d+)")) + totalSongLength = math.max(totalSongLength, bpm) + end + + if line:match("beat=") or line:match("stop=") or + line:match("zoom_top=") or line:match("zoom_bottom=") or line:match("center_split=") then + trickyValue = trickyValue + 0.5 + end + end + + local graphValues = { + notes = (notesCount / totalSongLength), + peak = (notesCount / totalMeasures) * 10, + tsumami = tsumamiValue, + tricky = trickyValue, + handtrip = handTripCount, + onehand = oneHandCount, + } + + game.Log("graphValues", game.LOGGER_DEBUG) + for k,v in pairs(graphValues) do + game.Log(k..": "..v, game.LOGGER_DEBUG) + end + + local scaleFactors = { + notes = 2, + peak = 50, + tsumami = 500, + tricky = 50, + handtrip = 100, + onehand = 50, + } + + for key, factor in pairs(scaleFactors) do + -- Apply the scaling factor to each value + self._graphdata[key] = graphValues[key] / factor + + -- Limit to a maximum of 125% + self._graphdata[key] = math.min(1.25, self._graphdata[key]) + end + + game.Log("_graphdata", game.LOGGER_DEBUG) + for k,v in pairs(self._graphdata) do + game.Log(k..": "..v, game.LOGGER_DEBUG) + end + end +end + +return Radar diff --git a/scripts/songselect/songwheel.lua b/scripts/songselect/songwheel.lua index 01e7c60..6b53e78 100644 --- a/scripts/songselect/songwheel.lua +++ b/scripts/songselect/songwheel.lua @@ -9,6 +9,9 @@ local Numbers = require('components.numbers') 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) @@ -39,8 +42,6 @@ local searchInfoPanelImage = gfx.CreateSkinImage("song_select/search_info_panel. 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), @@ -125,6 +126,9 @@ local songPlateHeight = 172 local selectedIndex = 1 local selectedDifficulty = 1 +local radar = Radar.new(Point2D.new(0, 0)) +local updateRadar = true + local jacketCache = {} local top50diffs = {} @@ -424,8 +428,8 @@ function drawData() -- Draws the song data on the left panel gfx.ImageRect(96, 529, 410*0.85, 168*0.85, top50JacketOverlayImage, 1, 0) end - gfx.Save() -- Draw best score + gfx.Save() gfx.BeginPath() local scoreNumber = 0 @@ -464,29 +468,32 @@ function drawData() -- Draws the song data on the left panel gfx.Restore() -- Draw BPM + gfx.Save() 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.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.GlobalAlpha(1) + 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() @@ -508,20 +515,20 @@ function drawData() -- Draws the song data on the left panel gfx.BeginPath() gfx.ImageRect(DIFF_X_START+(index-1)*DIFF_GAP-tw/2, 1050, tw, th, diffLabelImage, 1, 0) end - gfx.GlobalAlpha(1) - + 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.GlobalAlpha(1) + gfx.Restore() end @@ -1069,6 +1076,30 @@ function tickTransitions(deltaTime) end end +---This function is basically a workaround for the ForceRender call +local function drawRadar() + gfx.FontSize(28) + gfx.Translate(500, 500) + + local strokeColor = ColorRGBA.new(255, 255, 255, 255) + local fillColor = ColorRGBA.new(0, 0, 0, 191) + + gfx.ResetScissor() + radar:drawBackground(fillColor) + radar:drawOutline(3, strokeColor) + + --NOTE: Bug: forcerender resets every transformation, need to re-setup view transform afterwards + radar:drawRadarMesh() + + Dim.transformToScreenSpace() + + gfx.Save() + gfx.Translate(500,500) + radar:drawRadialTicks(strokeColor) + radar:drawAttributes() + gfx.Restore() +end + draw_songwheel = function(deltaTime) drawBackground(deltaTime) @@ -1077,6 +1108,9 @@ draw_songwheel = function(deltaTime) isFilterWheelActive = game.GetSkinSetting('_songWheelOverlayActive') == 1 drawData() + + drawRadar() + drawCursor() drawFilterInfo(deltaTime) @@ -1105,6 +1139,13 @@ render = function (deltaTime) Sound.stopMusic() + if 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() @@ -1148,6 +1189,7 @@ songs_changed = function (withAll) game.SetSkinSetting('_volforce', totalForce) end + set_index = function(newIndex) transitionScrollScale = 0 transitionAfterscrollScale = 0 @@ -1162,11 +1204,15 @@ set_index = function(newIndex) scrollingUp = true end + updateRadar = true + game.PlaySample('song_wheel/cursor_change.wav') selectedIndex = newIndex end +local json = require("common.json") + set_diff = function(newDiff) if newDiff ~= selectedDifficulty then jacketCache = {} -- Clear the jacket cache for the new diff jackets @@ -1174,6 +1220,8 @@ set_diff = function(newDiff) game.PlaySample('song_wheel/diff_change.wav') end + updateRadar = true + selectedDifficulty = newDiff irLeaderboard = {} irRequestStatus = 1 diff --git a/shaders/radar.fs b/shaders/radar.fs new file mode 100644 index 0000000..ae5cbbd --- /dev/null +++ b/shaders/radar.fs @@ -0,0 +1,66 @@ +#version 330 +#extension GL_ARB_separate_shader_objects : enable + +const float PI = 3.1415926535897932384626433832795; +const float PI_2 = 1.57079632679489661923; + +layout(location=1) in vec2 fsTex; +layout(location=2) in vec3 fragPos; + +layout(location=0) out vec4 target; + +uniform vec4 colorCenter; +uniform vec4 colorMax; + +uniform float maxSize; + +float lerp(float x, vec2 p0, vec2 p1) +{ + return p0.y + (p1.y - p0.y) * ((x - p0.x)/(p1.x - p0.x)); +} + +vec4 toHSV(vec4 rgb) +{ + vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0); + vec4 p = mix(vec4(rgb.bg, K.wz), vec4(rgb.gb, K.xy), step(rgb.b, rgb.g)); + vec4 q = mix(vec4(p.xyw, rgb.r), vec4(rgb.r, p.yzx), step(p.x, rgb.r)); + + float d = q.x - min(q.w, q.y); + float e = 1.0e-10; + return vec4(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x, rgb.a); +} + +vec4 toRGB(vec4 hsv) +{ + vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); + vec3 p = abs(fract(hsv.xxx + K.xyz) * 6.0 - K.www); + return vec4(hsv.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), hsv.y), hsv.a); +} + +// x is in [0.0, 1.0] +vec4 lerpColor(float x, vec4 color1, vec4 color2) +{ + // convert RGB color space to HSV + vec4 hsv1 = toHSV(color1), hsv2 = toHSV(color2); + float hue = (mod(mod((hsv2.x-hsv1.x), 1.) + 1.5, 1.)-0.5)*x + hsv1.x; + vec4 hsv = vec4(hue, mix(hsv1.yzw, hsv2.yzw, x)); + // convert HSV color space back to RGB + return toRGB(hsv); +} + +float EaseInSine(float x) +{ + return 1.0 - cos((x * PI) / 2.0); +} + +vec4 coordToColor(vec2 pos, float maxSize) +{ + float r = length(pos); + float factor = lerp(r, vec2(0,0), vec2(maxSize,1)); + return lerpColor(EaseInSine(factor), colorCenter, colorMax); +} + +void main() +{ + target = coordToColor(fragPos.xy, maxSize); +} diff --git a/shaders/radar.vs b/shaders/radar.vs new file mode 100644 index 0000000..2826d10 --- /dev/null +++ b/shaders/radar.vs @@ -0,0 +1,22 @@ +#version 330 +#extension GL_ARB_separate_shader_objects : enable + +layout(location=0) in vec2 inPos; +layout(location=1) in vec2 inTex; + +out gl_PerVertex +{ + vec4 gl_Position; +}; +layout(location=1) out vec2 fsTex; +layout(location=2) out vec3 fragPos; + +uniform mat4 proj; +uniform mat4 world; + +void main() +{ + fsTex = inTex; + gl_Position = proj * world * vec4(inPos.xy, 0, 1); + fragPos = vec3(inPos.xy, 0); +} diff --git a/shaders/radarVertex.fs b/shaders/radarVertex.fs new file mode 100644 index 0000000..3e1f4ce --- /dev/null +++ b/shaders/radarVertex.fs @@ -0,0 +1,11 @@ +#version 330 +#extension GL_ARB_separate_shader_objects : enable + +layout(location=1) in vec2 fsTex; +layout(location=0) out vec4 target; +layout(location=2) in vec4 inColor; + +void main() +{ + target = inColor; +} diff --git a/shaders/radarVertex.vs b/shaders/radarVertex.vs new file mode 100644 index 0000000..3a6193b --- /dev/null +++ b/shaders/radarVertex.vs @@ -0,0 +1,94 @@ +#version 330 +#extension GL_ARB_separate_shader_objects : enable + +const float PI = 3.1415926535897932384626433832795; +const float PI_2 = 1.57079632679489661923; + +layout(location=0) in vec2 inPos; +layout(location=1) in vec2 inTex; + +out gl_PerVertex +{ + vec4 gl_Position; +}; +layout(location=1) out vec2 fsTex; +layout(location=2) out vec4 vertexColor; + +uniform mat4 proj; +uniform mat4 world; + +uniform vec4 colorCenter; +uniform vec4 colorMax; + +uniform float maxSize; + +// Polar coordinate utility functions +float hypot(vec2 pos) +{ + return sqrt(pos.x * pos.x + pos.y * pos.y); +} + +/* +float atan2(vec2 pos) +{ + if (pos.x > 0) + { + return atan(pos.y / pos.x); + } + + if (pos.x < 0 && pos.y >= 0) + { + return atan(pos.y / pos.x) + PI; + } + + if (pos.x < 0 && pos.y < 0) + { + return atan(pos.y / pos.x) - PI; + } + + if (pos.x == 0 && pos.y > 0) + { + return PI_2; + } + + if (pos.x == 0 && pos.y < 0) + { + return -PI_2; + } + + // The following is not mathematically correct, as it's undefined + return 0.0; +} + +vec2 toPolar(vec2 cartesian) +{ + return vec2(hypot(pos), atan2(pos)); +} +*/ + +float lerp(float x, vec2 p0, vec2 p1) +{ + return p0.y + (p1.y - p0.y) * ((x - p0.x)/(p1.x - p0.x)); +} + +// x is in [0.0, 1.0] +vec4 lerpColor(float x, vec4 color1, vec4 color2) +{ + return color1 + (color2 - color1) * x; +} + +vec4 coordToColor(vec2 pos, float maxSize) +{ + float r = hypot(pos); + //float phi = atan2(pos); + + float factor = lerp(r, vec2(0,0), vec2(maxSize,1)); + + return lerpColor(factor, colorCenter, colorMax); +} + +void main() +{ + gl_Position = proj * world * vec4(inPos.xy, 0, 1); + vertexColor = coordToColor(inPos.xy, maxSize); +} -- 2.40.1 From 48b503ac839ebe8f2000af216e738813f5a2ff1c Mon Sep 17 00:00:00 2001 From: Hersi Date: Mon, 13 Nov 2023 01:42:13 +0100 Subject: [PATCH 02/12] doc update --- docs/lua_api/gfx.lua | 4 +- docs/lua_api/shadedmesh.lua | 290 +++++++++++++++++------------------- 2 files changed, 136 insertions(+), 158 deletions(-) diff --git a/docs/lua_api/gfx.lua b/docs/lua_api/gfx.lua index 2541f20..1405e35 100644 --- a/docs/lua_api/gfx.lua +++ b/docs/lua_api/gfx.lua @@ -472,7 +472,7 @@ UpdateImagePattern = function(pattern, sx, sy, ix, iy, angle, alpha) end ---@param size? integer UpdateLabel = function(label, text, size) end ----@type table +---@class gfx gfx = { BLEND_ZERO = 1, BLEND_ONE = 2, @@ -595,4 +595,4 @@ gfx = { Translate = Translate, UpdateImagePattern = UpdateImagePattern, UpdateLabel = UpdateLabel, -}; \ No newline at end of file +}; diff --git a/docs/lua_api/shadedmesh.lua b/docs/lua_api/shadedmesh.lua index a6a4855..3edd500 100644 --- a/docs/lua_api/shadedmesh.lua +++ b/docs/lua_api/shadedmesh.lua @@ -1,117 +1,4 @@ --- Adds a texture that was loaded with `gfx.LoadSharedTexture` to the material that can be used in the shader code ----@param uniformName string ----@param textureName string -AddSharedTexture = function(uniformName, textureName) end - --- Adds a texture to the material that can be used in the shader code ----@param uniformName string ----@param path string # prepended with `skins//textures/` -AddSkinTexture = function(uniformName, path) end - --- Adds a texture to the material that can be used in the shader code ----@param uniformName string ----@param path string -AddTexture = function(uniformName, path) end - --- Gets the translation of the mesh ----@return number x, number y, number z -GetPosition = function() end - --- Gets the rotation (in degrees) of the mesh ----@return number roll, number yaw, number pitch -GetRotation = function() end - --- Gets the scale of the mesh ----@return number x, number y, number z -GetScale = function() end - --- Sets the blending mode ----@param mode integer # options also available as fields of the object prefixed with `BLEND` --- `Normal` = 0 (default) --- `Additive` = 1 --- `Multiply` = 2 -SetBlendMode = function(mode) end - --- Sets the geometry data ----@param data table # array of vertices in clockwise order starting from the top left e.g. --- ``` --- { --- { { 0, 0 }, { 0, 0 } }, --- { { 50, 0 }, { 1, 0 } }, --- { { 50, 50 }, { 1, 1 } }, --- { { 0, 50 }, { 0, 1 } }, --- } --- ``` -SetData = function(data) end - --- Sets the material is opaque or non-opaque (default) ----@param opaque boolean -SetOpaque = function(opaque) end - --- Sets the value of the specified uniform ----@param uniformName string ----@param value number # `float` -SetParam = function(uniformName, value) end - --- Sets the value of the specified 2d vector uniform ----@param uniformName string ----@param x number # `float` ----@param y number # `float` -SetParamVec2 = function(uniformName, x, y) end - --- Sets the value of the specified 3d vector uniform ----@param uniformName string ----@param x number # `float` ----@param y number # `float` ----@param z number # `float` -SetParamVec3 = function(uniformName, x, y, z) end - --- Sets the value of the specified 4d vector uniform ----@param uniformName string ----@param x number # `float` ----@param y number # `float` ----@param z number # `float` ----@param w number # `float` -SetParamVec4 = function(uniformName, x, y, z, w) end - --- Sets the translation for the mesh --- Relative to the screen for `ShadedMesh` --- Relative to the center of the crit line for `ShadedMeshOnTrack` ----@param x number ----@param y number ----@param z? number # Default `0` -SetPosition = function(x, y, z) end - --- Sets the format for geometry data provided by `SetData` ----@param type integer # options also available as fields of the object prefixed with `PRIM` --- `TriangleList` = 0 (default) --- `TriangleStrip` = 1 --- `TriangleFan` = 2 --- `LineList` = 3 --- `LineStrip` = 4 --- `PointList` = 5 -SetPrimitiveType = function(type) end - --- Sets the rotation (in degrees) of the mesh --- **WARNING:** For `ShadedMesh`, pitch and yaw may clip, rendering portions or the entire mesh invisible ----@param roll number ----@param yaw? number # Default `0` ----@param pitch? number # Default `0` -SetRotation = function(roll, yaw, pitch) end - --- Sets the scale of the mesh ----@param x number ----@param y number ----@param z? number # Default `0` -SetScale = function(x, y, z) end - --- Sets the wireframe mode of the object (does not render texture) --- Useful for debugging models or geometry shaders ----@param useWireframe boolean -SetWireframe = function(useWireframe) end - --- Renders the `ShadedMesh` object -Draw = function() end +---@diagnostic disable:missing-return ---@class ShadedMesh ShadedMesh = { @@ -125,57 +12,148 @@ ShadedMesh = { PRIM_LINELIST = 3, PRIM_LINESTRIP = 4, PRIM_POINTLIST = 5, - - AddSharedTexture = AddSharedTexture, - AddSkinTexture = AddSkinTexture, - AddTexture = AddTexture, - Draw = Draw, - GetPosition = GetPosition, - GetRotation = GetRotation, - GetScale = GetScale, - SetBlendMode = SetBlendMode, - SetData = SetData, - SetOpaque = SetOpaque, - SetParam = SetParam, - SetParamVec2 = SetParamVec2, - SetParamVec3 = SetParamVec3, - SetParamVec4 = SetParamVec4, - SetPosition = SetPosition, - SetPrimitiveType = SetPrimitiveType, - SetRotation = SetRotation, - SetScale = SetScale, - SetWireframe = SetWireframe, }; --- Gets the length of the mesh ----@return number length -GetLength = function() end +-- Adds a texture that was loaded with `gfx.LoadSharedTexture` to the material that can be used in the shader code +---@param uniformName string +---@param textureName string +function ShadedMesh:AddSharedTexture(uniformName, textureName) end --- Sets the y-scale of the mesh based on its length --- Useful for creating fake buttons which may have variable length based on duration ----@param length number -ScaleToLength = function(length) end +-- Adds a texture to the material that can be used in the shader code +---@param uniformName string +---@param path string # prepended with `skins//textures/` +function ShadedMesh:AddSkinTexture(uniformName, path) end --- Stops meshes beyond the track from being rendered if `doClip` ----@param doClip boolean -SetClipWithTrack = function(doClip) end +-- Adds a texture to the material that can be used in the shader code +---@param uniformName string +---@param path string +function ShadedMesh:AddTexture(uniformName, path) end --- Sets the length (in the y-direction relative to the track) of the mesh ----@param length number # Optional constants: `BUTTON_TEXTURE_LENGTH`, `FXBUTTON_TEXTURE_LENGTH`, and `TRACK_LENGTH` -SetLength = function(length) end +-- Gets the translation of the mesh +---@return number x, number y, number z +function ShadedMesh:GetPosition() end + +-- Gets the rotation (in degrees) of the mesh +---@return number roll, number yaw, number pitch +function ShadedMesh:GetRotation() end + +-- Gets the scale of the mesh +---@return number x, number y, number z +function ShadedMesh:GetScale() end + +-- Sets the blending mode +---@param mode integer # options also available as fields of the object prefixed with `BLEND` +-- `Normal` = 0 (default) +-- `Additive` = 1 +-- `Multiply` = 2 +function ShadedMesh:SetBlendMode(mode) end + +-- Sets the geometry data +---@param data table # array of vertices in clockwise order starting from the top left e.g. +-- ``` +-- { +-- { { 0, 0 }, { 0, 0 } }, +-- { { 50, 0 }, { 1, 0 } }, +-- { { 50, 50 }, { 1, 1 } }, +-- { { 0, 50 }, { 0, 1 } }, +-- } +-- ``` +function ShadedMesh:SetData(data) end + +-- Sets the material is opaque or non-opaque (default) +---@param opaque boolean +function ShadedMesh:SetOpaque(opaque) end + +-- Sets the value of the specified uniform +---@param uniformName string +---@param value number # `float` +function ShadedMesh:SetParam(uniformName, value) end + +-- Sets the value of the specified 2d vector uniform +---@param uniformName string +---@param x number # `float` +---@param y number # `float` +function ShadedMesh:SetParamVec2(uniformName, x, y) end + +-- Sets the value of the specified 3d vector uniform +---@param uniformName string +---@param x number # `float` +---@param y number # `float` +---@param z number # `float` +function ShadedMesh:SetParamVec3(uniformName, x, y, z) end + +-- Sets the value of the specified 4d vector uniform +---@param uniformName string +---@param x number # `float` +---@param y number # `float` +---@param z number # `float` +---@param w number # `float` +function ShadedMesh:SetParamVec4(uniformName, x, y, z, w) end + +-- Sets the translation for the mesh +-- Relative to the screen for `ShadedMesh` +-- Relative to the center of the crit line for `ShadedMeshOnTrack` +---@param x number +---@param y number +---@param z? number # Default `0` +function ShadedMesh:SetPosition(x, y, z) end + +-- Sets the format for geometry data provided by `SetData` +---@param type integer # options also available as fields of the object prefixed with `PRIM` +-- `TriangleList` = 0 (default) +-- `TriangleStrip` = 1 +-- `TriangleFan` = 2 +-- `LineList` = 3 +-- `LineStrip` = 4 +-- `PointList` = 5 +function ShadedMesh:SetPrimitiveType(type) end + +-- Sets the rotation (in degrees) of the mesh +-- **WARNING:** For `ShadedMesh`, pitch and yaw may clip, rendering portions or the entire mesh invisible +---@param roll number +---@param yaw? number # Default `0` +---@param pitch? number # Default `0` +function ShadedMesh:SetRotation(roll, yaw, pitch) end + +-- Sets the scale of the mesh +---@param x number +---@param y number +---@param z? number # Default `0` +function ShadedMesh:SetScale(x, y, z) end + +-- Sets the wireframe mode of the object (does not render texture) +-- Useful for debugging models or geometry shaders +---@param useWireframe boolean +function ShadedMesh:SetWireframe(useWireframe) end + +-- Renders the `ShadedMesh` object +function ShadedMesh:Draw() end --- Uses an existing game mesh ----@param meshName string # Options: `'button'`, `'fxbutton'`, and `'track'` -UseGameMesh = function(meshName) end ---@class ShadedMeshOnTrack : ShadedMesh ---@field BUTTON_TEXTURE_LENGTH number ---@field FXBUTTON_TEXTURE_LENGTH number ---@field TRACK_LENGTH number ShadedMeshOnTrack = { - GetLength = GetLength, - UseGameMesh = UseGameMesh, - ScaleToLength = ScaleToLength, - SetClipWithTrack = SetClipWithTrack, - SetLength = SetLength, -}; \ No newline at end of file +}; + +-- Gets the length of the mesh +---@return number length +function ShadedMeshOnTrack:GetLength() end + +-- Sets the y-scale of the mesh based on its length +-- Useful for creating fake buttons which may have variable length based on duration +---@param length number +function ShadedMeshOnTrack:ScaleToLength(length) end + +-- Stops meshes beyond the track from being rendered if `doClip` +---@param doClip boolean +function ShadedMeshOnTrack:SetClipWithTrack(doClip) end + +-- Sets the length (in the y-direction relative to the track) of the mesh +---@param length number # Optional constants: `BUTTON_TEXTURE_LENGTH`, `FXBUTTON_TEXTURE_LENGTH`, and `TRACK_LENGTH` +function ShadedMeshOnTrack:SetLength(length) end + +-- Uses an existing game mesh +---@param meshName string # Options: `'button'`, `'fxbutton'`, and `'track'` +function ShadedMeshOnTrack:UseGameMesh(meshName) end -- 2.40.1 From d6e4c84a430f3377eae0ac5da15a201095999f11 Mon Sep 17 00:00:00 2001 From: Hersi Date: Tue, 14 Nov 2023 07:35:06 +0100 Subject: [PATCH 03/12] Add Continuum fonts --- fonts/contb.ttf | Bin 0 -> 45908 bytes fonts/contl.ttf | Bin 0 -> 56676 bytes fonts/contm.ttf | Bin 0 -> 46584 bytes 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 fonts/contb.ttf create mode 100644 fonts/contl.ttf create mode 100644 fonts/contm.ttf diff --git a/fonts/contb.ttf b/fonts/contb.ttf new file mode 100644 index 0000000000000000000000000000000000000000..45ab0e2f5657a208b14c57858a08f493f2b231d6 GIT binary patch literal 45908 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%0Ak23 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 literal 0 HcmV?d00001 diff --git a/fonts/contm.ttf b/fonts/contm.ttf new file mode 100644 index 0000000000000000000000000000000000000000..08c8df887c13aab3cf0129de3c68b4fdc0c4e858 GIT binary patch literal 46584 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 Date: Tue, 14 Nov 2023 07:35:27 +0100 Subject: [PATCH 04/12] doc update --- docs/lua_api/IR.lua | 32 +++++++++++++++++++------------- docs/lua_api/gfx.lua | 9 ++++----- docs/lua_api/songwheel.lua | 3 ++- 3 files changed, 25 insertions(+), 19 deletions(-) diff --git a/docs/lua_api/IR.lua b/docs/lua_api/IR.lua index 2deea4a..1a9c0cf 100644 --- a/docs/lua_api/IR.lua +++ b/docs/lua_api/IR.lua @@ -23,47 +23,53 @@ IRData = {} ---@field serverTime integer ---@field serverName string ---@field irVersion string -IRHeartbeatResponseBody = {} ---@class IRRecordResponseBody ---@field record ServerScore -IRRecordResponseBody = {} ----@class IRLeaderboardResponseBody ----@field scores ServerScore[] -IRLeaderboardResponseBody = {} +---@alias IRLeaderboardResponseBody ServerScore[] ---@class IRResponse ---@field statusCode integer ---@field description string ----@field body nil|IRHeartbeatResponseBody|IRRecordResponseBody|IRLeaderboardResponseBody -IRResponse = {} + +---@class IRHeartbeatResponse : IRResponse +---@field body IRHeartbeatResponseBody + +---@class IRChartTrackedResponse : IRResponse +---@field body {} + +---@class IRRecordResponse : IRResponse +---@field body IRRecordResponseBody + +---@class IRLeaderboardResponse : IRResponse +---@field body ServerScore[] -- Performs a Heartbeat request. ----@param callback fun(res: IRResponse) # Callback function receives IRResponse as it's first parameter +---@param callback fun(res: IRHeartbeatResponse) # Callback function receives IRResponse as it's first parameter local function Heartbeat(callback) end -- Performs a Chart Tracked request for the chart with the provided hash. ---@param hash string # song hash ----@param callback fun(res: IRResponse) # Callback function receives IRResponse as it's first parameter +---@param callback fun(res: IRChartTrackedResponse) # Callback function receives IRResponse as it's first parameter local function ChartTracked(hash, callback) end -- Performs a Record request for the chart with the provided hash. ---@param hash string # song hash ----@param callback fun(res: IRResponse) # Callback function receives IRResponse as it's first parameter +---@param callback fun(res: IRRecordResponse) # Callback function receives IRResponse as it's first parameter local function Record(hash, callback) end -- Performs a Leaderboard request for the chart with the provided hash, with parameters mode and n. ---@param hash string # song hash ---@param mode "best"|"rivals" # request leaderboard mode ---@param n integer # limit the number of requested scores ----@param callback fun(res: IRResponse) # Callback function receives IRResponse as it's first parameter +---@param callback fun(res: IRLeaderboardResponse) # Callback function receives IRResponse as it's first parameter local function Leaderboard(hash, mode, n, callback) end ----@type table +---@class IR IR = { Heartbeat = Heartbeat, ChartTracked = ChartTracked, Record = Record, Leaderboard = Leaderboard -} \ No newline at end of file +} diff --git a/docs/lua_api/gfx.lua b/docs/lua_api/gfx.lua index 1405e35..02a8071 100644 --- a/docs/lua_api/gfx.lua +++ b/docs/lua_api/gfx.lua @@ -267,7 +267,7 @@ LoadSharedSkinTexture = function(name, path) end -- Loads a font fromt the specified filename -- Sets it as the current font if it is already loaded ----@param name? string +---@param name string ---@param filename string LoadFont = function(name, filename) end @@ -280,11 +280,10 @@ LoadFont = function(name, filename) end ---@return any # returns `placeholder` until the image is loaded LoadImageJob = function(filepath, placeholder, w, h) end --- Loads a font from `skins//textures/` +-- Loads a font from `skins//fonts/` -- Sets it as the current font if it is already loaded ----@param name? string ----@param filename string -LoadSkinFont = function(name, filename) end +---@param name string +LoadSkinFont = function(name) end -- Loads an image outside of the main thread to prevent rendering lock-up -- Image will be loaded at original size unless `w` and `h` are provided diff --git a/docs/lua_api/songwheel.lua b/docs/lua_api/songwheel.lua index c86b9be..372956c 100644 --- a/docs/lua_api/songwheel.lua +++ b/docs/lua_api/songwheel.lua @@ -1,3 +1,4 @@ +---@diagnostic disable: lowercase-global -- songwheel `songwheel` table ---@class SongWheelScore @@ -35,7 +36,7 @@ SongWheelDifficulty = {} ---@class SongWheelSong ---@field artist string # Chart artist ---@field difficulties SongWheelDifficulty[] # Array of difficulties for the current song ----@field bpm number # Chart BPM +---@field bpm string # Chart BPM ---@field id integer # Song id, unique static identifier ---@field path string # Full filepath to the chart folder on the disk ---@field title string # Chart title -- 2.40.1 From 52cc1beb7d2e1a9ee0ff8f2f0791469d684ea89a Mon Sep 17 00:00:00 2001 From: Hersi Date: Tue, 14 Nov 2023 07:35:48 +0100 Subject: [PATCH 05/12] add dump() to utils --- scripts/common/util.lua | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/scripts/common/util.lua b/scripts/common/util.lua index 7f10481..dcc1655 100644 --- a/scripts/common/util.lua +++ b/scripts/common/util.lua @@ -79,6 +79,19 @@ local function firstAlphaNum(s) return ''; end +local function dump(o) + if type(o) == 'table' then + local s = '{ ' + for k,v in pairs(o) do + if type(k) ~= 'number' then k = '"'..k..'"' end + s = s .. '['..k..'] = ' .. dump(v) .. ',' + end + return s .. '} ' + else + return tostring(o) + end +end + return { split = split, filter = filter, @@ -91,4 +104,5 @@ return { mix = mix, modIndex = modIndex, firstAlphaNum = firstAlphaNum, + dump = dump } -- 2.40.1 From cba1ebd790ac137554183ff2d7f9bee4dee61eac Mon Sep 17 00:00:00 2001 From: Hersi Date: Tue, 14 Nov 2023 07:39:48 +0100 Subject: [PATCH 06/12] finish radar component text "stroke" is a dimmed color instead of black add text alignment to radar attributes fix radar rotation (rotate 90deg ccw) optimize some angle and vertex position calculations add some warnings to buggy code --- scripts/components/radar.lua | 112 ++++++++++++++++++----------------- 1 file changed, 59 insertions(+), 53 deletions(-) diff --git a/scripts/components/radar.lua b/scripts/components/radar.lua index 75f77ae..baa4be1 100644 --- a/scripts/components/radar.lua +++ b/scripts/components/radar.lua @@ -35,7 +35,8 @@ end local function renderOutlinedText(pos, text, outlineWidth, color) local x, y = pos:coords() - gfx.FillColor(ColorRGBA.BLACK:components()); + local dimColor = color:mix(ColorRGBA.BLACK, 0.8) + gfx.FillColor(dimColor:components()); gfx.Text(text, x - outlineWidth, y + outlineWidth); gfx.Text(text, x - outlineWidth, y - outlineWidth); gfx.Text(text, x + outlineWidth, y + outlineWidth); @@ -67,18 +68,20 @@ end RadarAttributes = { ---Create RadarAttributes instance ---@param text? string # default "" - ---@param pos? Point2D # default (0, 0) + ---@param offset? Point2D # default (0, 0) ---@param color? ColorRGBA # default BLACK + ---@param align? integer # gfx.TEXT_ALIGN_<...> values, default gfx.TEXT_ALIGN_CENTER + gfx.TEXT_ALIGN_BASELINE ---@return RadarAttributes - new = function (text, pos, color) + new = function (text, offset, color, align) ---@class RadarAttributes ---@field text string - ---@field pos Point2D + ---@field offset Point2D ---@field color ColorRGBA local o = { text = text or "", - pos = pos or Point2D.ZERO, - color = color or ColorRGBA.BLACK + offset = offset or Point2D.ZERO, + color = color or ColorRGBA.BLACK, + align = align or gfx.TEXT_ALIGN_CENTER + gfx.TEXT_ALIGN_BASELINE } setmetatable(o, RadarAttributes) @@ -92,17 +95,17 @@ RadarAttributes.__index = RadarAttributes Radar = { ---@type RadarAttributes[][] ATTRIBUTES = { - {RadarAttributes.new("notes", Point2D.new(0, -30), ColorRGBA.CYAN),}, - {RadarAttributes.new("peak", Point2D.new(40, -20), ColorRGBA.RED),}, - {RadarAttributes.new("tsumami", Point2D.new(40, 20), RADAR_PURPLE),}, - {RadarAttributes.new("tricky", Point2D.new(0, 30), ColorRGBA.YELLOW),}, + {RadarAttributes.new("notes", Point2D.new(0, 0), ColorRGBA.CYAN, gfx.TEXT_ALIGN_CENTER + gfx.TEXT_ALIGN_BOTTOM),}, + {RadarAttributes.new("peak", Point2D.new(0, 0), ColorRGBA.RED, gfx.TEXT_ALIGN_CENTER + gfx.TEXT_ALIGN_BOTTOM), }, + {RadarAttributes.new("tsumami", Point2D.new(0, 0), RADAR_PURPLE, gfx.TEXT_ALIGN_CENTER + gfx.TEXT_ALIGN_TOP),}, + {RadarAttributes.new("tricky", Point2D.new(0, 0), ColorRGBA.YELLOW, gfx.TEXT_ALIGN_CENTER + gfx.TEXT_ALIGN_TOP),}, { - RadarAttributes.new("hand", Point2D.new(-40, 20), RADAR_MAGENTA), - RadarAttributes.new("trip", Point2D.new(5, 18), RADAR_MAGENTA), + RadarAttributes.new("hand", Point2D.new(0, 0), RADAR_MAGENTA, gfx.TEXT_ALIGN_CENTER + gfx.TEXT_ALIGN_TOP), + RadarAttributes.new("trip", Point2D.new(5, 16), RADAR_MAGENTA, gfx.TEXT_ALIGN_CENTER + gfx.TEXT_ALIGN_TOP), }, { - RadarAttributes.new("one", Point2D.new(-40, -20), RADAR_GREEN), - RadarAttributes.new("hand", Point2D.new(-5, 18), RADAR_GREEN), + RadarAttributes.new("one", Point2D.new(6, -16), RADAR_GREEN, gfx.TEXT_ALIGN_CENTER + gfx.TEXT_ALIGN_BOTTOM), + RadarAttributes.new("hand", Point2D.new(0, 0), RADAR_GREEN, gfx.TEXT_ALIGN_CENTER + gfx.TEXT_ALIGN_BOTTOM), } }, RADIUS = 100.0, @@ -123,34 +126,34 @@ Radar = { onehand = 0, }, _hexagonMesh = gfx.CreateShadedMesh("radar"), - outlineVertices = {}, - attributePositions = {}, + _outlineVertices = {}, + _attributePositions = {}, ---@type Point2D[][] + _angleStep = (2 * math.pi) / #Radar.ATTRIBUTES, -- 360° / no. attributes, in radians + _initRotation = math.pi / 2, -- 90°, in radians pos = pos or Point2D.ZERO, scale = radius and radius / Radar.RADIUS or 1.0, } local sides = #Radar.ATTRIBUTES - local angleStep = (2 * math.pi) / sides - local rotationAngle = angleStep / 2 local outlineRadius = Radar.RADIUS - local attributeRadius = Radar.RADIUS + 20 + local attributeRadius = Radar.RADIUS + 30 for i = 0, sides - 1 do local attrIdx = i + 1 - local angle = i * angleStep - rotationAngle + local angle = i * o._angleStep - o._initRotation local cosAngle = math.cos(angle) local sinAngle = math.sin(angle) -- cache outline vertices - table.insert(o.outlineVertices, Point2D.new(outlineRadius * cosAngle, outlineRadius * sinAngle)) + table.insert(o._outlineVertices, Point2D.new(outlineRadius * cosAngle, outlineRadius * sinAngle)) -- cache attribute positions - table.insert(o.attributePositions, {}) + table.insert(o._attributePositions, {}) for j = 1, #Radar.ATTRIBUTES[attrIdx] do local attr = Radar.ATTRIBUTES[attrIdx][j] local attributePos = Point2D.new(attributeRadius * cosAngle, attributeRadius * sinAngle) - attributePos.x = attributePos.x + attr.pos.x - attributePos.y = attributePos.y + attr.pos.y - table.insert(o.attributePositions, j, attributePos) + attributePos.x = attributePos.x + attr.offset.x + attributePos.y = attributePos.y + attr.offset.y + table.insert(o._attributePositions[attrIdx], j, attributePos) end end @@ -165,9 +168,9 @@ Radar.__index = Radar function Radar:drawOutline(w, color) ---@cast self Radar - for i = 1, #self.outlineVertices do - local j = i % #self.outlineVertices + 1 - drawLine(self.outlineVertices[i], self.outlineVertices[j], w, color) + for i = 1, #self._outlineVertices do + local j = i % #self._outlineVertices + 1 + drawLine(self._outlineVertices[i], self._outlineVertices[j], w, color) end end @@ -181,7 +184,7 @@ function Radar:drawRadialTicks(color, ticks) gfx.Save() gfx.StrokeColor(color:components()) - for i, vertex in ipairs(self.outlineVertices) do + for i, vertex in ipairs(self._outlineVertices) do gfx.BeginPath() gfx.MoveTo(0, 0) gfx.LineTo(vertex.x, vertex.y) @@ -226,9 +229,9 @@ function Radar:drawBackground(fillColor) gfx.Save() gfx.BeginPath() - gfx.MoveTo(self.outlineVertices[1].x, self.outlineVertices[1].y) - for i = 2, #self.outlineVertices do - gfx.LineTo(self.outlineVertices[i].x, self.outlineVertices[i].y) + gfx.MoveTo(self._outlineVertices[1].x, self._outlineVertices[1].y) + for i = 2, #self._outlineVertices do + gfx.LineTo(self._outlineVertices[i].x, self._outlineVertices[i].y) end gfx.ClosePath() @@ -243,13 +246,14 @@ function Radar:drawAttributes() gfx.Save() - gfx.FontSize(20) - for i = 1, #self.attributePositions do - local attrPos = self.attributePositions[i] + gfx.LoadSkinFont("contb.ttf") + gfx.FontSize(21) + for i = 1, #self._attributePositions do + local attrPos = self._attributePositions[i] for j = 1, #attrPos do local pos = attrPos[j] local attr = Radar.ATTRIBUTES[i][j] - gfx.TextAlign(gfx.TEXT_ALIGN_MIDDLE + gfx.TEXT_ALIGN_CENTER) + gfx.TextAlign(attr.align) renderOutlinedText(pos, string.upper(attr.text), 1, attr.color) end end @@ -257,6 +261,11 @@ function Radar:drawAttributes() gfx.Restore() end +---Draw shaded radar mesh +--- +---Bug: ForceRender resets every transformation, you need to re-setup view transform afterwards. +---ForceRender also resets the gfx stack, USC will crash if you try to call gfx.Restore(), +---make sure the gfx stack is clean before calling radar:drawRadarMesh() function Radar:drawRadarMesh() ---@cast self Radar @@ -269,10 +278,8 @@ function Radar:drawRadarMesh() self._graphdata.onehand, } - local color1 = ColorRGBA.new(255, 12, 48, 230) -- magenta-ish - local color2 = ColorRGBA.new(112, 119, 255, 230) -- light blue-ish purple - - local currentTime = 0 + local colorMax = ColorRGBA.new(255, 12, 48, 230) -- magenta-ish + local colorCenter = ColorRGBA.new(112, 119, 255, 230) -- light blue-ish purple -- Calculate the maximum size based on the constraint local maxSize = self.RADIUS * self.scale + 10 @@ -280,23 +287,20 @@ function Radar:drawRadarMesh() self._hexagonMesh:SetParam("maxSize", maxLineLength + .0) -- Set the color of the hexagon - self._hexagonMesh:SetParamVec4("colorMax", color1:componentsFloat()) - self._hexagonMesh:SetParamVec4("colorCenter", color2:componentsFloat()) + self._hexagonMesh:SetParamVec4("colorMax", colorMax:componentsFloat()) + self._hexagonMesh:SetParamVec4("colorCenter", colorCenter:componentsFloat()) -- Set the primitive type to triangles self._hexagonMesh:SetPrimitiveType(self._hexagonMesh.PRIM_TRIFAN) -- Calculate the vertices of the hexagon local sides = #Radar.ATTRIBUTES - local angleStep = (2 * math.pi) / sides - local rotationAngle = angleStep / 2 - --local rotationAngle = -math.pi / 2 - local vertices = {} + local vertices = {} table.insert(vertices, {{0, 0}, {0, 0}}) for i = 0, sides do local j = i % sides + 1 - local angle = i * angleStep - rotationAngle + local angle = i * self._angleStep - self._initRotation --local angle = math.rad(60 * (i-1)) + rotationAngle local scale = scaleFact[j] @@ -309,16 +313,18 @@ function Radar:drawRadarMesh() -- Set the hexagon's vertices self._hexagonMesh:SetData(vertices) - --self._hexagonMesh:SetPosition(-maxSize, maxSize) self._hexagonMesh:Draw() - - --NOTE: Bug: forcerender resets every transformation, need to re-setup view transform afterwards + + -- YOU! You are the reason for all my pain! gfx.ForceRender() end +--NOTE: THIS IS BUGGY, ForceRender fucks up so many things, call the individual draw functions at top level function Radar:drawGraph() ---@cast self Radar + game.Log("Radar:drawGraph() SHOULD NOT BE CALLED", game.LOGGER_WARNING) + gfx.Save() gfx.Reset() gfx.ResetScissor() @@ -333,10 +339,10 @@ function Radar:drawGraph() local strokeColor = ColorRGBA.new(255, 255, 255, 100) local fillColor = ColorRGBA.new(0, 0, 0, 191) - self:drawBackground(strokeColor, fillColor) - self:drawOutline(3, ColorRGBA.WHITE) + self:drawBackground(fillColor) + self:drawOutline(3, strokeColor) self:drawRadarMesh() - self:drawRadialTicks() + self:drawRadialTicks(strokeColor) self:drawAttributes() local pos = Point2D.new(self.pos:coords()) -- 2.40.1 From e9d848d92fbd4b26088ed11eab7adf0ee1c371ae Mon Sep 17 00:00:00 2001 From: Hersi Date: Tue, 14 Nov 2023 07:41:51 +0100 Subject: [PATCH 07/12] radar positioning and colors in songwheel.lua --- scripts/songselect/songwheel.lua | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/scripts/songselect/songwheel.lua b/scripts/songselect/songwheel.lua index 6b53e78..29b6413 100644 --- a/scripts/songselect/songwheel.lua +++ b/scripts/songselect/songwheel.lua @@ -1078,23 +1078,30 @@ end ---This function is basically a workaround for the ForceRender call local function drawRadar() - gfx.FontSize(28) - gfx.Translate(500, 500) + local x, y = 375, 650 + local scale = 0.666 - local strokeColor = ColorRGBA.new(255, 255, 255, 255) + 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) - --NOTE: Bug: forcerender resets every transformation, need to re-setup view transform afterwards + --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(500,500) + gfx.Translate(x, y) + gfx.Scale(scale, scale) radar:drawRadialTicks(strokeColor) radar:drawAttributes() gfx.Restore() -- 2.40.1 From a0ceb0bd01ef35f8362e98ac6f5a533d21ed4cab Mon Sep 17 00:00:00 2001 From: Hersi Date: Tue, 14 Nov 2023 07:44:28 +0100 Subject: [PATCH 08/12] clear lua linting errors and warnings in songwheel.lua fix(?) volforce indicator --- scripts/songselect/songwheel.lua | 527 +++++++++++++++---------------- 1 file changed, 253 insertions(+), 274 deletions(-) diff --git a/scripts/songselect/songwheel.lua b/scripts/songselect/songwheel.lua index 29b6413..232944c 100644 --- a/scripts/songselect/songwheel.lua +++ b/scripts/songselect/songwheel.lua @@ -135,7 +135,7 @@ local top50diffs = {} local irRequestStatus = 1 -- 0=unused, 1=not requested, 2=loading, others are status codes local irRequestTimeout = 2 -local irLeaderboard = {} +local irLeaderboard = {} ---@type ServerScore[]|{} local irLeaderboardsCache = {} local transitionScrollScale = 0 @@ -162,6 +162,7 @@ local transitionSearchInfoEnterScale = 0 local transitionSearchBackgroundAlpha = 0 local transitionSearchbarOffsetY = 0 local transitionSearchInfoOffsetY = 0 +local transitionSearchBackgroundInfoAlpha = Easing.inOutQuad(transitionSearchInfoEnterScale) local transitionLaserScale = 0 local transitionLaserY = 0 @@ -199,32 +200,32 @@ local resolutionChange = function(x, y) game.Log('resX:' .. resX .. ' // resY:' .. resY .. ' // fullX:' .. fullX .. ' // fullY:' .. fullY, game.LOGGER_ERROR) end -function getCorrectedIndex(from, offset) - total = #songwheel.songs +local function getCorrectedIndex(from, offset) + local total = #songwheel.songs if (math.abs(offset) > total) then - if (offset < 0) 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 + local index = from + offset - if index < 1 then - index = total + (from+offset) -- this only happens if the offset is negative - end + 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 + if index > total then + local indexesUntilEnd = total - from + index = offset - indexesUntilEnd -- this only happens if the offset is positive + end - return index + return index end -function getJacketImage(song) +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) @@ -234,12 +235,12 @@ function getJacketImage(song) return jacketCache[song.id] end -function getGradeImageForScore(score) +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 + if scoreCutoff > bestGradeCutoff then gradeImage = gradeImages[gradeName] bestGradeCutoff = scoreCutoff end @@ -249,31 +250,30 @@ function getGradeImageForScore(score) return gradeImage end -function drawLaserAnim() +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 -function drawBackground(deltaTime) +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 + 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) @@ -287,7 +287,7 @@ function drawBackground(deltaTime) drawLaserAnim() - if song and diff and (not isFilterWheelActive and transitionLeaveReappearTimer == 0) then + if song and diff and (not isFilterWheelActive and transitionLeaveReappearTimer == 0) then gfx.BeginPath() gfx.ImageRect(0, 0, desw, desh, dataGlowOverlayImage, transitionAfterscrollDataOverlayAlpha, 0) gfx.BeginPath() @@ -304,7 +304,9 @@ function drawBackground(deltaTime) end -function drawSong(song, y) +---@param song SongWheelSong +---@param y number +local function drawSong(song, y) if (not song) then return end local songX = desw/2+28 @@ -315,14 +317,14 @@ function drawSong(song, y) end local bestScore - if selectedSongDifficulty.scores then + 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() @@ -331,7 +333,7 @@ function drawSong(song, y) -- 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) @@ -351,7 +353,7 @@ function drawSong(song, y) 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 @@ -364,7 +366,7 @@ function drawSong(song, y) local gradeImage = gradeImages.none local gradeAlpha = 1 - if bestScore then + if bestScore then gradeImage = getGradeImageForScore(bestScore.score) if (bestScore.score >= gradeCutoffs.S) then @@ -376,13 +378,13 @@ function drawSong(song, y) gfx.ImageRect(songX+391, y+47, 60, 60, gradeImage, gradeAlpha, 0) -- Draw top 50 label if applicable - if (top50diffs[selectedSongDifficulty.id]) then + if (top50diffs[selectedSongDifficulty.hash]) then gfx.BeginPath() gfx.ImageRect(songX+82, y+109, 506*0.85, 26*0.85, top50OverlayImage, 1, 0) end end -function drawSongList() +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 @@ -395,7 +397,7 @@ function drawSongList() 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) @@ -409,131 +411,8 @@ function drawSongList() gfx.GlobalAlpha(1) end -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.id]) 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 - 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 - ---@param diff SongWheelDifficulty -function drawLocalLeaderboard(diff) +local function drawLocalLeaderboard(diff) gfx.LoadSkinFont('Digital-Serial-Bold.ttf') gfx.FontSize(26) @@ -575,31 +454,32 @@ function drawLocalLeaderboard(diff) gfx.Text(username or "-", sbBarContentLeftX, scoreBoardY + sbBarHeight/2 + i*sbBarHeight) gfx.BeginPath() - gfx.Text(score or "- - - - - - - -", sbBarContentRightX, scoreBoardY + sbBarHeight/2 + i*sbBarHeight) + local scoreText = score and tostring(score) or "- - - - - - - -" + gfx.Text(scoreText, sbBarContentRightX, scoreBoardY + sbBarHeight/2 + i*sbBarHeight) end end -function drawIrLeaderboard() +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() @@ -635,8 +515,8 @@ function drawIrLeaderboard() -- 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 + + if irScore then local rank = index gfx.TextAlign(gfx.TEXT_ALIGN_CENTER + gfx.TEXT_ALIGN_MIDDLE) gfx.BeginPath() @@ -645,7 +525,7 @@ function drawIrLeaderboard() 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) @@ -656,7 +536,130 @@ function drawIrLeaderboard() end end -function drawFilterInfo(deltatime) +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 @@ -665,17 +668,17 @@ function drawFilterInfo(deltatime) 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) @@ -686,7 +689,7 @@ function drawFilterInfo(deltatime) gfx.Text(sortOptionLabel or '', desw-150, 130) end -function drawCursor() +local function drawCursor() if isFilterWheelActive or transitionLeaveScale ~= 0 then return false end gfx.BeginPath() @@ -697,7 +700,7 @@ function drawCursor() gfx.ImageRect(desw / 2 - 14, desh / 2 - 213 / 2, 555, 213, cursorImage, 1, 0) end -function drawSearch() +local function drawSearch() if (not songwheel.searchInputActive and searchPreviousActiveState) then searchPreviousActiveState = false game.PlaySample('sort_wheel/enter.wav') @@ -705,7 +708,7 @@ function drawSearch() 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 @@ -731,7 +734,7 @@ function drawSearch() 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 @@ -767,7 +770,7 @@ function drawSearch() gfx.Text(songwheel.searchText, xPos + 160, yPos + 83.2) end -function drawScrollbar() +local function drawScrollbar() if isFilterWheelActive or transitionLeaveScale ~= 0 then return end -- Scrollbar Background @@ -810,7 +813,33 @@ function drawScrollbar() end end -function refreshIrLeaderboard(deltaTime) +---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 @@ -839,56 +868,10 @@ function refreshIrLeaderboard(deltaTime) end irRequestStatus = 2 -- Loading - -- onIrLeaderboardFetched({ - -- statusCode = 20, - -- body = {} - -- }) IR.Leaderboard(diff.hash, 'best', 4, onIrLeaderboardFetched) end -function dump(o) - if type(o) == 'table' then - local s = '{ ' - for k,v in pairs(o) do - if type(k) ~= 'number' then k = '"'..k..'"' end - s = s .. '['..k..'] = ' .. dump(v) .. ',' - end - return s .. '} ' - else - return tostring(o) - end -end - -function onIrLeaderboardFetched(res) - irRequestStatus = res.statusCode - - local song = songwheel.songs[selectedIndex] - local diff = song and song.difficulties[selectedDifficulty] or false - game.Log(diff.hash, game.LOGGER_ERROR) - - if res.statusCode == IRData.States.Success then - game.Log('Raw IR reposonse: ' .. dump(res.body), game.LOGGER_ERROR) - local tempIrLB = res.body - - table.sort(tempIrLB, function (a,b) - -- game.Log(a.score .. ' ?? ' .. b.score, game.LOGGER_ERROR) - return a.score > b.score - end) - - -- for i, tempScore in ipairs(tempIrLeaderboard) do - -- irLeaderboard[tempScore.ranking] = tempScore - -- end - - irLeaderboard = tempIrLB - irLeaderboardsCache[diff.hash] = irLeaderboard - - game.Log(dump(irLeaderboard), game.LOGGER_ERROR) - else - game.Log("IR error " .. res.statusCode, game.LOGGER_ERROR) - end -end - -function tickTransitions(deltaTime) +local function tickTransitions(deltaTime) if transitionScrollScale < 1 then transitionScrollScale = transitionScrollScale + deltaTime / 0.1 -- transition should last for that time in seconds else @@ -904,7 +887,7 @@ function tickTransitions(deltaTime) transitionAfterscrollScale = 1 end - if scrollingUp then + if scrollingUp then transitionScrollOffsetY = Easing.inQuad(1-transitionScrollScale) * songPlateHeight else transitionScrollOffsetY = Easing.inQuad(1-transitionScrollScale) * -songPlateHeight @@ -949,8 +932,6 @@ function tickTransitions(deltaTime) end end - transitionSearchBackgroundInfoAlpha = Easing.inOutQuad(transitionSearchInfoEnterScale) - -- Grade alpha if transitionAfterscrollScale >= 0.03 and transitionAfterscrollScale < 0.033 then transitionAfterscrollGradeAlpha = 0.5 @@ -959,7 +940,7 @@ function tickTransitions(deltaTime) else transitionAfterscrollGradeAlpha = 0 end - + -- Badge alpha if transitionAfterscrollScale >= 0.032 and transitionAfterscrollScale < 0.035 then transitionAfterscrollBadgeAlpha = 0.5 @@ -981,7 +962,7 @@ function tickTransitions(deltaTime) else transitionAfterscrollTextSongArtist = 1 end - + -- Difficulties alpha if transitionAfterscrollScale < 0.025 then transitionAfterscrollDifficultiesAlpha = math.min(1, transitionAfterscrollScale / 0.025) @@ -1001,7 +982,7 @@ function tickTransitions(deltaTime) 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, + transitionJacketBgScrollAlpha = math.max(0, math.min(1, 1-((transitionJacketBgScrollScale-0.8) / 0.05)) ) else @@ -1018,25 +999,17 @@ function tickTransitions(deltaTime) 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 - songBpm = songwheel.songs[selectedIndex].bpm - - -- Is a variable BPM - if (type(songBpm) == "string") then - local s = common.split(songBpm, '-') - songBpm = tonumber(s[1]) -- Lowest bpm value - end - end - - -- If the original songBpm is "2021.04.01" for example, the above code can produce `nil` in the songBpm - -- since it cannot parse the number out of that string. Here we implement a fallback, to not crash - -- USC on whacky charts. Whacky charters, quit using batshit insane bpm values. It makes me angery >:( - if (songBpm == nil) then - songBpm = 120 + 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 @@ -1107,7 +1080,7 @@ local function drawRadar() gfx.Restore() end -draw_songwheel = function(deltaTime) +local draw_songwheel = function(deltaTime) drawBackground(deltaTime) drawSongList() @@ -1132,13 +1105,14 @@ draw_songwheel = function(deltaTime) local debugScrollingUp= "FALSE" if scrollingUp then debugScrollingUp = "TRUE" end - if game.GetSkinSetting('debug_showInformation') then + 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 gfx.ResetTransform() end +---@diagnostic disable-next-line:lowercase-global render = function (deltaTime) tickTransitions(deltaTime) @@ -1164,6 +1138,7 @@ render = function (deltaTime) refreshIrLeaderboard(deltaTime) end +---@diagnostic disable-next-line:lowercase-global songs_changed = function (withAll) irLeaderboardsCache = {} -- Reset LB cache @@ -1173,30 +1148,33 @@ songs_changed = function (withAll) 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] - 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 - top50diffs[diffs[i].id] = true - totalForce = totalForce + diffs[i].force - end - 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] + 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 @@ -1206,22 +1184,23 @@ set_index = function(newIndex) 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 + 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') + 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 + if newDiff ~= selectedDifficulty then jacketCache = {} -- Clear the jacket cache for the new diff jackets game.PlaySample('song_wheel/diff_change.wav') -- 2.40.1 From 2363b381ed07410fb5c91890924e7731f4893ce6 Mon Sep 17 00:00:00 2001 From: Hersi Date: Sat, 18 Nov 2023 09:30:13 +0100 Subject: [PATCH 09/12] radar calc rewrite --- scripts/common/util.lua | 26 +++- scripts/components/radar.lua | 242 +++++++++++++++++++++++++++++------ 2 files changed, 231 insertions(+), 37 deletions(-) diff --git a/scripts/common/util.lua b/scripts/common/util.lua index dcc1655..fe162be 100644 --- a/scripts/common/util.lua +++ b/scripts/common/util.lua @@ -92,6 +92,28 @@ local function dump(o) end end +local function all(t, predicate) + predicate = predicate or function(e) return e end + + for _, e in ipairs(t) do + if not predicate(e) then + return false + end + end + return true +end + +local function any(t, predicate) + predicate = predicate or function(e) return e end + + for _, e in ipairs(t) do + if predicate(e) then + return true + end + end + return false +end + return { split = split, filter = filter, @@ -104,5 +126,7 @@ return { mix = mix, modIndex = modIndex, firstAlphaNum = firstAlphaNum, - dump = dump + dump = dump, + all = all, + any = any } diff --git a/scripts/components/radar.lua b/scripts/components/radar.lua index baa4be1..aa68e6c 100644 --- a/scripts/components/radar.lua +++ b/scripts/components/radar.lua @@ -8,6 +8,7 @@ require("api.point2d") require("api.color") local Dim = require("common.dimensions") +local Util = require("common.util") Dim.updateResolution() @@ -15,6 +16,8 @@ local RADAR_PURPLE = ColorRGBA.new(238, 130, 238) local RADAR_MAGENTA = ColorRGBA.new(191, 70, 235) local RADAR_GREEN = ColorRGBA.new(0, 255, 100) +local maxScaleFactor = 1.8 + ---@param p1 Point2D ---@param p2 Point2D ---@param width number @@ -282,8 +285,8 @@ function Radar:drawRadarMesh() local colorCenter = ColorRGBA.new(112, 119, 255, 230) -- light blue-ish purple -- Calculate the maximum size based on the constraint - local maxSize = self.RADIUS * self.scale + 10 - local maxLineLength = maxSize + (maxSize / 2) + local maxSize = self.RADIUS * self.scale + local maxLineLength = maxSize * maxScaleFactor self._hexagonMesh:SetParam("maxSize", maxLineLength + .0) -- Set the color of the hexagon @@ -304,8 +307,7 @@ function Radar:drawRadarMesh() --local angle = math.rad(60 * (i-1)) + rotationAngle local scale = scaleFact[j] - local lineLength = maxLineLength * scale - lineLength = math.min(lineLength, maxLineLength) -- Cap the length + local lineLength = maxSize * scale local px = lineLength * math.cos(angle) local py = lineLength * math.sin(angle) table.insert(vertices, {{px, py}, {0, 0}}) @@ -366,52 +368,220 @@ function Radar:updateGraph(info, dif) --local txtFilePath = extractedSubstring .. "radar\\" .. dif .. ".txt" --local song = io.open(txtFilePath, "r") - local song = io.open(info.."/"..dif..".ksh") - game.Log(info.."/"..dif..".ksh", game.LOGGER_DEBUG) + local fullPath = info.."/"..dif..".ksh" + local song = io.open(fullPath) + game.Log('Reading chart data from "'..fullPath..'"', game.LOGGER_DEBUG) game.Log(song and "file open" or "file not found", game.LOGGER_DEBUG) if song then local chartData = song:read("*all") song:close() local notesCount, knobCount, oneHandCount, handTripCount = 0, 0, 0, 0 + local chartLineCount = 0 + local notesValue = 0 + local peakValue = 0 + local tsumamiValue = 0 local trickyValue = 0 local totalMeasures = 0 - local totalSongLength = 0 - local tsumamiValue = 0 + local lastNotes = {} + local lastFx = {} + local measureLength = 0 + + ---@cast chartData string for line in chartData:gmatch("[^\r\n]+") do - local noteType, fxType, laserType = line:match("(%d%d%d%d)|(%d%d)|([%-%a ]+)") + -- || - if noteType and fxType then - local noteCount = noteType:match("1") and 1 or 0 + --game.Log(line, game.LOGGER_DEBUG) - notesCount = notesCount + noteCount - knobCount = knobCount + (fxType == "02" and 1 or 0) + local patternBt = "([012][012][012][012])" + local patternFx = "([012ABDFGHIJKLPQSTUVWX][012ABDFGHIJKLPQSTUVWX])" + local patternLaser = "([%-:%dA-Za-o][%-:%dA-Za-o])" + local patternLaneSpin = "([@S][%(%)<>]%d+)" -- optional + local pattern = patternBt.."|"..patternFx.."|"..patternLaser - oneHandCount = oneHandCount + (laserType:match("[79A-D:]") and 1 or 0) - handTripCount = handTripCount + (laserType:match("[HKPUV:]") and 1 or 0) - if laserType ~= "--" then - tsumamiValue = tsumamiValue + 0.5 + -- match line format + + local noteType, fxType, laserType = line:match(pattern) + local laneSpin = line:match(patternLaneSpin) + + if noteType and fxType and laserType then + chartLineCount = chartLineCount + 1 + + -- convert strings to array, to be easily indexable + + noteType = {noteType:match("([012])([012])([012])([012])")} + fxType = {fxType:match("([012ABDFGHIJKLPQSTUVWX])([012ABDFGHIJKLPQSTUVWX])")} + laserType = {laserType:match("([%-:%dA-Za-o])([%-:%dA-Za-o])")} + + ---@cast noteType string[] + ---@cast fxType string[] + ---@cast laserType string[] + + -- parse notes + + local function isNewNote(idx, note) + if note == "2" and lastNotes[idx] ~= note then + -- a new hold note + return true + end + if note == "1" then + -- a chip + return true + end end + for noteIdx, note in ipairs(noteType) do + if isNewNote(noteIdx, note) then + notesCount = notesCount + 1 + end + end + + -- parse fx + + local function isNewFx(idx, fx) + if fx:match("[1ABDFGHIJKLPQSTUVWX]") and lastFx[idx] ~= fx then + -- a new hold note + return true + end + if fx == "2" then + -- a chip + return true + end + end + for fxIdx, fx in ipairs(fxType) do + if isNewFx(fxIdx, fx) then + notesCount = notesCount + 1 + end + end + + -- parse laser + + for _, laser in ipairs(laserType) do + if laser ~= "-" then + knobCount = knobCount + 1 + end + end + + -- figure out one-handed notes (there's a BT or FX while a hand is manipulating a knob) + -- also try to figure out cross-handed notes (one-handed notes, but on the same side as knob) + + local function countBtFx() + local count = 0 + for noteIdx, note in ipairs(noteType) do + if isNewNote(noteIdx, note) then + count = count + 1 + end + end + for fxIdx, fx in ipairs(fxType) do + if isNewFx(fxIdx, fx) then + count = count + 1 + end + end + return count + end + ---@param side "left"|"right" + local function countSide(side) + local count = 0 + local notes = {} + local fx = "" + if side == "left" then + notes = {noteType[1], noteType[2]} + fx = fxType[1] + if isNewFx(1, fx) then + count = count + 1 + end + elseif side == "right" then + notes = {noteType[3], noteType[4]} + fx = fxType[2] + if isNewFx(2, fx) then + count = count + 1 + end + else + game.Log("countSide: Invalid side parameter", game.LOGGER_ERROR) + return 0 + end + for noteIdx, note in ipairs(notes) do + if isNewNote(noteIdx, note) then + count = count + 1 + end + end + return count + end + if laserType[1] ~= "-" and laserType[2] == "-" then + oneHandCount = oneHandCount + countBtFx() + handTripCount = handTripCount + countSide("left") + end + if laserType[1] == "-" and laserType[2] ~= "-" then + oneHandCount = oneHandCount + countBtFx() + handTripCount = handTripCount + countSide("right") + end + + lastNotes = noteType + lastFx = fxType + + measureLength = measureLength + 1 end if line == "--" then + -- end of measure + measureLength = math.max(1, measureLength) + + local relativeMeasureLength = measureLength / 192 + + -- calculate peak density + local peak = (notesCount / 6) / relativeMeasureLength + peakValue = math.max(peakValue, peak) + --[[ + local debuglog = { + measureLength = measureLength, + notesCount = notesCount, + relativeMeasureLength = relativeMeasureLength, + peak = peak, + } + for k, v in pairs(debuglog) do + game.Log(k..": "..v, game.LOGGER_DEBUG) + end + ]] + + -- cumulate "time" spent operating the knobs + local tsumami = (knobCount / 2) / relativeMeasureLength + tsumamiValue = tsumamiValue + tsumami + + measureLength = 0 + notesCount = 0 + + -- cumulate peak values (used to average notes over the length of the song) + notesValue = notesValue + peak + totalMeasures = totalMeasures + 1 - elseif line:match("t=(%d+)") then - local bpm = tonumber(line:match("t=(%d+)")) - totalSongLength = math.max(totalSongLength, bpm) end - if line:match("beat=") or line:match("stop=") or - line:match("zoom_top=") or line:match("zoom_bottom=") or line:match("center_split=") then - trickyValue = trickyValue + 0.5 + local beat = line:match("beat=(%d+/%d+)") + if beat then + beat = {beat:match("(%d+)/(%d+)")} + end + + --BUG: This is not correct, it needs to account for effect length + local function isTricky() + local tricks = { + "beat", + "stop", + "zoom_top", + "zoom_bottom", + "zoom_side", + "center_split", + } + return Util.any(tricks, function(e) return line:match("e") end) + end + if laneSpin or isTricky() then + trickyValue = trickyValue + 1 end end local graphValues = { - notes = (notesCount / totalSongLength), - peak = (notesCount / totalMeasures) * 10, - tsumami = tsumamiValue, + notes = notesValue / totalMeasures, + peak = peakValue, + tsumami = tsumamiValue / totalMeasures, tricky = trickyValue, handtrip = handTripCount, onehand = oneHandCount, @@ -422,21 +592,21 @@ function Radar:updateGraph(info, dif) game.Log(k..": "..v, game.LOGGER_DEBUG) end - local scaleFactors = { - notes = 2, - peak = 50, - tsumami = 500, - tricky = 50, - handtrip = 100, - onehand = 50, + local calibration = { + notes = 10, + peak = 48, + tsumami = 20000, + tricky = 128, + handtrip = 300, + onehand = 300, } - for key, factor in pairs(scaleFactors) do + for key, factor in pairs(calibration) do -- Apply the scaling factor to each value self._graphdata[key] = graphValues[key] / factor - -- Limit to a maximum of 125% - self._graphdata[key] = math.min(1.25, self._graphdata[key]) + -- Limit to maximum scale factor + self._graphdata[key] = math.min(self._graphdata[key], maxScaleFactor) end game.Log("_graphdata", game.LOGGER_DEBUG) -- 2.40.1 From bc191e45f0198737cfc9fcd3059b8247f1de8471 Mon Sep 17 00:00:00 2001 From: Hersi Date: Sat, 18 Nov 2023 09:30:38 +0100 Subject: [PATCH 10/12] fix bug where radar is seen when filter is active --- scripts/songselect/songwheel.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/songselect/songwheel.lua b/scripts/songselect/songwheel.lua index 232944c..34754ea 100644 --- a/scripts/songselect/songwheel.lua +++ b/scripts/songselect/songwheel.lua @@ -1051,6 +1051,8 @@ end ---This function is basically a workaround for the ForceRender call local function drawRadar() + if isFilterWheelActive or transitionLeaveScale ~= 0 then return end + local x, y = 375, 650 local scale = 0.666 -- 2.40.1 From 0951fc029c0c04a1a20190b1cad30c5099b351b8 Mon Sep 17 00:00:00 2001 From: Hersi Date: Thu, 23 Nov 2023 07:29:10 +0100 Subject: [PATCH 11/12] add effect radar toggle in skin settings --- config-definitions.json | 9 +++++++++ scripts/songselect/songwheel.lua | 3 +++ 2 files changed, 12 insertions(+) diff --git a/config-definitions.json b/config-definitions.json index d2b5376..91af2dd 100644 --- a/config-definitions.json +++ b/config-definitions.json @@ -97,5 +97,14 @@ "label": "Show debug information (sometimes in the middle of the screen when you're playing)", "type": "bool", "default": false + }, + + "separator_g": {}, + "Experimental features": { "type": "label" }, + + "songselect_showEffectRadar": { + "label": "Show Effect Radar for compatible songs (VERY WIP)", + "type": "bool", + "default": false } } diff --git a/scripts/songselect/songwheel.lua b/scripts/songselect/songwheel.lua index 34754ea..030f687 100644 --- a/scripts/songselect/songwheel.lua +++ b/scripts/songselect/songwheel.lua @@ -114,6 +114,8 @@ game.LoadSkinSample('song_wheel/diff_change.wav') local scoreNumbers = Numbers.load_number_image("score_num") local difficultyNumbers = Numbers.load_number_image("diff_num") +local songselect_showEffectRadar = game.GetSkinSetting("songselect_showEffectRadar") or false + local LEADERBOARD_PLACE_NAMES = { '1st', '2nd', @@ -1051,6 +1053,7 @@ end ---This function is basically a workaround for the ForceRender call local function drawRadar() + if not songselect_showEffectRadar then return end if isFilterWheelActive or transitionLeaveScale ~= 0 then return end local x, y = 375, 650 -- 2.40.1 From 4f3131a38815a7ae348e01539614fac96825b869 Mon Sep 17 00:00:00 2001 From: Hersi Date: Thu, 23 Nov 2023 07:37:48 +0100 Subject: [PATCH 12/12] update changelog --- CHANGELOG | Bin 55484 -> 56850 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index c0b0bf076323b133b68bab7840bcab8d84d50ccd..801cdd9ca2aa04ede1cb81940c578c2bc3a9e51f 100644 GIT binary patch delta 1092 zcmZ9Ly>8S{5QQg76{Ho2$OV`<4Xxr-$xrVnbL^KJG6e9p{X z5f#d*JmaLa=tbO2#+;6^r!t!R>7OFx-bO3i5`iGMu{{I}#* za1Zx7tX7~S2h#Pw-v<8IyW49i3MG)-W~tu)sQSkjcUB|d)#iZC4SwvuChPv`){PAZ zxyZDQ%}23EtTC$2QKa;LUIkO_Rf_i7!8RTfBYTXkjiu2s1SgXrPx@b0>#oJ66g!}X?0YQ7>cuu9u--AL8)kGlX5oh}JkNg3kHXO-h-K zT?Sf9Q{5BtIldK<35>qaTHSEIs-87f