Effect Radar Implementation v1 #46
|
@ -92,6 +92,28 @@ local function dump(o)
|
||||||
end
|
end
|
||||||
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 {
|
return {
|
||||||
split = split,
|
split = split,
|
||||||
filter = filter,
|
filter = filter,
|
||||||
|
@ -104,5 +126,7 @@ return {
|
||||||
mix = mix,
|
mix = mix,
|
||||||
modIndex = modIndex,
|
modIndex = modIndex,
|
||||||
firstAlphaNum = firstAlphaNum,
|
firstAlphaNum = firstAlphaNum,
|
||||||
dump = dump
|
dump = dump,
|
||||||
|
all = all,
|
||||||
|
any = any
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ require("api.point2d")
|
||||||
require("api.color")
|
require("api.color")
|
||||||
|
|
||||||
local Dim = require("common.dimensions")
|
local Dim = require("common.dimensions")
|
||||||
|
local Util = require("common.util")
|
||||||
|
|
||||||
Dim.updateResolution()
|
Dim.updateResolution()
|
||||||
|
|
||||||
|
@ -15,6 +16,8 @@ local RADAR_PURPLE = ColorRGBA.new(238, 130, 238)
|
||||||
local RADAR_MAGENTA = ColorRGBA.new(191, 70, 235)
|
local RADAR_MAGENTA = ColorRGBA.new(191, 70, 235)
|
||||||
local RADAR_GREEN = ColorRGBA.new(0, 255, 100)
|
local RADAR_GREEN = ColorRGBA.new(0, 255, 100)
|
||||||
|
|
||||||
|
local maxScaleFactor = 1.8
|
||||||
|
|
||||||
---@param p1 Point2D
|
---@param p1 Point2D
|
||||||
---@param p2 Point2D
|
---@param p2 Point2D
|
||||||
---@param width number
|
---@param width number
|
||||||
|
@ -282,8 +285,8 @@ function Radar:drawRadarMesh()
|
||||||
local colorCenter = ColorRGBA.new(112, 119, 255, 230) -- light blue-ish purple
|
local colorCenter = ColorRGBA.new(112, 119, 255, 230) -- light blue-ish purple
|
||||||
|
|
||||||
-- Calculate the maximum size based on the constraint
|
-- Calculate the maximum size based on the constraint
|
||||||
local maxSize = self.RADIUS * self.scale + 10
|
local maxSize = self.RADIUS * self.scale
|
||||||
local maxLineLength = maxSize + (maxSize / 2)
|
local maxLineLength = maxSize * maxScaleFactor
|
||||||
self._hexagonMesh:SetParam("maxSize", maxLineLength + .0)
|
self._hexagonMesh:SetParam("maxSize", maxLineLength + .0)
|
||||||
|
|
||||||
-- Set the color of the hexagon
|
-- Set the color of the hexagon
|
||||||
|
@ -304,8 +307,7 @@ function Radar:drawRadarMesh()
|
||||||
|
|
||||||
--local angle = math.rad(60 * (i-1)) + rotationAngle
|
--local angle = math.rad(60 * (i-1)) + rotationAngle
|
||||||
local scale = scaleFact[j]
|
local scale = scaleFact[j]
|
||||||
local lineLength = maxLineLength * scale
|
local lineLength = maxSize * scale
|
||||||
lineLength = math.min(lineLength, maxLineLength) -- Cap the length
|
|
||||||
local px = lineLength * math.cos(angle)
|
local px = lineLength * math.cos(angle)
|
||||||
local py = lineLength * math.sin(angle)
|
local py = lineLength * math.sin(angle)
|
||||||
table.insert(vertices, {{px, py}, {0, 0}})
|
table.insert(vertices, {{px, py}, {0, 0}})
|
||||||
|
@ -366,52 +368,220 @@ function Radar:updateGraph(info, dif)
|
||||||
--local txtFilePath = extractedSubstring .. "radar\\" .. dif .. ".txt"
|
--local txtFilePath = extractedSubstring .. "radar\\" .. dif .. ".txt"
|
||||||
|
|
||||||
--local song = io.open(txtFilePath, "r")
|
--local song = io.open(txtFilePath, "r")
|
||||||
local song = io.open(info.."/"..dif..".ksh")
|
local fullPath = info.."/"..dif..".ksh"
|
||||||
game.Log(info.."/"..dif..".ksh", game.LOGGER_DEBUG)
|
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)
|
game.Log(song and "file open" or "file not found", game.LOGGER_DEBUG)
|
||||||
if song then
|
if song then
|
||||||
local chartData = song:read("*all")
|
local chartData = song:read("*all")
|
||||||
song:close()
|
song:close()
|
||||||
|
|
||||||
local notesCount, knobCount, oneHandCount, handTripCount = 0, 0, 0, 0
|
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 trickyValue = 0
|
||||||
local totalMeasures = 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
|
for line in chartData:gmatch("[^\r\n]+") do
|
||||||
local noteType, fxType, laserType = line:match("(%d%d%d%d)|(%d%d)|([%-%a ]+)")
|
-- <bt-lanes x 4>|<fx-lanes x 2>|<laser-lanes x 2><lane-spin (optional)>
|
||||||
|
|
||||||
if noteType and fxType then
|
--game.Log(line, game.LOGGER_DEBUG)
|
||||||
local noteCount = noteType:match("1") and 1 or 0
|
|
||||||
|
|
||||||
notesCount = notesCount + noteCount
|
local patternBt = "([012][012][012][012])"
|
||||||
knobCount = knobCount + (fxType == "02" and 1 or 0)
|
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)
|
-- match line format
|
||||||
handTripCount = handTripCount + (laserType:match("[HKPUV:]") and 1 or 0)
|
|
||||||
if laserType ~= "--" then
|
local noteType, fxType, laserType = line:match(pattern)
|
||||||
tsumamiValue = tsumamiValue + 0.5
|
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
|
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
|
end
|
||||||
|
|
||||||
if line == "--" then
|
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
|
totalMeasures = totalMeasures + 1
|
||||||
elseif line:match("t=(%d+)") then
|
|
||||||
local bpm = tonumber(line:match("t=(%d+)"))
|
|
||||||
totalSongLength = math.max(totalSongLength, bpm)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if line:match("beat=") or line:match("stop=") or
|
local beat = line:match("beat=(%d+/%d+)")
|
||||||
line:match("zoom_top=") or line:match("zoom_bottom=") or line:match("center_split=") then
|
if beat then
|
||||||
trickyValue = trickyValue + 0.5
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
local graphValues = {
|
local graphValues = {
|
||||||
notes = (notesCount / totalSongLength),
|
notes = notesValue / totalMeasures,
|
||||||
peak = (notesCount / totalMeasures) * 10,
|
peak = peakValue,
|
||||||
tsumami = tsumamiValue,
|
tsumami = tsumamiValue / totalMeasures,
|
||||||
tricky = trickyValue,
|
tricky = trickyValue,
|
||||||
handtrip = handTripCount,
|
handtrip = handTripCount,
|
||||||
onehand = oneHandCount,
|
onehand = oneHandCount,
|
||||||
|
@ -422,21 +592,21 @@ function Radar:updateGraph(info, dif)
|
||||||
game.Log(k..": "..v, game.LOGGER_DEBUG)
|
game.Log(k..": "..v, game.LOGGER_DEBUG)
|
||||||
end
|
end
|
||||||
|
|
||||||
local scaleFactors = {
|
local calibration = {
|
||||||
notes = 2,
|
notes = 10,
|
||||||
peak = 50,
|
peak = 48,
|
||||||
tsumami = 500,
|
tsumami = 20000,
|
||||||
tricky = 50,
|
tricky = 128,
|
||||||
handtrip = 100,
|
handtrip = 300,
|
||||||
onehand = 50,
|
onehand = 300,
|
||||||
}
|
}
|
||||||
|
|
||||||
for key, factor in pairs(scaleFactors) do
|
for key, factor in pairs(calibration) do
|
||||||
-- Apply the scaling factor to each value
|
-- Apply the scaling factor to each value
|
||||||
self._graphdata[key] = graphValues[key] / factor
|
self._graphdata[key] = graphValues[key] / factor
|
||||||
|
|
||||||
-- Limit to a maximum of 125%
|
-- Limit to maximum scale factor
|
||||||
self._graphdata[key] = math.min(1.25, self._graphdata[key])
|
self._graphdata[key] = math.min(self._graphdata[key], maxScaleFactor)
|
||||||
end
|
end
|
||||||
|
|
||||||
game.Log("_graphdata", game.LOGGER_DEBUG)
|
game.Log("_graphdata", game.LOGGER_DEBUG)
|
||||||
|
|
Loading…
Reference in New Issue