Effect Radar Implementation v1 #46

Merged
hersi merged 12 commits from feature/radar into master 2023-11-23 07:41:15 +01:00
18 changed files with 1438 additions and 449 deletions

BIN
CHANGELOG

Binary file not shown.

View File

@ -97,5 +97,14 @@
"label": "Show debug information (sometimes in the middle of the screen when you're playing)", "label": "Show debug information (sometimes in the middle of the screen when you're playing)",
"type": "bool", "type": "bool",
"default": false "default": false
},
"separator_g": {},
"Experimental features": { "type": "label" },
"songselect_showEffectRadar": {
"label": "Show Effect Radar for compatible songs (VERY WIP)",
"type": "bool",
"default": false
} }
} }

View File

@ -23,47 +23,53 @@ IRData = {}
---@field serverTime integer ---@field serverTime integer
---@field serverName string ---@field serverName string
---@field irVersion string ---@field irVersion string
IRHeartbeatResponseBody = {}
---@class IRRecordResponseBody ---@class IRRecordResponseBody
---@field record ServerScore ---@field record ServerScore
IRRecordResponseBody = {}
---@class IRLeaderboardResponseBody ---@alias IRLeaderboardResponseBody ServerScore[]
---@field scores ServerScore[]
IRLeaderboardResponseBody = {}
---@class IRResponse ---@class IRResponse
---@field statusCode integer ---@field statusCode integer
---@field description string ---@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. -- 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 local function Heartbeat(callback) end
-- Performs a Chart Tracked request for the chart with the provided hash. -- Performs a Chart Tracked request for the chart with the provided hash.
---@param hash string # song 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 local function ChartTracked(hash, callback) end
-- Performs a Record request for the chart with the provided hash. -- Performs a Record request for the chart with the provided hash.
---@param hash string # song 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 local function Record(hash, callback) end
-- Performs a Leaderboard request for the chart with the provided hash, with parameters mode and n. -- Performs a Leaderboard request for the chart with the provided hash, with parameters mode and n.
---@param hash string # song hash ---@param hash string # song hash
---@param mode "best"|"rivals" # request leaderboard mode ---@param mode "best"|"rivals" # request leaderboard mode
---@param n integer # limit the number of requested scores ---@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 local function Leaderboard(hash, mode, n, callback) end
---@type table ---@class IR
IR = { IR = {
Heartbeat = Heartbeat, Heartbeat = Heartbeat,
ChartTracked = ChartTracked, ChartTracked = ChartTracked,
Record = Record, Record = Record,
Leaderboard = Leaderboard Leaderboard = Leaderboard
} }

View File

