450 lines
15 KiB
Lua
450 lines
15 KiB
Lua
--[[
|
|
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()
|
|
|
|
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);
|
|
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 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, offset, color, align)
|
|
---@class RadarAttributes
|
|
---@field text string
|
|
---@field offset Point2D
|
|
---@field color ColorRGBA
|
|
local o = {
|
|
text = text or "",
|
|
offset = offset or Point2D.ZERO,
|
|
color = color or ColorRGBA.BLACK,
|
|
align = align or gfx.TEXT_ALIGN_CENTER + gfx.TEXT_ALIGN_BASELINE
|
|
}
|
|
|
|
setmetatable(o, RadarAttributes)
|
|
return o
|
|
end
|
|
}
|
|
RadarAttributes.__index = RadarAttributes
|
|
|
|
|
|
---@class CRadar
|
|
Radar = {
|
|
---@type RadarAttributes[][]
|
|
ATTRIBUTES = {
|
|
{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(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(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,
|
|
|
|
---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 = {}, ---@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 outlineRadius = Radar.RADIUS
|
|
local attributeRadius = Radar.RADIUS + 30
|
|
|
|
for i = 0, sides - 1 do
|
|
local attrIdx = i + 1
|
|
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))
|
|
-- 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.offset.x
|
|
attributePos.y = attributePos.y + attr.offset.y
|
|
table.insert(o._attributePositions[attrIdx], 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.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(attr.align)
|
|
renderOutlinedText(pos, string.upper(attr.text), 1, attr.color)
|
|
end
|
|
end
|
|
|
|
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
|
|
|
|
local scaleFact = {
|
|
self._graphdata.notes,
|
|
self._graphdata.peak,
|
|
self._graphdata.tsumami,
|
|
self._graphdata.tricky,
|
|
self._graphdata.handtrip,
|
|
self._graphdata.onehand,
|
|
}
|
|
|
|
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
|
|
local maxLineLength = maxSize + (maxSize / 2)
|
|
self._hexagonMesh:SetParam("maxSize", maxLineLength + .0)
|
|
|
|
-- Set the color of the hexagon
|
|
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 vertices = {}
|
|
table.insert(vertices, {{0, 0}, {0, 0}})
|
|
for i = 0, sides do
|
|
local j = i % sides + 1
|
|
local angle = i * self._angleStep - self._initRotation
|
|
|
|
--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:Draw()
|
|
|
|
-- 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()
|
|
|
|
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(fillColor)
|
|
self:drawOutline(3, strokeColor)
|
|
self:drawRadarMesh()
|
|
self:drawRadialTicks(strokeColor)
|
|
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
|