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)