@ -267,7 +267,7 @@ LoadSharedSkinTexture = function(name, path) end
-- Loads a font fromt the specified filename -- Loads a font fromt the specified filename
-- Sets it as the current font if it is already loaded -- Sets it as the current font if it is already loaded
---@param name? string ---@param name string
---@param filename string ---@param filename string
LoadFont = function(name, filename) end LoadFont = function(name, filename) end
@ -280,11 +280,10 @@ LoadFont = function(name, filename) end
---@return any # returns `placeholder` until the image is loaded ---@return any # returns `placeholder` until the image is loaded
LoadImageJob = function(filepath, placeholder, w, h) end LoadImageJob = function(filepath, placeholder, w, h) end
-- Loads a font from `skins/<skin>/textures/<path>` -- Loads a font from `skins/<skin>/fonts/<name>`
-- Sets it as the current font if it is already loaded -- Sets it as the current font if it is already loaded
---@param name? string ---@param name string
---@param filename string LoadSkinFont = function(name) end
LoadSkinFont = function(name, filename) end
-- Loads an image outside of the main thread to prevent rendering lock-up -- 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 -- Image will be loaded at original size unless `w` and `h` are provided
@ -472,7 +471,7 @@ UpdateImagePattern = function(pattern, sx, sy, ix, iy, angle, alpha) end
---@param size? integer ---@param size? integer
UpdateLabel = function(label, text, size) end UpdateLabel = function(label, text, size) end
---@type table ---@class gfx
gfx = { gfx = {
BLEND_ZERO = 1, BLEND_ZERO = 1,
BLEND_ONE = 2, BLEND_ONE = 2,
@ -595,4 +594,4 @@ gfx = {
Translate = Translate, Translate = Translate,
UpdateImagePattern = UpdateImagePattern, UpdateImagePattern = UpdateImagePattern,
UpdateLabel = UpdateLabel, UpdateLabel = UpdateLabel,
}; };

View File

@ -1,117 +1,4 @@
-- Adds a texture that was loaded with `gfx.LoadSharedTexture` to the material that can be used in the shader code ---@diagnostic disable:missing-return
---@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/<skin>/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
---@class ShadedMesh ---@class ShadedMesh
ShadedMesh = { ShadedMesh = {
@ -125,57 +12,148 @@ ShadedMesh = {
PRIM_LINELIST = 3, PRIM_LINELIST = 3,
PRIM_LINESTRIP = 4, PRIM_LINESTRIP = 4,
PRIM_POINTLIST = 5, 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 -- Adds a texture that was loaded with `gfx.LoadSharedTexture` to the material that can be used in the shader code
---@return number length ---@param uniformName string
GetLength = function() end ---@param textureName string
function ShadedMesh:AddSharedTexture(uniformName, textureName) end
-- Sets the y-scale of the mesh based on its length -- Adds a texture to the material that can be used in the shader code
-- Useful for creating fake buttons which may have variable length based on duration ---@param uniformName string
---@param length number ---@param path string # prepended with `skins/<skin>/textures/`
ScaleToLength = function(length) end function ShadedMesh:AddSkinTexture(uniformName, path) end
-- Stops meshes beyond the track from being rendered if `doClip` -- Adds a texture to the material that can be used in the shader code
---@param doClip boolean ---@param uniformName string
SetClipWithTrack = function(doClip) end ---@param path string
function ShadedMesh:AddTexture(uniformName, path) end
-- Sets the length (in the y-direction relative to the track) of the mesh -- Gets the translation of the mesh
---@param length number # Optional constants: `BUTTON_TEXTURE_LENGTH`, `FXBUTTON_TEXTURE_LENGTH`, and `TRACK_LENGTH` ---@return number x, number y, number z
SetLength = function(length) end 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 ---@class ShadedMeshOnTrack : ShadedMesh
---@field BUTTON_TEXTURE_LENGTH number ---@field BUTTON_TEXTURE_LENGTH number
---@field FXBUTTON_TEXTURE_LENGTH number ---@field FXBUTTON_TEXTURE_LENGTH number
---@field TRACK_LENGTH number ---@field TRACK_LENGTH number
ShadedMeshOnTrack = { ShadedMeshOnTrack = {
GetLength = GetLength, };
UseGameMesh = UseGameMesh,
ScaleToLength = ScaleToLength, -- Gets the length of the mesh
SetClipWithTrack = SetClipWithTrack, ---@return number length
SetLength = SetLength, 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

View File

@ -1,3 +1,4 @@
---@diagnostic disable: lowercase-global
-- songwheel `songwheel` table -- songwheel `songwheel` table
---@class SongWheelScore ---@class SongWheelScore
@ -35,7 +36,7 @@ SongWheelDifficulty = {}
---@class SongWheelSong ---@class SongWheelSong
---@field artist string # Chart artist ---@field artist string # Chart artist
---@field difficulties SongWheelDifficulty[] # Array of difficulties for the current song ---@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 id integer # Song id, unique static identifier
---@field path string # Full filepath to the chart folder on the disk ---@field path string # Full filepath to the chart folder on the disk
---@field title string # Chart title ---@field title string # Chart title

BIN
fonts/contb.ttf Normal file

Binary file not shown.

BIN
fonts/contl.ttf Normal file

Binary file not shown.

BIN
fonts/contm.ttf Normal file

Binary file not shown.

74
scripts/api/color.lua Normal file
View File

@ -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

28
scripts/api/point2d.lua Normal file
View File

@ -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

View File

@ -59,6 +59,10 @@ local function lerp(x, x0, y0, x1, y1)
return y0 + (x - x0) * (y1 - y0) / (x1 - x0) return y0 + (x - x0) * (y1 - y0) / (x1 - x0)
end end
local function mix(x, y, a)
return (1 - a) * x + a * y
end
--modulo operation for index value --modulo operation for index value
local function modIndex(index, mod) local function modIndex(index, mod)
return (index - 1) % mod + 1 return (index - 1) % mod + 1
@ -75,6 +79,41 @@ local function firstAlphaNum(s)
return ''; return '';
end 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
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,
@ -84,6 +123,10 @@ return {
roundToZero = roundToZero, roundToZero = roundToZero,
areaOverlap = areaOverlap, areaOverlap = areaOverlap,
lerp = lerp, lerp = lerp,
mix = mix,
modIndex = modIndex, modIndex = modIndex,
firstAlphaNum = firstAlphaNum, firstAlphaNum = firstAlphaNum,
dump = dump,
all = all,
any = any
} }

View File

@ -0,0 +1,619 @@
--[[
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")
local Util = require("common.util")
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)
local maxScaleFactor = 1.8
---@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
local maxLineLength = maxSize * maxScaleFactor
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 = maxSize * scale
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 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 lastNotes = {}
local lastFx = {}
local measureLength = 0
---@cast chartData string
for line in chartData:gmatch("[^\r\n]+") do
-- <bt-lanes x 4>|<fx-lanes x 2>|<laser-lanes x 2><lane-spin (optional)>
--game.Log(line, game.LOGGER_DEBUG)
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
-- 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
end
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 = notesValue / totalMeasures,
peak = peakValue,
tsumami = tsumamiValue / totalMeasures,
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 calibration = {
notes = 10,
peak = 48,
tsumami = 20000,
tricky = 128,
handtrip = 300,
onehand = 300,
}
for key, factor in pairs(calibration) do
-- Apply the scaling factor to each value
self._graphdata[key] = graphValues[key] / factor
-- Limit to maximum scale factor
self._graphdata[key] = math.min(self._graphdata[key], maxScaleFactor)
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

View File

@ -9,6 +9,9 @@ local Numbers = require('components.numbers')
local VolforceCalc = require('components.volforceCalc') local VolforceCalc = require('components.volforceCalc')
require("api.point2d")
require("components.radar")
local dataPanelImage = gfx.CreateSkinImage("song_select/data_bg_overlay.png", 1) 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 dataGlowOverlayImage = gfx.CreateSkinImage("song_select/data_panel/data_glow_overlay.png", 1)
local gradeBgImage = gfx.CreateSkinImage("song_select/data_panel/grade_bg.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 defaultJacketImage = gfx.CreateSkinImage("song_select/loading.png", 0)
local difficultyLabelImages = { local difficultyLabelImages = {
gfx.CreateSkinImage("song_select/plate/difficulty_labels/novice.png", 1), gfx.CreateSkinImage("song_select/plate/difficulty_labels/novice.png", 1),
gfx.CreateSkinImage("song_select/plate/difficulty_labels/advanced.png", 1), gfx.CreateSkinImage("song_select/plate/difficulty_labels/advanced.png", 1),
@ -113,6 +114,8 @@ game.LoadSkinSample('song_wheel/diff_change.wav')
local scoreNumbers = Numbers.load_number_image("score_num") local scoreNumbers = Numbers.load_number_image("score_num")
local difficultyNumbers = Numbers.load_number_image("diff_num") local difficultyNumbers = Numbers.load_number_image("diff_num")
local songselect_showEffectRadar = game.GetSkinSetting("songselect_showEffectRadar") or false
local LEADERBOARD_PLACE_NAMES = { local LEADERBOARD_PLACE_NAMES = {
'1st', '1st',
'2nd', '2nd',
@ -125,13 +128,16 @@ local songPlateHeight = 172
local selectedIndex = 1 local selectedIndex = 1
local selectedDifficulty = 1 local selectedDifficulty = 1
local radar = Radar.new(Point2D.new(0, 0))
local updateRadar = true
local jacketCache = {} local jacketCache = {}
local top50diffs = {} local top50diffs = {}
local irRequestStatus = 1 -- 0=unused, 1=not requested, 2=loading, others are status codes local irRequestStatus = 1 -- 0=unused, 1=not requested, 2=loading, others are status codes
local irRequestTimeout = 2 local irRequestTimeout = 2
local irLeaderboard = {} local irLeaderboard = {} ---@type ServerScore[]|{}
local irLeaderboardsCache = {} local irLeaderboardsCache = {}
local transitionScrollScale = 0 local transitionScrollScale = 0
@ -158,6 +164,7 @@ local transitionSearchInfoEnterScale = 0
local transitionSearchBackgroundAlpha = 0 local transitionSearchBackgroundAlpha = 0
local transitionSearchbarOffsetY = 0 local transitionSearchbarOffsetY = 0
local transitionSearchInfoOffsetY = 0 local transitionSearchInfoOffsetY = 0
local transitionSearchBackgroundInfoAlpha = Easing.inOutQuad(transitionSearchInfoEnterScale)
local transitionLaserScale = 0 local transitionLaserScale = 0
local transitionLaserY = 0 local transitionLaserY = 0
@ -195,32 +202,32 @@ local resolutionChange = function(x, y)
game.Log('resX:' .. resX .. ' // resY:' .. resY .. ' // fullX:' .. fullX .. ' // fullY:' .. fullY, game.LOGGER_ERROR) game.Log('resX:' .. resX .. ' // resY:' .. resY .. ' // fullX:' .. fullX .. ' // fullY:' .. fullY, game.LOGGER_ERROR)
end end
function getCorrectedIndex(from, offset) local function getCorrectedIndex(from, offset)
total = #songwheel.songs local total = #songwheel.songs
if (math.abs(offset) > total) then if (math.abs(offset) > total) then
if (offset < 0) then if (offset < 0) then
offset = offset + total*math.floor(math.abs(offset)/total) offset = offset + total*math.floor(math.abs(offset)/total)
else else
offset = offset - total*math.floor(math.abs(offset)/total) offset = offset - total*math.floor(math.abs(offset)/total)
end end
end end
index = from + offset local index = from + offset
if index < 1 then if index < 1 then
index = total + (from+offset) -- this only happens if the offset is negative index = total + (from+offset) -- this only happens if the offset is negative
end end
if index > total then if index > total then
indexesUntilEnd = total - from local indexesUntilEnd = total - from
index = offset - indexesUntilEnd -- this only happens if the offset is positive index = offset - indexesUntilEnd -- this only happens if the offset is positive
end end
return index return index
end end
function getJacketImage(song) local function getJacketImage(song)
if not jacketCache[song.id] or jacketCache[song.id]==defaultJacketImage then if not jacketCache[song.id] or jacketCache[song.id]==defaultJacketImage then
jacketCache[song.id] = gfx.LoadImageJob(song.difficulties[ jacketCache[song.id] = gfx.LoadImageJob(song.difficulties[
math.min(selectedDifficulty, #song.difficulties) math.min(selectedDifficulty, #song.difficulties)
@ -230,12 +237,12 @@ function getJacketImage(song)
return jacketCache[song.id] return jacketCache[song.id]
end end
function getGradeImageForScore(score) local function getGradeImageForScore(score)
local gradeImage = gradeImages.none local gradeImage = gradeImages.none
local bestGradeCutoff = 0 local bestGradeCutoff = 0
for gradeName, scoreCutoff in pairs(gradeCutoffs) do for gradeName, scoreCutoff in pairs(gradeCutoffs) do
if scoreCutoff <= score then if scoreCutoff <= score then
if scoreCutoff > bestGradeCutoff then if scoreCutoff > bestGradeCutoff then
gradeImage = gradeImages[gradeName] gradeImage = gradeImages[gradeName]
bestGradeCutoff = scoreCutoff bestGradeCutoff = scoreCutoff
end end
@ -245,31 +252,30 @@ function getGradeImageForScore(score)
return gradeImage return gradeImage
end end
function drawLaserAnim() local function drawLaserAnim()
gfx.Save() gfx.Save()
gfx.BeginPath() gfx.BeginPath()
gfx.Scissor(0, transitionLaserY, desw, 100) gfx.Scissor(0, transitionLaserY, desw, 100)
gfx.ImageRect(0, 0, desw, desh, laserAnimBaseImage, 1, 0) gfx.ImageRect(0, 0, desw, desh, laserAnimBaseImage, 1, 0)
gfx.Restore() gfx.Restore()
end end
function drawBackground(deltaTime) local function drawBackground(deltaTime)
Background.draw(deltaTime) Background.draw(deltaTime)
local song = songwheel.songs[selectedIndex] local song = songwheel.songs[selectedIndex]
local diff = song and song.difficulties[selectedDifficulty] or false local diff = song and song.difficulties[selectedDifficulty] or false
if (not isFilterWheelActive and transitionLeaveReappearTimer == 0) then if (not isFilterWheelActive and transitionLeaveReappearTimer == 0) then
-- If the score for song exists -- If the score for song exists
if song and diff then if song and diff then
local jacketImage = getJacketImage(song) local jacketImage = getJacketImage(song)
gfx.BeginPath() gfx.BeginPath()
gfx.ImageRect(transitionJacketBgScrollPosX, 0, 900, 900, jacketImage or defaultJacketImage, transitionJacketBgScrollAlpha, 0) gfx.ImageRect(transitionJacketBgScrollPosX, 0, 900, 900, jacketImage or defaultJacketImage, transitionJacketBgScrollAlpha, 0)
gfx.BeginPath() gfx.BeginPath()
gfx.FillColor(0,0,0,math.floor(transitionJacketBgScrollAlpha*64)) gfx.FillColor(0,0,0,math.floor(transitionJacketBgScrollAlpha*64))
gfx.Rect(0,0,900,900) gfx.Rect(0,0,900,900)
@ -283,7 +289,7 @@ function drawBackground(deltaTime)
drawLaserAnim() 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.BeginPath()
gfx.ImageRect(0, 0, desw, desh, dataGlowOverlayImage, transitionAfterscrollDataOverlayAlpha, 0) gfx.ImageRect(0, 0, desw, desh, dataGlowOverlayImage, transitionAfterscrollDataOverlayAlpha, 0)
gfx.BeginPath() gfx.BeginPath()
@ -300,7 +306,9 @@ function drawBackground(deltaTime)
end end
function drawSong(song, y) ---@param song SongWheelSong
---@param y number
local function drawSong(song, y)
if (not song) then return end if (not song) then return end
local songX = desw/2+28 local songX = desw/2+28
@ -311,14 +319,14 @@ function drawSong(song, y)
end end
local bestScore local bestScore
if selectedSongDifficulty.scores then if selectedSongDifficulty.scores then
bestScore = selectedSongDifficulty.scores[1] bestScore = selectedSongDifficulty.scores[1]
end end
-- Draw the bg for the song plate -- Draw the bg for the song plate
gfx.BeginPath() gfx.BeginPath()
gfx.ImageRect(songX, y, 515, 172, songPlateBg, 1, 0) gfx.ImageRect(songX, y, 515, 172, songPlateBg, 1, 0)
-- Draw jacket -- Draw jacket
local jacketImage = getJacketImage(song) local jacketImage = getJacketImage(song)
gfx.BeginPath() gfx.BeginPath()
@ -327,7 +335,7 @@ function drawSong(song, y)
-- Draw the overlay for the song plate (that bottom black bar) -- Draw the overlay for the song plate (that bottom black bar)
gfx.BeginPath() gfx.BeginPath()
gfx.ImageRect(songX, y, 515, 172, songPlateBottomBarOverlayImage, 1, 0) gfx.ImageRect(songX, y, 515, 172, songPlateBottomBarOverlayImage, 1, 0)
-- Draw the difficulty notch background -- Draw the difficulty notch background
gfx.BeginPath() gfx.BeginPath()
local diffIndex = Charting.GetDisplayDifficulty(selectedSongDifficulty.jacketPath, selectedSongDifficulty.difficulty) local diffIndex = Charting.GetDisplayDifficulty(selectedSongDifficulty.jacketPath, selectedSongDifficulty.difficulty)
@ -347,7 +355,7 @@ function drawSong(song, y)
if selectedSongDifficulty.topBadge then if selectedSongDifficulty.topBadge then
badgeImage = badgeImages[selectedSongDifficulty.topBadge+1] badgeImage = badgeImages[selectedSongDifficulty.topBadge+1]
end end
local badgeAlpha = 1 local badgeAlpha = 1
if (selectedSongDifficulty.topBadge >= 3) then if (selectedSongDifficulty.topBadge >= 3) then
badgeAlpha = transitionFlashAlpha -- If hard clear or above, flash the badge badgeAlpha = transitionFlashAlpha -- If hard clear or above, flash the badge
@ -360,7 +368,7 @@ function drawSong(song, y)
local gradeImage = gradeImages.none local gradeImage = gradeImages.none
local gradeAlpha = 1 local gradeAlpha = 1
if bestScore then if bestScore then
gradeImage = getGradeImageForScore(bestScore.score) gradeImage = getGradeImageForScore(bestScore.score)
if (bestScore.score >= gradeCutoffs.S) then if (bestScore.score >= gradeCutoffs.S) then
@ -372,13 +380,13 @@ function drawSong(song, y)
gfx.ImageRect(songX+391, y+47, 60, 60, gradeImage, gradeAlpha, 0) gfx.ImageRect(songX+391, y+47, 60, 60, gradeImage, gradeAlpha, 0)
-- Draw top 50 label if applicable -- Draw top 50 label if applicable
if (top50diffs[selectedSongDifficulty.id]) then if (top50diffs[selectedSongDifficulty.hash]) then
gfx.BeginPath() gfx.BeginPath()
gfx.ImageRect(songX+82, y+109, 506*0.85, 26*0.85, top50OverlayImage, 1, 0) gfx.ImageRect(songX+82, y+109, 506*0.85, 26*0.85, top50OverlayImage, 1, 0)
end end
end end
function drawSongList() local function drawSongList()
gfx.GlobalAlpha(1-transitionLeaveScale) gfx.GlobalAlpha(1-transitionLeaveScale)
local numOfSongsAround = 7 -- How many songs should be up and how many should be down of the selected one local numOfSongsAround = 7 -- How many songs should be up and how many should be down of the selected one
@ -391,7 +399,7 @@ function drawSongList()
drawSong(songwheel.songs[songIndex], desh/2-songPlateHeight/2-songPlateHeight*i + yOffset) drawSong(songwheel.songs[songIndex], desh/2-songPlateHeight/2-songPlateHeight*i + yOffset)
i=i+1 i=i+1
end end
-- Draw the selected song -- Draw the selected song
drawSong(songwheel.songs[selectedIndex], desh/2-songPlateHeight/2 + yOffset) drawSong(songwheel.songs[selectedIndex], desh/2-songPlateHeight/2 + yOffset)
@ -405,128 +413,8 @@ function drawSongList()
gfx.GlobalAlpha(1) gfx.GlobalAlpha(1)
end 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
gfx.Save()
-- Draw best score
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.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.FontSize(28)
gfx.GlobalAlpha(transitionAfterscrollTextSongTitle)
gfx.Text(song.title, 30+(1-transitionAfterscrollTextSongTitle)*20, 955)
-- Draw artist
gfx.GlobalAlpha(transitionAfterscrollTextSongArtist)
gfx.Text(song.artist, 30+(1-transitionAfterscrollTextSongArtist)*30, 997)
gfx.GlobalAlpha(1)
-- Draw difficulties
local DIFF_X_START = 98.5
local DIFF_GAP = 114.8
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.GlobalAlpha(1)
-- Scoreboard
drawLocalLeaderboard(diff)
drawIrLeaderboard()
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)
end
---@param diff SongWheelDifficulty ---@param diff SongWheelDifficulty
function drawLocalLeaderboard(diff) local function drawLocalLeaderboard(diff)
gfx.LoadSkinFont('Digital-Serial-Bold.ttf') gfx.LoadSkinFont('Digital-Serial-Bold.ttf')
gfx.FontSize(26) gfx.FontSize(26)
@ -568,31 +456,32 @@ function drawLocalLeaderboard(diff)
gfx.Text(username or "-", sbBarContentLeftX, scoreBoardY + sbBarHeight/2 + i*sbBarHeight) gfx.Text(username or "-", sbBarContentLeftX, scoreBoardY + sbBarHeight/2 + i*sbBarHeight)
gfx.BeginPath() 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
end end
function drawIrLeaderboard() local function drawIrLeaderboard()
if not IRData.Active then if not IRData.Active then
return return
end end
gfx.LoadSkinFont('Digital-Serial-Bold.ttf') gfx.LoadSkinFont('Digital-Serial-Bold.ttf')
gfx.FontSize(26) gfx.FontSize(26)
local scoreBoardX = 75 local scoreBoardX = 75
local scoreBoardY = 1500 local scoreBoardY = 1500
local sbBarWidth = 336*1.2 local sbBarWidth = 336*1.2
local sbBarHeight = 33 local sbBarHeight = 33
local sbBarContentLeftX = scoreBoardX + 80 local sbBarContentLeftX = scoreBoardX + 80
local sbBarContentRightX = scoreBoardX + sbBarWidth/2 + 30 local sbBarContentRightX = scoreBoardX + sbBarWidth/2 + 30
-- Draw the header -- Draw the header
gfx.BeginPath() gfx.BeginPath()
gfx.ImageRect(scoreBoardX, scoreBoardY, sbBarWidth, sbBarHeight, scoreBoardBarBgImage, 1, 0) gfx.ImageRect(scoreBoardX, scoreBoardY, sbBarWidth, sbBarHeight, scoreBoardBarBgImage, 1, 0)
gfx.TextAlign(gfx.TEXT_ALIGN_CENTER + gfx.TEXT_ALIGN_MIDDLE) gfx.TextAlign(gfx.TEXT_ALIGN_CENTER + gfx.TEXT_ALIGN_MIDDLE)
gfx.BeginPath() gfx.BeginPath()
@ -628,8 +517,8 @@ function drawIrLeaderboard()
-- Becuase the scores are in "random order", we have to do this -- Becuase the scores are in "random order", we have to do this
for index, irScore in ipairs(irLeaderboard) do for index, irScore in ipairs(irLeaderboard) do
-- local irScore = irLeaderboard[i] -- local irScore = irLeaderboard[i]
if irScore then if irScore then
local rank = index local rank = index
gfx.TextAlign(gfx.TEXT_ALIGN_CENTER + gfx.TEXT_ALIGN_MIDDLE) gfx.TextAlign(gfx.TEXT_ALIGN_CENTER + gfx.TEXT_ALIGN_MIDDLE)
gfx.BeginPath() gfx.BeginPath()
@ -638,7 +527,7 @@ function drawIrLeaderboard()
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_MIDDLE) gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_MIDDLE)
gfx.BeginPath() gfx.BeginPath()
gfx.Text(string.upper(irScore.username), sbBarContentLeftX, scoreBoardY + sbBarHeight/2 + rank*sbBarHeight) gfx.Text(string.upper(irScore.username), sbBarContentLeftX, scoreBoardY + sbBarHeight/2 + rank*sbBarHeight)
gfx.BeginPath() gfx.BeginPath()
gfx.Text(string.format("%d", irScore.score), sbBarContentRightX, scoreBoardY + sbBarHeight/2 + rank*sbBarHeight) gfx.Text(string.format("%d", irScore.score), sbBarContentRightX, scoreBoardY + sbBarHeight/2 + rank*sbBarHeight)
@ -649,7 +538,130 @@ function drawIrLeaderboard()
end end
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') gfx.LoadSkinFont('NotoSans-Regular.ttf')
if (songwheel.searchInputActive) then if (songwheel.searchInputActive) then
@ -658,17 +670,17 @@ function drawFilterInfo(deltatime)
gfx.BeginPath() gfx.BeginPath()
gfx.ImageRect(5, 95, 417*0.85, 163*0.85, filterInfoBgImage, 1, 0) gfx.ImageRect(5, 95, 417*0.85, 163*0.85, filterInfoBgImage, 1, 0)
local folderLabel = game.GetSkinSetting('_songWheelActiveFolderLabel') local folderLabel = game.GetSkinSetting('_songWheelActiveFolderLabel')
local subFolderLabel = game.GetSkinSetting('_songWheelActiveSubFolderLabel') local subFolderLabel = game.GetSkinSetting('_songWheelActiveSubFolderLabel')
local sortOptionLabel = game.GetSkinSetting('_songWheelActiveSortOptionLabel') local sortOptionLabel = game.GetSkinSetting('_songWheelActiveSortOptionLabel')
gfx.FontSize(24) gfx.FontSize(24)
gfx.TextAlign(gfx.TEXT_ALIGN_CENTER + gfx.TEXT_ALIGN_MIDDLE) gfx.TextAlign(gfx.TEXT_ALIGN_CENTER + gfx.TEXT_ALIGN_MIDDLE)
gfx.BeginPath() gfx.BeginPath()
gfx.Text(folderLabel or '', 167, 131) gfx.Text(folderLabel or '', 167, 131)
gfx.BeginPath() gfx.BeginPath()
gfx.Text(subFolderLabel or '', 195, 166) gfx.Text(subFolderLabel or '', 195, 166)
@ -679,7 +691,7 @@ function drawFilterInfo(deltatime)
gfx.Text(sortOptionLabel or '', desw-150, 130) gfx.Text(sortOptionLabel or '', desw-150, 130)
end end
function drawCursor() local function drawCursor()
if isFilterWheelActive or transitionLeaveScale ~= 0 then return false end if isFilterWheelActive or transitionLeaveScale ~= 0 then return false end
gfx.BeginPath() gfx.BeginPath()
@ -690,7 +702,7 @@ function drawCursor()
gfx.ImageRect(desw / 2 - 14, desh / 2 - 213 / 2, 555, 213, cursorImage, 1, 0) gfx.ImageRect(desw / 2 - 14, desh / 2 - 213 / 2, 555, 213, cursorImage, 1, 0)
end end
function drawSearch() local function drawSearch()
if (not songwheel.searchInputActive and searchPreviousActiveState) then if (not songwheel.searchInputActive and searchPreviousActiveState) then
searchPreviousActiveState = false searchPreviousActiveState = false
game.PlaySample('sort_wheel/enter.wav') game.PlaySample('sort_wheel/enter.wav')
@ -698,7 +710,7 @@ function drawSearch()
searchPreviousActiveState = true searchPreviousActiveState = true
game.PlaySample('sort_wheel/leave.wav') game.PlaySample('sort_wheel/leave.wav')
end end
if (songwheel.searchText ~= '' and searchInfoPreviousActiveState == true) then if (songwheel.searchText ~= '' and searchInfoPreviousActiveState == true) then
searchInfoPreviousActiveState = false searchInfoPreviousActiveState = false
elseif (songwheel.searchText == '' and searchInfoPreviousActiveState == false) then elseif (songwheel.searchText == '' and searchInfoPreviousActiveState == false) then
@ -724,7 +736,7 @@ function drawSearch()
local infoXPos = 0 local infoXPos = 0
local infoYStartPos = desh - sh - 772 + 242 local infoYStartPos = desh - sh - 772 + 242
local infoYPos = infoYStartPos + transitionSearchInfoOffsetY local infoYPos = infoYStartPos + transitionSearchInfoOffsetY
if (game.GetSkinSetting('gameplay_showSearchControls')) then if (game.GetSkinSetting('gameplay_showSearchControls')) then
gfx.ImageRect(infoXPos, infoYPos, sw, sh, searchInfoPanelImage, transitionSearchBackgroundInfoAlpha, 0) gfx.ImageRect(infoXPos, infoYPos, sw, sh, searchInfoPanelImage, transitionSearchBackgroundInfoAlpha, 0)
end end
@ -760,7 +772,7 @@ function drawSearch()
gfx.Text(songwheel.searchText, xPos + 160, yPos + 83.2) gfx.Text(songwheel.searchText, xPos + 160, yPos + 83.2)
end end
function drawScrollbar() local function drawScrollbar()
if isFilterWheelActive or transitionLeaveScale ~= 0 then return end if isFilterWheelActive or transitionLeaveScale ~= 0 then return end
-- Scrollbar Background -- Scrollbar Background
@ -803,7 +815,33 @@ function drawScrollbar()
end end
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 if not IRData.Active then
return return
end end
@ -832,56 +870,10 @@ function refreshIrLeaderboard(deltaTime)
end end
irRequestStatus = 2 -- Loading irRequestStatus = 2 -- Loading
-- onIrLeaderboardFetched({
-- statusCode = 20,
-- body = {}
-- })
IR.Leaderboard(diff.hash, 'best', 4, onIrLeaderboardFetched) IR.Leaderboard(diff.hash, 'best', 4, onIrLeaderboardFetched)
end end
function dump(o) local function tickTransitions(deltaTime)
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)
if transitionScrollScale < 1 then if transitionScrollScale < 1 then
transitionScrollScale = transitionScrollScale + deltaTime / 0.1 -- transition should last for that time in seconds transitionScrollScale = transitionScrollScale + deltaTime / 0.1 -- transition should last for that time in seconds
else else
@ -897,7 +889,7 @@ function tickTransitions(deltaTime)
transitionAfterscrollScale = 1 transitionAfterscrollScale = 1
end end
if scrollingUp then if scrollingUp then
transitionScrollOffsetY = Easing.inQuad(1-transitionScrollScale) * songPlateHeight transitionScrollOffsetY = Easing.inQuad(1-transitionScrollScale) * songPlateHeight
else else
transitionScrollOffsetY = Easing.inQuad(1-transitionScrollScale) * -songPlateHeight transitionScrollOffsetY = Easing.inQuad(1-transitionScrollScale) * -songPlateHeight
@ -942,8 +934,6 @@ function tickTransitions(deltaTime)
end end
end end
transitionSearchBackgroundInfoAlpha = Easing.inOutQuad(transitionSearchInfoEnterScale)
-- Grade alpha -- Grade alpha
if transitionAfterscrollScale >= 0.03 and transitionAfterscrollScale < 0.033 then if transitionAfterscrollScale >= 0.03 and transitionAfterscrollScale < 0.033 then
transitionAfterscrollGradeAlpha = 0.5 transitionAfterscrollGradeAlpha = 0.5
@ -952,7 +942,7 @@ function tickTransitions(deltaTime)
else else
transitionAfterscrollGradeAlpha = 0 transitionAfterscrollGradeAlpha = 0
end end
-- Badge alpha -- Badge alpha
if transitionAfterscrollScale >= 0.032 and transitionAfterscrollScale < 0.035 then if transitionAfterscrollScale >= 0.032 and transitionAfterscrollScale < 0.035 then
transitionAfterscrollBadgeAlpha = 0.5 transitionAfterscrollBadgeAlpha = 0.5
@ -974,7 +964,7 @@ function tickTransitions(deltaTime)
else else
transitionAfterscrollTextSongArtist = 1 transitionAfterscrollTextSongArtist = 1
end end
-- Difficulties alpha -- Difficulties alpha
if transitionAfterscrollScale < 0.025 then if transitionAfterscrollScale < 0.025 then
transitionAfterscrollDifficultiesAlpha = math.min(1, transitionAfterscrollScale / 0.025) transitionAfterscrollDifficultiesAlpha = math.min(1, transitionAfterscrollScale / 0.025)
@ -994,7 +984,7 @@ function tickTransitions(deltaTime)
elseif transitionJacketBgScrollScale >= 0.05 and transitionJacketBgScrollScale < 0.1 then elseif transitionJacketBgScrollScale >= 0.05 and transitionJacketBgScrollScale < 0.1 then
transitionJacketBgScrollAlpha = math.min(1, (transitionJacketBgScrollScale-0.05) / 0.05) transitionJacketBgScrollAlpha = math.min(1, (transitionJacketBgScrollScale-0.05) / 0.05)
elseif transitionJacketBgScrollScale >= 0.8 and transitionJacketBgScrollScale < 1 then 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)) math.min(1, 1-((transitionJacketBgScrollScale-0.8) / 0.05))
) )
else else
@ -1011,25 +1001,17 @@ function tickTransitions(deltaTime)
end end
transitionLaserY = desh - math.min(transitionLaserScale * 2 * desh, desh) transitionLaserY = desh - math.min(transitionLaserScale * 2 * desh, desh)
-- Flash transition -- Flash transition
if transitionFlashScale < 1 then if transitionFlashScale < 1 then
---@type number|string
local songBpm = 120 local songBpm = 120
if (songwheel.songs[selectedIndex] and game.GetSkinSetting('animations_affectWithBPM')) then if (songwheel.songs[selectedIndex] and game.GetSkinSetting('animations_affectWithBPM')) then
songBpm = songwheel.songs[selectedIndex].bpm local songBpmStr = songwheel.songs[selectedIndex].bpm
local songBpmStrs = common.split(songBpmStr, '-')
-- Is a variable BPM local minBpm = tonumber(songBpmStrs[1]) -- Lowest bpm value
if (type(songBpm) == "string") then songBpm = minBpm or songBpm
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
end end
transitionFlashScale = transitionFlashScale + deltaTime / (60/songBpm) -- transition should last for that time in seconds transitionFlashScale = transitionFlashScale + deltaTime / (60/songBpm) -- transition should last for that time in seconds
@ -1069,7 +1051,41 @@ function tickTransitions(deltaTime)
end end
end end
draw_songwheel = function(deltaTime) ---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
local scale = 0.666
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)
--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(x, y)
gfx.Scale(scale, scale)
radar:drawRadialTicks(strokeColor)
radar:drawAttributes()
gfx.Restore()
end
local draw_songwheel = function(deltaTime)
drawBackground(deltaTime) drawBackground(deltaTime)
drawSongList() drawSongList()
@ -1077,6 +1093,9 @@ draw_songwheel = function(deltaTime)
isFilterWheelActive = game.GetSkinSetting('_songWheelOverlayActive') == 1 isFilterWheelActive = game.GetSkinSetting('_songWheelOverlayActive') == 1
drawData() drawData()
drawRadar()
drawCursor() drawCursor()
drawFilterInfo(deltaTime) drawFilterInfo(deltaTime)
@ -1091,13 +1110,14 @@ draw_songwheel = function(deltaTime)
local debugScrollingUp= "FALSE" local debugScrollingUp= "FALSE"
if scrollingUp then debugScrollingUp = "TRUE" end 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) 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 end
gfx.ResetTransform() gfx.ResetTransform()
end end
---@diagnostic disable-next-line:lowercase-global
render = function (deltaTime) render = function (deltaTime)
tickTransitions(deltaTime) tickTransitions(deltaTime)
@ -1105,6 +1125,13 @@ render = function (deltaTime)
Sound.stopMusic() 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() Dim.updateResolution()
Wallpaper.render() Wallpaper.render()
@ -1116,6 +1143,7 @@ render = function (deltaTime)
refreshIrLeaderboard(deltaTime) refreshIrLeaderboard(deltaTime)
end end
---@diagnostic disable-next-line:lowercase-global
songs_changed = function (withAll) songs_changed = function (withAll)
irLeaderboardsCache = {} -- Reset LB cache irLeaderboardsCache = {} -- Reset LB cache
@ -1125,29 +1153,33 @@ songs_changed = function (withAll)
game.SetSkinSetting('_songWheelScrollbarTotal', #songwheel.songs) game.SetSkinSetting('_songWheelScrollbarTotal', #songwheel.songs)
game.SetSkinSetting('_songWheelScrollbarIndex', selectedIndex) game.SetSkinSetting('_songWheelScrollbarIndex', selectedIndex)
local diffs = {} local diffs = {}
for i = 1, #songwheel.allSongs do for i = 1, #songwheel.allSongs do
local song = songwheel.allSongs[i] local song = songwheel.allSongs[i]
for j = 1, #song.difficulties do for j = 1, #song.difficulties do
local diff = song.difficulties[j] local diff = song.difficulties[j]
diff.force = VolforceCalc.calc(diff) table.insert(diffs, {hash = diff.hash, force = VolforceCalc.calc(diff)})
table.insert(diffs, diff) end
end end
end
table.sort(diffs, function (l, r) table.sort(diffs, function (l, r)
return l.force > r.force return l.force > r.force
end) end)
totalForce = 0
for i = 1, 50 do local totalForce = 0
if diffs[i] then for i = 1, 50 do
top50diffs[diffs[i].id] = true local diff = diffs[i]
totalForce = totalForce + diffs[i].force if not diff then
end break
end end
top50diffs[diff.hash] = true
totalForce = totalForce + diff.force
end
game.SetSkinSetting('_volforce', totalForce) game.SetSkinSetting('_volforce', totalForce)
end end
---@diagnostic disable-next-line:lowercase-global
set_index = function(newIndex) set_index = function(newIndex)
transitionScrollScale = 0 transitionScrollScale = 0
transitionAfterscrollScale = 0 transitionAfterscrollScale = 0
@ -1157,23 +1189,30 @@ set_index = function(newIndex)
game.SetSkinSetting('_songWheelScrollbarTotal', #songwheel.songs) game.SetSkinSetting('_songWheelScrollbarTotal', #songwheel.songs)
game.SetSkinSetting('_songWheelScrollbarIndex', newIndex) game.SetSkinSetting('_songWheelScrollbarIndex', newIndex)
scrollingUp = false scrollingUp = false
if ((newIndex > selectedIndex and not (newIndex == #songwheel.songs and selectedIndex == 1)) or (newIndex == 1 and selectedIndex == #songwheel.songs)) then if ((newIndex > selectedIndex and not (newIndex == #songwheel.songs and selectedIndex == 1)) or (newIndex == 1 and selectedIndex == #songwheel.songs)) then
scrollingUp = true scrollingUp = true
end end
game.PlaySample('song_wheel/cursor_change.wav') updateRadar = true
game.PlaySample('song_wheel/cursor_change.wav')
selectedIndex = newIndex selectedIndex = newIndex
end end
local json = require("common.json")
---@diagnostic disable-next-line:lowercase-global
set_diff = function(newDiff) set_diff = function(newDiff)
if newDiff ~= selectedDifficulty then if newDiff ~= selectedDifficulty then
jacketCache = {} -- Clear the jacket cache for the new diff jackets jacketCache = {} -- Clear the jacket cache for the new diff jackets
game.PlaySample('song_wheel/diff_change.wav') game.PlaySample('song_wheel/diff_change.wav')
end end
updateRadar = true
selectedDifficulty = newDiff selectedDifficulty = newDiff
irLeaderboard = {} irLeaderboard = {}
irRequestStatus = 1 irRequestStatus = 1

66
shaders/radar.fs Normal file
View File

@ -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);
}

22
shaders/radar.vs Normal file
View File

@ -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);
}

11
shaders/radarVertex.fs Normal file
View File

@ -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;
}

94
shaders/radarVertex.vs Normal file
View File

@ -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);
}