Compare commits

..

No commits in common. "master" and "fdigl-develop" have entirely different histories.

265 changed files with 12462 additions and 52082 deletions

1
.gitattributes vendored
View File

@ -1 +0,0 @@
* text=auto

18
.gitignore vendored
View File

@ -1,21 +1,7 @@
# IDE files
.vscode
# secret(?) assets
_asset
song-assets
skin-assets
# generated skin files
nautica.json
skin.cfg
# any crew that's not the default one, we do not package crew
textures/crew/*
!textures/crew/appeal_card.png
!textures/crew/frame.png
!textures/crew/portrait.png
!textures/crew/make-a-crew/frame_glow.png
!textures/crew/make-a-crew/frame_metal.png
!textures/crew/make-a-crew/instructions.txt
!textures/crew/anim/nothing
.vscode
/textures/crew/

BIN
CHANGELOG

Binary file not shown.

View File

@ -1,38 +1,37 @@
# ExperimentalGear skin for USC
Project Starter: GSK Bladez
## Coding
- [REDACTED]
- Hersi
- RealFD
- fdigl
- Hoshikara
- GSK Bladez
- Local
- Kuenaimaku
## Graphics
- GSK Bladez
- Neardayo
- YellowBird
- Dengekiko
## Translation
- GSK Bladez
- Neardayo
- RealFD
## Misc. Help
- Neardayo
- DDX
- GM*DEO
## Beta Testing
- Gam
- TealStar
- Dengikiko
- Adamyes
- Gio
- Mattadome
# ExperimentalGear skin for USC
Project Starter: GSK Bladez
## Coding
- [REDACTED]
- Hersi
- RealFD
- fdigl
- Hoshikara
- GSK Bladez
- Local
## Graphics
- GSK Bladez
- Neardayo
- YellowBird
- Dengekiko
## Translation
- GSK Bladez
- Neardayo
- RealFD
## Misc. Help
- Neardayo
- DDX
- GM*DEO
## Beta Testing
- Gam
- TealStar
- Dengikiko
- Adamyes
- Gio
- Mattadome

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,86 +1,86 @@
local newFormatTemplate = {
{
Bg={
Base={
Tex={{"",""}},
Tilt=false,
OffsetY=0.17,
Anim=true,
ScaleSoft=false,
ClampTiling=false,
},
Overlay={ -- is meant to be displayed overtop Base
Tex="",
Float=true,
FloatFactor=2.0,
OffsetY=0.12,
FlashEffect=true,
Tilt=true,
},
Layer={
Tex={{"",""}},
ScaleHard=false,
Brighten=0.6,
},
u={
Pivot=0.36
},
},
Center={
Tex={{"",""}},
u={
Scale=2.8,
Pulse=true,
Float=true,
FloatFactor=0.5,
FloatXFactor=0,
FloatRotationFactor=0,
FadeEffect=true,
Tilt=false,
Anim=true,
OffsetY=-0.1,
Glow=false,
Rotate=false,
},
LayerEffect={
Tex="",
Fade=true,
Rotate=true,
RotateSpeed=-2.0,
DodgeBlend=true,
Glow=true,
Alpha=0.7,
Scale=0.5,
},
},
Tunnel={ -- todo: give option to do sth with blend mode
Tex={{"",""}},
u={
Stretch=0.3,
ScaleX=0.8,
ScaleY=0.9,
Fog=10.0,
FlashEffect=true,
VortexEffect=false,
VortexFactor=1.0,
DodgeBlend=false
}
},
Particle={
Tex={{"",""}},
u={
Speed=0.3,
OffsetY=-0.02,
Amount=3,
Scale=1.0,
ExtraRotation=0.125
},
},
luaParticleEffect={
particles={
{"star-particle.png", 32}
}
},
speed=0.6,
}
local newFormatTemplate = {
{
Bg={
Base={
Tex={{"",""}},
Tilt=false,
OffsetY=0.17,
Anim=true,
ScaleSoft=false,
ClampTiling=false,
},
Overlay={ -- is meant to be displayed overtop Base
Tex="",
Float=true,
FloatFactor=2.0,
OffsetY=0.12,
FlashEffect=true,
Tilt=true,
},
Layer={
Tex={{"",""}},
ScaleHard=false,
Brighten=0.6,
},
u={
Pivot=0.36
},
},
Center={
Tex={{"",""}},
u={
Scale=2.8,
Pulse=true,
Float=true,
FloatFactor=0.5,
FloatXFactor=0,
FloatRotationFactor=0,
FadeEffect=true,
Tilt=false,
Anim=true,
OffsetY=-0.1,
Glow=false,
Rotate=false,
},
LayerEffect={
Tex="",
Fade=true,
Rotate=true,
RotateSpeed=-2.0,
DodgeBlend=true,
Glow=true,
Alpha=0.7,
Scale=0.5,
},
},
Tunnel={ -- todo: give option to do sth with blend mode
Tex={{"",""}},
u={
Stretch=0.3,
ScaleX=0.8,
ScaleY=0.9,
Fog=10.0,
FlashEffect=true,
VortexEffect=false,
VortexFactor=1.0,
DodgeBlend=false
}
},
Particle={
Tex={{"",""}},
u={
Speed=0.3,
OffsetY=-0.02,
Amount=3,
Scale=1.0,
ExtraRotation=0.125
},
},
luaParticleEffect={
particles={
{"star-particle.png", 32}
}
},
speed=0.6,
}
}

View File

@ -1,124 +1,68 @@
{
"User information": {"type": "label"},
"username": {
"type": "text",
"label": "Username (max 8 characters)",
"default": "GUEST"
},
"separator_a": {},
"MSG": {
"type": "text",
"label": "Message (max 8 characters)",
"default": "Hellooooooo"
},
"separator_b": {},
"Animations": {"type": "label"},
"animations_affectWithBPM": {
"type": "bool",
"label": "Affect speed of some animations with the current song's BPM",
"default": false
},
"animations_skipIntro": {
"type": "bool",
"label": "Skip intro splash screens",
"default": false
},
"separator_c": {},
"Crew": { "type": "label" },
"single_idol": {
"label": "Crew idol animations, folder name in `crew/anim/`",
"type": "text",
"default": "nothing"
},
"words": {
"type": "selection",
"label": "Language",
"default": "EN",
"values": ["EN", "DE", "SK", "test2"]
},
"separator_d": {},
"Audio": { "type": "label" },
"audio_systemVoice": {
"label": "Turn on Rasis",
"type": "bool",
"default": false
},
"separator_e": {},
"Gameplay": { "type": "label" },
"gameplay_ucDifferentColor": {
"label": "Use different colors for UC and PUC chain numbers",
"type": "bool",
"default": false
},
"gameplay_showSearchControls": {
"label": "Show song select controls when searching",
"type": "bool",
"default": true
},
"gameplay_earlyLateFor": {
"label": "Show Early/Late display for",
"type": "selection",
"default": "NEAR (or worse)",
"values": ["CRITICAL (or worse)", "NEAR (or worse)", "OFF"]
},
"gameplay_earlyLatePosition": {
"label": "Early/Late display position",
"type": "selection",
"default": "STANDARD",
"values": ["UPPER+", "UPPER", "STANDARD", "LOWER"]
},
"gameplay_msFor": {
"label": "Show millisecond display for",
"type": "selection",
"default": "NEAR (or worse)",
"values": ["ALL", "CRITICAL (or worse)", "NEAR (or worse)", "NONE"]
},
"separator_f": {},
"Debug": { "type": "label" },
"debug_showInformation": {
"label": "Show debug information (sometimes in the middle of the screen when you're playing)",
"type": "bool",
"default": false
},
"separator_g": {},
"Experimental features": { "type": "label" },
"songselect_showEffectRadar": {
"label": "Show Effect Radar for compatible songs (VERY WIP)",
"type": "bool",
"default": false
},
"songselect_enableTimer": {
"label": "Display a countdown timer until a demo is played, when available (Buggy)",
"type": "bool",
"default": false
},
"songselect_freezeTimer": {
"label": "Freeze timer on value (in seconds, -1 to disable)",
"type": "int",
"default": -1,
"min": -1,
"max": 5999
}
}
{
"User information": {"type": "label"},
"username": {
"type": "text",
"label": "Username (max 8 characters)",
"default": "GUEST"
},
"separator_a": {},
"MSG": {
"type": "text",
"label": "Message (max 8 characters)",
"default": "Hellooooooo"
},
"separator_b": {},
"Animations": {"type": "label"},
"animations_affectWithBPM": {
"type": "bool",
"label": "Affect speed of some animations with the current song's BPM",
"default": false
},
"separator_c": {},
"Crew": { "type": "label" },
"single_idol": {
"label": "!!!ALWAYS MATCH THE NAME!!!",
"type": "text",
"default": "nothing"
},
"words": {
"type": "selection",
"label": "Language",
"default": "EN",
"values": ["EN", "DE", "SK", "HU", "test2"]
},
"separator_d": {},
"Audio": { "type": "label" },
"audio_systemVoice": {
"label": "Turn on Rasis",
"type": "bool",
"default": false
},
"separator_e": {},
"Gameplay": { "type": "label" },
"gameplay_ucDifferentColor": {
"label": "Use different colors for UC and PUC chain numbers",
"type": "bool",
"default": false
},
"separator_f": {},
"Debug": { "type": "label" },
"debug_showInformation": {
"label": "Show debug information (sometimes in the middle of the screen when you're playing)",
"type": "bool",
"default": false
}
}

View File

@ -1,75 +0,0 @@
-- IR State enum
---@class States
local States = {
Unused = 0,
Pending = 10,
Success = 20,
Accepted = 22,
BadRequest = 40,
Unauthorized = 41,
ChartRefused = 42,
Forbidden = 43,
NotFound = 44,
ServerError = 50,
RequestFailure = 60
}
---@class IRData
---@field Active boolean # USC IR configured and active
---@field States States # IR reposonse state enum
IRData = {}
---@class IRHeartbeatResponseBody
---@field serverTime integer
---@field serverName string
---@field irVersion string
---@class IRRecordResponseBody
---@field record ServerScore
---@alias IRLeaderboardResponseBody ServerScore[]
---@class IRResponse
---@field statusCode integer
---@field description string
---@class IRHeartbeatResponse : IRResponse
---@field body IRHeartbeatResponseBody
---@class IRChartTrackedResponse : IRResponse
---@field body {}
---@class IRRecordResponse : IRResponse
---@field body IRRecordResponseBody
---@class IRLeaderboardResponse : IRResponse
---@field body ServerScore[]
-- Performs a Heartbeat request.
---@param callback fun(res: IRHeartbeatResponse) # Callback function receives IRResponse as it's first parameter
local function Heartbeat(callback) end
-- Performs a Chart Tracked request for the chart with the provided hash.
---@param hash string # song hash
---@param callback fun(res: IRChartTrackedResponse) # Callback function receives IRResponse as it's first parameter
local function ChartTracked(hash, callback) end
-- Performs a Record request for the chart with the provided hash.
---@param hash string # song hash
---@param callback fun(res: IRRecordResponse) # Callback function receives IRResponse as it's first parameter
local function Record(hash, callback) end
-- Performs a Leaderboard request for the chart with the provided hash, with parameters mode and n.
---@param hash string # song hash
---@param mode "best"|"rivals" # request leaderboard mode
---@param n integer # limit the number of requested scores
---@param callback fun(res: IRLeaderboardResponse) # Callback function receives IRResponse as it's first parameter
local function Leaderboard(hash, mode, n, callback) end
---@class IR
IR = {
Heartbeat = Heartbeat,
ChartTracked = ChartTracked,
Record = Record,
Leaderboard = Leaderboard
}

View File

@ -1,121 +0,0 @@
-- challengeresult `result` table
---@diagnostic disable:lowercase-global
---@diagnostic disable:missing-return
---@class ChallengeHitStat
---@field timeFrac number -- Fraction of when in the chart the note was hit, `0.0` to `1.0`
---@field lane integer -- `0` = A, `1` = B, `2` = C, `3` = D, `4` = L, `5` = R, `6` = Left Laser, `7` = Right Laser
---@field time integer -- When in the chart the note was hit, in milliseconds
---@field delta integer -- Delta value of the hit from 0
---@field rating integer -- `0 = Miss`, `1 = Near`, `2 = Crit`
---@class ChallengeHitWindow
---@field good integer # Near window, default `92`
---@field hold integer -- Hold window, default `138`
---@field miss integer -- Miss window, default `250`
---@field perfect integer -- Critical window, default `46`
---@field slam integer -- Slam window, default `84`
---@field type integer -- `1 = Normal` default, `2 = Hard` default values halved
---@class ChartResultScore
---@field auto_flags integer # Autoplay flag
---@field badge integer # `0` = Manual Exit, `1` = Played, `2` = Cleared, `3` = Hard Cleared, `4` = Full Chain, `5` = Perfect Chain
---@field combo integer # Best combo reached
---@field earlies integer # Total early hits
---@field gauge number # Ending gauge percentage, `0.0` to `1.0`
---@field gauge_option integer # Gauge option e.g. ARS
---@field gauge_type integer # `0` = Normal, `1` = Hard, `2` = Permissive, `3` = Blastive
---@field goods integer # Total near hits
---@field hitWindow ChallengeHitWindow # Hit windows of the score
---@field lates integer # Total late hits
---@field mirror integer # Mirror mode flag
---@field misses integer # Total errors
---@field perfects integer # Total critical hits
---@field random integer # Random mode flag
---@field score integer # Result score
---@field timestamp integer # Unix timestamp of the score
---@class ChartResult
---@field artist string # Chart artist
---@field autoplay boolean # Autoplay bool, always false
---@field auto_flags integer # Autoplay flag
---@field badge integer # `0` = Manual Exit, `1` = Played, `2` = Cleared, `3` = Hard Cleared, `4` = Full Chain, `5` = Perfect Chain
---@field bpm number # Chart BPM
---@field difficulty integer # Difficulty index
---@field duration integer # Chart duration, in milliseconds
---@field earlies integer # Total early hits
---@field effector string # Chart effector
---@field failReason string # Reason for failing the chart
---@field gauge number # Ending gauge percentage, `0.0` to `1.0`
---@field gaugeSamples number[] # Gauge values sampled (256 total) throughout the play
---@field gauge_option integer # Gauge option e.g. ARS
---@field gauge_type integer # `0` = Normal, `1` = Hard, `2` = Permissive, `3` = Blastive
---@field goods integer # Total near hits
---@field grade string # Result grade
---@field highScores ChartResultScore[] # All scores
---@field hitWindow ChallengeHitWindow # Result hit windows
---@field illustrator string # Chart jacket illustrator
---@field isSelf boolean # Always true
---@field jacketPath string # Full filepath to the jacket image on the disk
---@field lates integer # Total late hits
---@field level integer # Chart level
---@field maxCombo integer # Result max chain
---@field meanHitDelta number # Mean hit delta
---@field meanHitDeltaAbs number # Absolute value of mean hit delta
---@field medianHitDelta integer # Median hit delta
---@field medianHitDeltaAbs integer # Absolute value of median hit delta
---@field mirror boolean # Mirror mode bool
---@field misses integer # Total errors
---@field mission string # Always empty string
---@field noteHitStats ChallengeHitStat[] # Hit stats for every chip hit
---@field passed boolean # Chart passed
---@field percent integer # Chart challenge percent
---@field perfects integer # Total critical hits
---@field playbackSpeed number # Always 1.0
---@field random boolean # Random mode bool,
---@field realTitle string # Chart title, always without player name
---@field retryCount integer # Always 0
---@field score integer # Result score
---@field speedModType integer # `0` = XMOD, `1` = MMOD, `2` = CMOD
---@field speedModValue number # `HiSpeed` for `XMOD`, `ModSpeed` otherwise
---@field title string # Chart title
---@class ChallengeResult
---@field avgCrits integer # Average number of critical hits across the charts
---@field avgErrors integer # Average number of error hits across the charts
---@field avgGauge number # Average gauge percentage across the charts
---@field avgNears integer # Average number of near hits of the charts
---@field avgPercentage integer # Average completion percentage across the charts
---@field avgScore integer # Average score across the charts
---@field badge integer # `0` = Manual Exit, `1` = Played, `2` = Cleared, `3` = Hard Cleared, `4` = Full Chain, `5` = Perfect Chain
---@field charts ChartResult[] # array of result information for all played charts (note: might not be all charts in course)
---@field failReason string # Reason for failing the challenge
---@field grade string # Result grade
---@field isSelf boolean # Always true
---@field level integer # Chart or challenge level
---@field overallCrits integer # Total number of critical hits across the charts
---@field overallErrors integer # Total number of error hits across the charts
---@field overallNears integer # Total number of near hits across the charts
---@field passed boolean # Whether or not the challenge was passed
---@field requirement_text string # The challenge requirements separated by newline character `"\n"`
---@field title string # Challenge title
---Render, called every frame
---@param deltaTime number # time in seconds between frames
---@param showStats boolean # true when left FX is pressed
render = function (deltaTime, showStats) end
---This is called right after result is set, either for initial display or when the player whose score is being displayed is changed.
result_set = function () end
---The region of the screen to be saved in score screenshots.
---@return number x # top left X coordinate
---@return number y # top left Y coordinate
---@return number w # width
---@return number h # height
get_capture_rect = function () end
---Called when a screenshot has been captured successfully.
---@param path string # path to the saved screenshot
screenshot_captured = function (path) end

View File

@ -7,7 +7,7 @@ GetButton = function(button) end
-- Gets the absolute rotation of the specified knob
---@param knob integer # `0 = left`, `1 = right`
---@return number angle # in radians, `-2*pi` to `0` (turning CCW) and `0` to `2*pi` (turning CW)
---@return number angle # in radians, `0.0` to `2*pi`
GetKnob = function(knob) end
-- Gets the color of the specified laser

View File

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

View File

@ -1,7 +1,4 @@
-- result `result` table
---@diagnostic disable:lowercase-global
---@diagnostic disable:missing-return
-- result and challengeresult `result` table
---@class HitStat
---@field timeFrac number -- Fraction of when in the chart the note was hit, `0.0` to `1.0`
@ -10,6 +7,7 @@
---@field delta integer -- Delta value of the hit from 0
---@field hold integer -- `0` for chip/laser, otherwise `# Ticks` of hold
---@field rating integer -- `0 = Miss`, `1 = Near`, `2 = Crit`
HitStat = {};
---@class HitWindow
---@field good integer # Near window, default `92`
@ -18,6 +16,7 @@
---@field perfect integer -- Critical window, default `46`
---@field slam integer -- Slam window, default `84`
---@field type integer -- `1 = Normal` default, `2 = Hard` default values halved
HitWindow = {};
---@class Score
---@field auto_flags integer # Autoplay flag
@ -26,60 +25,42 @@
---@field gauge_option integer # Gauge option e.g. ARS
---@field gauge_type integer # `0` = Normal, `1` = Hard, `2` = Permissive, `3` = Blastive
---@field goods integer # Total near hits
---@field hitWindow HitWindow # Hit windows of the score
---@field hitWindow HitWindow|nil # Hit windows of the score, only for singleplayer results screen
---@field mirror integer # Mirror mode flag
---@field misses integer # Total errors
---@field name nil|string # Only for multiplayer results, name of the player
---@field perfects integer # Total critical hits
---@field random integer # Random mode flag
---@field score integer # Result score
---@field timestamp integer # Unix timestamp of the score
---@field uid nil|string # Only for multiplayer results, UID of the player
Score = {};
---@class MultiplayerScore
---@field badge integer # `0` = Manual Exit, `1` = Played, `2` = Cleared, `3` = Hard Cleared, `4` = Full Chain, `5` = Perfect Chain
---@field flags integer # Autoplay flag
---@field gauge number # Ending gauge percentage, `0.0` to `1.0`
---@field goods integer # Total near hits
---@field misses integer # Total errors
---@field name string # Name of the player
---@field perfects integer # Total critical hits
---@field score integer # Result score
---@field timestamp integer # Unix timestamp of the score
---@field uid string # UID of the player
---@class ChartResult : result
---@field passed boolean # Whether or not challenge requirements were met for this chart
---@field failReason string # Fail reason if a challenge requirement was not met
ChartResult = {};
---@class ServerScoreOptions
---@field gaugeType integer # An enum value representing the gauge type used. 0 = normal, 1 = hard. Further values are not currently specified.
---@field gaugeOpt integer # Reserved
---@field mirror boolean # Mirror mode enabled
---@field random boolean # Note shuffle enabled
---@field autoFlags integer # A bitfield of elements of the game that are automated. Any non-zero value means that the score was at least partially auto.
---@class ServerScore
---@field score integer # Submitted score
---@field gauge number # Submitted Gauge result
---@field timestamp integer # Unix timestamp of the score
---@field crit integer # Hits inside the critical window
---@field near integer # Hits inside the near window
---@field early integer # Hits inside the near window which were early
---@field late integer # Hits inside the near window which were late
---@field combo integer # Best combo reached
---@field error integer # Missed notes
---@field options ServerScoreOptions # The options in use. Includes gauge type, etc.
---@field windows table # {perfect, good, hold, miss, slam} hit windows in milliseconds
---@field yours boolean # This score belongs to the current player
---@field justSet boolean # This score belongs to the current player, and is the score that was just achieved
---@class Result
---@class result
---@field artist string # Chart artist
---@field auto_flags integer # Autoplay flag
---@field autoplay boolean # Autoplay bool
---@field avgCrits integer # Only for challenge results, average number of critical hits across the charts
---@field avgErrors integer # Only for challenge results, average number of error hits across the charts
---@field avgGauge number # Only for challenge results, average gauge percentage across the charts
---@field avgNears integer # Only for challenge results, average number of near hits of the charts
---@field avgPercentage integer # Only for challenge results, average completion percentage across the charts
---@field avgScore integer # Only for challenge results, average score across the charts
---@field badge integer # `0` = Manual Exit, `1` = Played, `2` = Cleared, `3` = Hard Cleared, `4` = Full Chain, `5` = Perfect Chain
---@field bpm number # Chart BPM
---@field charts ChartResult[] # Only for challenge results, array of chart results
---@field chartHash string # Chart hash
---@field difficulty integer # Difficulty index
---@field displayIndex nil|integer # Only for multiplayer results, the index of the score being viewed
---@field duration integer # Chart duration, in milliseconds
---@field earlies integer # Total early hits
---@field effector string # Chart effector
---@field failReason string # Reason for failing the challenge
---@field flags integer # Gameplay option flags e.g. gauge type, mirror/random mode
---@field gauge number # Ending gauge percentage, `0.0` to `1.0`
---@field gauge_option integer # Gauge option e.g. ARS
@ -87,18 +68,16 @@
---@field gaugeSamples number[] # Gauge values sampled (256 total) throughout the play
---@field goods integer # Total near hits
---@field grade string # Result grade
---@field highScores (Score|MultiplayerScore)[] # All scores
---@field highScores Score[] # All scores
---@field hitWindow HitWindow # Result hit windows
---@field holdHitStats HitStat[]|nil # Hit stats for every hold object, only available for singleplayer if `isSelf = true`
---@field illustrator string # Chart jacket illustrator
---@field irState integer # Current state of the IR score submission request (a USC-IR code, including extensions 0/10/60)
---@field irDescription string # The description in the IR response (nil if irState is 0 or 10)
---@field irScores ServerScore[]|nil # Score submission result, nil if irState != 20
---@field irState integer # Internet ranking flag
---@field isSelf boolean # Only for multiplayer, `false` if score is of another player
---@field jacketPath string # Full filepath to the jacket image on the disk
---@field laserHitStats HitStat[]|nil # Hit stats for every laser object, only available for singleplayer if `isSelf = true`
---@field lates integer # Total late hits
---@field level integer # Chart level
---@field level integer # Chart or challenge level
---@field maxCombo integer # Result max chain
---@field meanHitDelta number # Mean hit delta
---@field meanHitDeltaAbs number # Absolute value of mean hit delta
@ -108,34 +87,20 @@
---@field misses integer # Total errors
---@field mission string # Only for practice mode
---@field noteHitStats HitStat[]|nil # Hit stats for every chip hit, only available for singleplayer if `isSelf = true`
---@field overallCrits integer # Only for challenge results, total number of critical hits across the charts
---@field overallErrors integer # Only for challenge results, total number of error hits across the charts
---@field overallNears integer # Only for challenge results, total number of near hits across the charts
---@field passed boolean # Only for challenge results, whether or not the challenge was passed
---@field perfects integer # Total critical hits
---@field playbackSpeed number # Only for practice mode, percentage from 0.25 to 1.0
---@field playerName nil|string # Only for multiplayer
---@field random boolean # Random mode bool,
---@field realTitle string # Chart title, always without player name
---@field requirement_text string # Only for challenge results, the challenge requirements separated by newline character `"\n"`
---@field retryCount integer # Only for practice mode
---@field score integer # Result score
---@field speedModType integer # Only for singleplayer, `0` = XMOD, `1` = MMOD, `2` = CMOD
---@field speedModValue number # Only for singleplayer, `HiSpeed` for `XMOD`, `ModSpeed` otherwise
---@field title string # Chart (with player name in multiplayer)
---@field uid nil|string # Only for multiplayer, UID of the __viewer__
result = {}
---Render, called every frame
---@param deltaTime number # time in seconds between frames
---@param showStats boolean # true when left FX is pressed
render = function (deltaTime, showStats) end
---This is called right after result is set, either for initial display or when the player whose score is being displayed is changed.
result_set = function () end
---The region of the screen to be saved in score screenshots.
---@return number x # top left X coordinate
---@return number y # top left Y coordinate
---@return number w # width
---@return number h # height
get_capture_rect = function () end
---Called when a screenshot has been captured successfully.
---@param path string # path to the saved screenshot
screenshot_captured = function (path) end
---@field title string # Chart (with player name in multiplayer) or challenge title
---@field uid nil|string # Only for multiplayer, UID of the player
result = {};

View File

@ -1,4 +1,117 @@
---@diagnostic disable:missing-return
-- Adds a texture that was loaded with `gfx.LoadSharedTexture` to the material that can be used in the shader code
---@param uniformName string
---@param textureName string
AddSharedTexture = function(uniformName, textureName) end
-- Adds a texture to the material that can be used in the shader code
---@param uniformName string
---@param path string # prepended with `skins/<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
ShadedMesh = {
@ -12,148 +125,57 @@ ShadedMesh = {
PRIM_LINELIST = 3,
PRIM_LINESTRIP = 4,
PRIM_POINTLIST = 5,
AddSharedTexture = AddSharedTexture,
AddSkinTexture = AddSkinTexture,
AddTexture = AddTexture,
Draw = Draw,
GetPosition = GetPosition,
GetRotation = GetRotation,
GetScale = GetScale,
SetBlendMode = SetBlendMode,
SetData = SetData,
SetOpaque = SetOpaque,
SetParam = SetParam,
SetParamVec2 = SetParamVec2,
SetParamVec3 = SetParamVec3,
SetParamVec4 = SetParamVec4,
SetPosition = SetPosition,
SetPrimitiveType = SetPrimitiveType,
SetRotation = SetRotation,
SetScale = SetScale,
SetWireframe = SetWireframe,
};
-- Adds a texture that was loaded with `gfx.LoadSharedTexture` to the material that can be used in the shader code
---@param uniformName string
---@param textureName string
function ShadedMesh:AddSharedTexture(uniformName, textureName) end
-- Gets the length of the mesh
---@return number length
GetLength = function() 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/`
function ShadedMesh:AddSkinTexture(uniformName, path) end
-- Sets the y-scale of the mesh based on its length
-- Useful for creating fake buttons which may have variable length based on duration
---@param length number
ScaleToLength = function(length) end
-- Adds a texture to the material that can be used in the shader code
---@param uniformName string
---@param path string
function ShadedMesh:AddTexture(uniformName, path) end
-- Stops meshes beyond the track from being rendered if `doClip`
---@param doClip boolean
SetClipWithTrack = function(doClip) end
-- Gets the translation of the mesh
---@return number x, number y, number z
function ShadedMesh:GetPosition() end
-- Gets the rotation (in degrees) of the mesh
---@return number roll, number yaw, number pitch
function ShadedMesh:GetRotation() end
-- Gets the scale of the mesh
---@return number x, number y, number z
function ShadedMesh:GetScale() end
-- Sets the blending mode
---@param mode integer # options also available as fields of the object prefixed with `BLEND`
-- `Normal` = 0 (default)
-- `Additive` = 1
-- `Multiply` = 2
function ShadedMesh:SetBlendMode(mode) end
-- Sets the geometry data
---@param data table # array of vertices in clockwise order starting from the top left e.g.
-- ```
-- {
-- { { 0, 0 }, { 0, 0 } },
-- { { 50, 0 }, { 1, 0 } },
-- { { 50, 50 }, { 1, 1 } },
-- { { 0, 50 }, { 0, 1 } },
-- }
-- ```
function ShadedMesh:SetData(data) end
-- Sets the material is opaque or non-opaque (default)
---@param opaque boolean
function ShadedMesh:SetOpaque(opaque) end
-- Sets the value of the specified uniform
---@param uniformName string
---@param value number # `float`
function ShadedMesh:SetParam(uniformName, value) end
-- Sets the value of the specified 2d vector uniform
---@param uniformName string
---@param x number # `float`
---@param y number # `float`
function ShadedMesh:SetParamVec2(uniformName, x, y) end
-- Sets the value of the specified 3d vector uniform
---@param uniformName string
---@param x number # `float`
---@param y number # `float`
---@param z number # `float`
function ShadedMesh:SetParamVec3(uniformName, x, y, z) end
-- Sets the value of the specified 4d vector uniform
---@param uniformName string
---@param x number # `float`
---@param y number # `float`
---@param z number # `float`
---@param w number # `float`
function ShadedMesh:SetParamVec4(uniformName, x, y, z, w) end
-- Sets the translation for the mesh
-- Relative to the screen for `ShadedMesh`
-- Relative to the center of the crit line for `ShadedMeshOnTrack`
---@param x number
---@param y number
---@param z? number # Default `0`
function ShadedMesh:SetPosition(x, y, z) end
-- Sets the format for geometry data provided by `SetData`
---@param type integer # options also available as fields of the object prefixed with `PRIM`
-- `TriangleList` = 0 (default)
-- `TriangleStrip` = 1
-- `TriangleFan` = 2
-- `LineList` = 3
-- `LineStrip` = 4
-- `PointList` = 5
function ShadedMesh:SetPrimitiveType(type) end
-- Sets the rotation (in degrees) of the mesh
-- **WARNING:** For `ShadedMesh`, pitch and yaw may clip, rendering portions or the entire mesh invisible
---@param roll number
---@param yaw? number # Default `0`
---@param pitch? number # Default `0`
function ShadedMesh:SetRotation(roll, yaw, pitch) end
-- Sets the scale of the mesh
---@param x number
---@param y number
---@param z? number # Default `0`
function ShadedMesh:SetScale(x, y, z) end
-- Sets the wireframe mode of the object (does not render texture)
-- Useful for debugging models or geometry shaders
---@param useWireframe boolean
function ShadedMesh:SetWireframe(useWireframe) end
-- Renders the `ShadedMesh` object
function ShadedMesh:Draw() end
-- Sets the length (in the y-direction relative to the track) of the mesh
---@param length number # Optional constants: `BUTTON_TEXTURE_LENGTH`, `FXBUTTON_TEXTURE_LENGTH`, and `TRACK_LENGTH`
SetLength = function(length) end
-- Uses an existing game mesh
---@param meshName string # Options: `'button'`, `'fxbutton'`, and `'track'`
UseGameMesh = function(meshName) end
---@class ShadedMeshOnTrack : ShadedMesh
---@field BUTTON_TEXTURE_LENGTH number
---@field FXBUTTON_TEXTURE_LENGTH number
---@field TRACK_LENGTH number
ShadedMeshOnTrack = {
};
-- Gets the length of the mesh
---@return number length
function ShadedMeshOnTrack:GetLength() end
-- Sets the y-scale of the mesh based on its length
-- Useful for creating fake buttons which may have variable length based on duration
---@param length number
function ShadedMeshOnTrack:ScaleToLength(length) end
-- Stops meshes beyond the track from being rendered if `doClip`
---@param doClip boolean
function ShadedMeshOnTrack:SetClipWithTrack(doClip) end
-- Sets the length (in the y-direction relative to the track) of the mesh
---@param length number # Optional constants: `BUTTON_TEXTURE_LENGTH`, `FXBUTTON_TEXTURE_LENGTH`, and `TRACK_LENGTH`
function ShadedMeshOnTrack:SetLength(length) end
-- Uses an existing game mesh
---@param meshName string # Options: `'button'`, `'fxbutton'`, and `'track'`
function ShadedMeshOnTrack:UseGameMesh(meshName) end
GetLength = GetLength,
UseGameMesh = UseGameMesh,
ScaleToLength = ScaleToLength,
SetClipWithTrack = SetClipWithTrack,
SetLength = SetLength,
};

View File

@ -1,63 +1,29 @@
---@diagnostic disable: lowercase-global
-- songwheel `songwheel` table
---@class SongWheelScore
---@field auto_flags integer # Autoplay flag
---@field badge integer # `0` = Manual Exit, `1` = Played, `2` = Cleared, `3` = Hard Cleared, `4` = Full Chain, `5` = Perfect Chain
---@field combo integer # Max combo
---@field earlies integer # Total early hits
---@field gauge number # Ending gauge percentage, `0.0` to `1.0`
---@field gauge_option integer # Gauge option e.g. ARS
---@field gauge_type integer # `0` = Normal, `1` = Hard, `2` = Permissive, `3` = Blastive
---@field goods integer # Total near hits
---@field isLocal integer # `0` = false, `1` = true
---@field lates integer # Total late hits
---@field mirror integer # Mirror mode flag
---@field misses integer # Total errors
---@field playerName string # Name of the player
---@field perfects integer # Total critical hits
---@field random integer # Random mode flag
---@field score integer # Result score
---@field timestamp integer # Unix timestamp of the score
SongWheelScore = {}
---@class SongWheelDifficulty
---@class Difficulty
---@field difficulty integer # Difficulty index
---@field effector string # Name of charter
---@field hash string # Difficulty hash
---@field id integer # Difficulty id, unique static identifier
---@field illustrator string # Difficulty jacket illustrator
---@field jacketPath string # Full filepath to the jacket image on the disk
---@field level integer # Difficulty level
---@field scores SongWheelScore[] # Scores for the current difficulty
---@field scores Score[] # Scores for the current difficulty
---@field topBadge integer # `0 = Never Played`, `1 = Played`, `2 = Cleared`, `3 = Hard Cleared`, `4 = Full Chain`, `5 = Perfect Chain`
SongWheelDifficulty = {}
Difficulty = {};
---@class SongWheelSong
---@class Song
---@field artist string # Chart artist
---@field difficulties SongWheelDifficulty[] # Array of difficulties for the current song
---@field bpm string # Chart BPM
---@field difficulties Difficulty[] # Array of difficulties for the current song
---@field bpm number # Chart BPM
---@field id integer # Song id, unique static identifier
---@field path string # Full filepath to the chart folder on the disk
---@field title string # Chart title
SongWheelSong = {}
Song = {};
---@class songwheel
---@field allSongs SongWheelSong[] # Array of all available songs
---@field allSongs Song[] # Array of all available songs
---@field searchInputActive boolean # Search status
---@field searchStatus string # Current song database status
---@field searchText string # Search input text
---@field songs SongWheelSong[] # Array of songs with the current filters/sorting applied
songwheel = {}
---Render, called every frame
---@param deltaTime number # time in seconds between frames
render = function (deltaTime) end
---Called when selected difficulty changes
---@param diff integer # Difficulty level
set_diff = function (diff) end
---Called when song database changes
---@param withAll boolean # Reload all songs
songs_changed = function (withAll) end
---@field songs Song[] # Array of songs with the current filters/sorting applied
songwheel = {};

View File

@ -1,35 +0,0 @@
local function Exit() end
local function Settings() end
local function Start() end
local function DLScreen() end
local function Update() end
local function Multiplayer() end
local function Challenges() end
Menu = {
Exit = Exit,
Settings = Settings,
Start = Start,
DLScreen = DLScreen,
Update = Update,
Multiplayer = Multiplayer,
Challenges = Challenges
}
--- Render frame for titlescreen
---@param deltaTime number Elapsed frametime since last frame
function render(deltaTime) end
--- Button event handler for titlescreen
---@param buttonCode integer Corresponds to game.Button_*
function button_pressed(buttonCode) end
--- Mouse event handler for titlescreen
---@param button integer
function mouse_pressed(button) end

View File

@ -1,136 +0,0 @@
{
"realTitle": "Brain Power",
"duration": 106678,
"medianHitDelta": 0,
"goods": 0,
"medianHitDeltaAbs": 0,
"bpm": "170-173",
"autoplay": false,
"misses": 1538,
"earlies": 0,
"score": 405489,
"artist": "ノマ",
"jacketPath": "<hidden>\\USC\\songs\\SDVX II Infinite Infection\\brain_power_noma\\nov_jacket.png",
"irScores": [
{
"lamp": 3,
"near": 30,
"crit": 1563,
"ranking": 1,
"timestamp": 1639691098,
"username": "Hersi",
"justSet": true,
"yours": true,
"error": 10,
"score": 9844042
},
{
"error": 16,
"near": 32,
"crit": 1555,
"ranking": 2,
"timestamp": 1590326862,
"score": 9800374,
"username": "joksulainen",
"lamp": 2
},
{
"error": 33,
"near": 10,
"crit": 1560,
"ranking": 3,
"timestamp": 1644462117,
"score": 9762944,
"username": "Aidrestan",
"lamp": 3
},
{
"error": 19,
"near": 48,
"crit": 1536,
"ranking": 4,
"timestamp": 1637049756,
"score": 9731752,
"username": "Kag",
"lamp": 2
}
],
"hitWindow": {
"good": 150,
"type": 1,
"perfect": 46,
"hold": 150,
"slam": 84,
"miss": 300
},
"random": false,
"auto_flags": 0,
"playerName": "test",
"chartHash": "0d33f1f26df67cac253a5a44bc018e5eff27af0c",
"displayIndex": 1,
"lates": 0,
"highScores": [
{
"misses": 1414,
"timestamp": 0,
"score": 1179039,
"perfects": 189,
"uid": "<hidden>",
"gauge": 0,
"name": "Hersi",
"badge": 1,
"flags": 0,
"goods": 0
},
{
"misses": 1538,
"timestamp": 0,
"score": 405489,
"perfects": 65,
"uid": "80084945-3570-49cd-b2b1-dc23e96fcaf4",
"gauge": 0,
"name": "test",
"badge": 1,
"flags": 0,
"goods": 0
}
],
"mission": "",
"gaugeSamples": [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0.00068503420334309, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0.00068503420334309, 0, 0, 0, 0, 0, 0,
0.00068503420334309, 0, 0.0013700684066862, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.00068503420334309, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0.00068503420334309, 0.00068503420334309, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.00068503420334309, 0, 0, 0, 0,
0.00068503420334309, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0.0013700684066862, 0, 0, 0.00068503420334309, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0.00068503420334309, 0, 0, 0, 0, 0, 0, 0, 0, 0
],
"difficulty": 2,
"gauge_option": 0,
"grade": "D",
"irDescription": "Successfully imported score.",
"retryCount": 0,
"playbackSpeed": 1,
"irState": 20,
"illustrator": "square_head",
"meanHitDeltaAbs": 0,
"isSelf": false,
"badge": 1,
"effector": "Megacycle",
"level": 15,
"uid": "<hidden>",
"maxCombo": 4,
"mirror": false,
"perfects": 65,
"gauge_type": 0,
"meanHitDelta": 0,
"gauge": 0,
"title": "<test> Brain Power"
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

BIN
fonts/commext.ttf Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,202 +0,0 @@
require "common.class"
require "api.graphics"
local Image = require "api.image"
---@class AnimationParams
---@field fps number?
---@field loop boolean?
---@field loopPoint integer?
---@field width number?
---@field height number?
---@field x number?
---@field y number?
---@field scaleX number?
---@field scaleY number?
---@field centered boolean?
---@field blendOp integer?
---@field color number[]?
---@field alpha number?
---@field stroke StrokeParams?
---@class Animation
---@field frames Image[]
---@field frameCount integer
---@field frameTime number
---@field loop boolean
---@field loopPoint integer
---@field width number?
---@field height number?
---@field x number?
---@field y number?
---@field scaleX number?
---@field scaleY number?
---@field centered boolean?
---@field blendOp integer?
---@field color number[]?
---@field alpha number?
---@field stroke StrokeParams?
local Animation = { };
---@class AnimationState
---@field animation Animation # The animation data this state is playing through
---@field frameIndex integer # Current frame in the animation
---@field timer number # Timer used to determine when to change to the next frame
---@field running boolean # Is the animation currently running and accepting updates?
---@field callback function? # Called when the animation completes
local AnimationState = { };
local function loadSequentialAnimationFrames(animPath)
local frames = { };
local count = 0;
local detectedFormat = nil;
while (true) do
local frame = nil;
if (detectedFormat) then
frame = Image.new(detectedFormat:format(animPath, count + 1), true);
else
for i = 1, 4 do
local format = '%s/%0' .. i .. 'd.png';
frame = Image.new(format:format(animPath, count + 1), true);
if (frame) then
detectedFormat = format;
break;
end
end
end
if (not frame) then
break;
end
count = count + 1;
frames[count] = frame;
end
return frames, count;
end
---Animation constructor
---@param animPath string
---@param params AnimationParams
---@return Animation
function Animation.new(animPath, params)
local frames, frameCount = loadSequentialAnimationFrames(animPath);
local instance = {
frames = frames,
frameCount = frameCount,
frameTime = 1 / (params.fps or 30),
loop = params.loop or false,
loopPoint = params.loopPoint or 1,
};
if (params.width ~= nil) then instance.width = params.width; end
if (params.height ~= nil) then instance.height = params.height; end
if (params.x ~= nil) then instance.x = params.x; end
if (params.y ~= nil) then instance.y = params.y; end
if (params.scaleX ~= nil) then instance.scaleX = params.scaleX; end
if (params.scaleY ~= nil) then instance.scaleY = params.scaleY; end
if (params.centered ~= nil) then instance.centered = params.centered; end
if (params.blendOp ~= nil) then instance.blendOp = params.blendOp; end
if (params.color ~= nil) then instance.color = params.color; end
if (params.alpha ~= nil) then instance.alpha = params.alpha; end
if (params.stroke ~= nil) then instance.stroke = params.stroke; end
return CreateInstance(Animation, instance);
end
---Create an AnimationState to play this animation.
---The AnimationState is not started.
---@param callback function?
---@return AnimationState
function Animation:createState(callback)
---@type AnimationState
local state = { animation = self, callback = callback, frameIndex = 1, timer = 0, running = false };
return CreateInstance(AnimationState, state);
end
---Create an AnimationState to play this animation and start it.
---@param callback function?
---@return AnimationState
function Animation:start(callback)
local state = self:createState(callback);
state:start();
return state;
end
---Start this AnimationState.
---Does nothing if it's already running.
function AnimationState:start()
self.running = true;
end
---Restart this AnimationState.
---The frame index is reset to 1.
function AnimationState:restart()
self.running = true;
self.frameIndex = 1;
self.timer = 0;
end
---Stop this AnimationState.
function AnimationState:stop()
self.running = false;
end
---Updates this AnimationState and then rendersit, passing on the given ImageParams to each frame.
---@param deltaTime number
---@param params? ImageParams
function AnimationState:render(deltaTime, params)
if (not self.running) then return; end;
self.timer = self.timer + deltaTime;
while (self.timer > self.animation.frameTime) do
self.timer = self.timer - self.animation.frameTime;
self.frameIndex = self.frameIndex + 1;
if (self.frameIndex > self.animation.frameCount) then
if (self.animation.loop) then
self.frameIndex = self.animation.loopPoint;
else
self.running = false;
if (self.callback) then
self.callback();
end
return;
end
end
end
if (params) then
if (params.width == nil) then params.width = self.animation.width; end
if (params.height == nil) then params.height = self.animation.height; end
if (params.x == nil) then params.x = self.animation.x; end
if (params.y == nil) then params.y = self.animation.y; end
if (params.scaleX == nil) then params.scaleX = self.animation.scaleX; end
if (params.scaleY == nil) then params.scaleY = self.animation.scaleY; end
if (params.centered == nil) then params.centered = self.animation.centered; end
if (params.blendOp == nil) then params.blendOp = self.animation.blendOp; end
if (params.alpha == nil) then params.alpha = self.animation.alpha; end
if (params.stroke == nil) then params.stroke = self.animation.stroke; end
end
local frame = self.animation.frames[self.frameIndex];
if (not frame) then
-- TODO(local): what do
else
frame:render(params);
end
end
return Animation;

View File

@ -1,74 +0,0 @@
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

View File

@ -1,6 +0,0 @@
-- TODO(local): put these class types somewhere more common
---@class StrokeParams
---@field color number[]?
---@field alpha number?
---@field size number?

View File

@ -1,171 +0,0 @@
require "common.class"
require "api.graphics"
---@class ImageParams
---@field width number
---@field height number
---@field x number?
---@field y number?
---@field scaleX number?
---@field scaleY number?
---@field centered boolean?
---@field blendOp integer?
---@field color number[]?
---@field alpha number?
---@field stroke StrokeParams?
---@class Image
---@field handle integer
---@field width number
---@field height number
---@field x number?
---@field y number?
---@field scaleX number?
---@field scaleY number?
---@field centered boolean?
---@field blendOp integer?
---@field color number[]?
---@field alpha number?
---@field stroke StrokeParams?
local Image = { };
---Image constructor
---@param imagePath string # The path to the skin image to load
---@return Image
function Image.new(imagePath, noFallback)
local handle = gfx.CreateSkinImage(imagePath or '', 0);
if (not handle) then
game.Log('Failed to load image "' .. imagePath .. '"', game.LOGGER_ERROR);
if (noFallback) then return nil; end
handle = gfx.CreateSkinImage('missing.png', 0);
if (not handle) then
game.Log('Failed to load fallback image "missing.png"', game.LOGGER_ERROR);
end
end
local width, height = 64, 64;
if (handle) then
width, height = gfx.ImageSize(handle);
end
local instance = {
handle = handle,
width = width,
height = height,
};
return CreateInstance(Image, instance);
end
---Set the width and height of this Image.
---@param width number
---@param height number
---@return Image # Returns self for method chaining
function Image:setSize(width, height)
if (type(width) ~= "number") then width = 0; end
if (type(height) ~= "number") then height = 0; end
self.width = width;
self.height = height;
return self;
end
---Set the stored position for this Image.
---If the position of this Image will not change frequently,
---using this method allows you to cache the render position
---instead of passing it to the render method on each invocation.
---@param x number
---@param y number
---@return Image # Returns self for method chaining
function Image:setPosition(x, y)
if (type(x) ~= "number") then x = 0; end
if (type(y) ~= "number") then y = 0; end
self.x = x;
self.y = y;
return self;
end
---Renders this Image, applying any of the given ImageParams,
---then any of the cached Image fields, then any default values.
---@param params? ImageParams
function Image:render(params)
params = params or { };
local sx = params.scaleX or self.scaleX or 1;
local sy = params.scaleY or self.scaleY or 1;
local x = params.x or self.x or 0;
local y = params.y or self.y or 0;
local w = (params.width or self.width ) * sx;
local h = (params.height or self.height) * sy;
if (params.centered or self.centered) then
x = x - w / 2;
y = y - h / 2;
end
local blendOp = params.blendOp or self.blendOp or gfx.BLEND_OP_SOURCE_OVER;
local r = 255;
local g = 255;
local b = 255;
if (params.color) then
r = params.color[1];
g = params.color[2];
b = params.color[3];
elseif (self.color) then
r = self.color[1];
g = self.color[2];
b = self.color[3];
end
local a = params.alpha or self.alpha or 1;
gfx.BeginPath();
gfx.GlobalCompositeOperation(blendOp);
if (not self.handle) then
gfx.FillColor(r, g, b, a);
gfx.Rect(x, y, w, h);
gfx.FillColor(255, 255, 255, 255);
else
gfx.SetImageTint(r, g, b);
gfx.ImageRect(x, y, w, h, self.handle, a, 0);
gfx.SetImageTint(255, 255, 255);
end
if (params.stroke or self.stroke) then
r = 255;
g = 255;
b = 255;
if (params.stroke.color) then
r = params.stroke.color[1];
g = params.stroke.color[2];
b = params.stroke.color[3];
elseif (self.stroke and self.stroke.color) then
r = self.stroke.color[1];
g = self.stroke.color[2];
b = self.stroke.color[3];
end
a = params.stroke.alpha or (self.stroke and self.stroke.alpha) or 255;
local size = params.stroke.size or (self.stroke and self.stroke.size) or 1;
gfx.StrokeColor(r, g, b, a);
gfx.StrokeWidth(size);
gfx.Stroke();
end
end
return Image;

View File

@ -1,28 +0,0 @@
---@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

@ -1,325 +1,302 @@
local Easing = require("common.easing");
local Footer = require("components.footer");
local DiffRectangle = require('components.diff_rectangle');
local common = require('common.util');
local Sound = require("common.sound")
local Numbers = require('components.numbers')
local VolforceWindow = require("components.volforceWindow")
require("common.gameconfig")
-- Lua code completion annotation
---@diagnostic disable-next-line
---@cast result ChallengeResult
-- Window variables
local resX, resY
-- Aspect Ratios
local landscapeWidescreenRatio = 16 / 9
local landscapeStandardRatio = 4 / 3
local portraitWidescreenRatio = 9 / 16
-- Portrait sizes
local fullX, fullY
local desw = 1080
local desh = 1920
local function resolutionChange(x, y)
resX = x
resY = y
fullX = portraitWidescreenRatio * y
fullY = y
end
local bgSfxPlayed = false;
local BAR_ALPHA = 191;
local HEADER_HEIGHT = 100
local backgroundImage = gfx.CreateSkinImage("bg_pattern.png", gfx.IMAGE_REPEATX | gfx.IMAGE_REPEATY)
local resultBgImage = gfx.CreateSkinImage("challenge_result/bg.png", 0);
local playerInfoOverlayBgImage = gfx.CreateSkinImage("challenge_result/player_info_overlay_bg.png", 0);
local headerTitleImage = gfx.CreateSkinImage("challenge_result/header/title.png", 0);
-- gameplay table does not have a current username field, because why would it lmao
-- workaround: retrieve it directly from Main.cfg file
local username = GameConfig["MultiplayerUsername"] or game.GetSkinSetting("username") or "";
local appealCardImage = gfx.CreateSkinImage("crew/appeal_card.png", 0);
local danBadgeImage = gfx.CreateSkinImage("dan/inf.png", 0);
local crewImage = gfx.CreateSkinImage("crew/portrait.png", 0);
local notchesImage = gfx.CreateSkinImage("challenge_result/notches.png", 0);
local trackBarsImage = gfx.CreateSkinImage("challenge_result/track_bars.png", 0);
local completionFailImage = gfx.CreateSkinImage("challenge_result/pass_states/fail.png", 0);
local completionPassImage = gfx.CreateSkinImage("challenge_result/pass_states/pass.png", 0);
local irHeartbeatRequested = false;
local IRserverName = "";
local badgeImages = {
gfx.CreateSkinImage("song_select/medal/nomedal.png", 1),
gfx.CreateSkinImage("song_select/medal/played.png", 1),
gfx.CreateSkinImage("song_select/medal/clear.png", 1),
gfx.CreateSkinImage("song_select/medal/hard.png", 1),
gfx.CreateSkinImage("song_select/medal/uc.png", 1),
gfx.CreateSkinImage("song_select/medal/puc.png", 1),
}
local gradeImages = {
S = gfx.CreateSkinImage("common/grades/S.png", 0),
AAA_P = gfx.CreateSkinImage("common/grades/AAA+.png", 0),
AAA = gfx.CreateSkinImage("common/grades/AAA.png", 0),
AA_P = gfx.CreateSkinImage("common/grades/AA+.png", 0),
AA = gfx.CreateSkinImage("common/grades/AA.png", 0),
A_P = gfx.CreateSkinImage("common/grades/A+.png", 0),
A = gfx.CreateSkinImage("common/grades/A.png", 0),
B = gfx.CreateSkinImage("common/grades/B.png", 0),
C = gfx.CreateSkinImage("common/grades/C.png", 0),
D = gfx.CreateSkinImage("common/grades/D.png", 0),
none = gfx.CreateSkinImage("common/grades/none.png", 0),
}
local percRequired = nil;
local percGet = nil;
-- AUDIO
game.LoadSkinSample("challenge_result.wav")
function resetLayoutInformation()
resx, resy = game.GetResolution()
desw = 1080
desh = 1920
scale = resx / desw
end
local function handleSfx()
if not bgSfxPlayed then
Sound.stopMusic();
game.PlaySample("challenge_result.wav", true)
bgSfxPlayed = true
end
if game.GetButton(game.BUTTON_STA) then
game.StopSample("challenge_result.wav")
end
if game.GetButton(game.BUTTON_BCK) then
game.StopSample("challenge_result.wav")
end
end
function drawBackground()
gfx.BeginPath()
gfx.ImageRect(0, 0, desw, desh, resultBgImage, 1, 0);
end
function drawHeader()
gfx.BeginPath();
gfx.FillColor(0, 0, 0, BAR_ALPHA);
gfx.Rect(0, 0, desw, HEADER_HEIGHT);
gfx.Fill();
gfx.ClosePath()
gfx.ImageRect(desw / 2 - 209, HEADER_HEIGHT / 2 - 52, 419, 105, headerTitleImage, 1, 0)
end
function drawPlayerInfo()
-- Draw crew
gfx.BeginPath()
gfx.ImageRect(460, 215, 522, 362, crewImage, 1, 0);
-- Draw the info bg
gfx.BeginPath()
gfx.ImageRect(300, 352, 374 * 0.85, 222 * 0.85, playerInfoOverlayBgImage, 1, 0);
-- Draw appeal card
gfx.BeginPath();
gfx.ImageRect(145, 364, 103 * 1.25, 132 * 1.25, appealCardImage, 1, 0);
-- Draw description
gfx.FontSize(28)
gfx.LoadSkinFont("Digital-Serial-Bold.ttf")
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_MIDDLE)
gfx.Text("Hellooooooo", 310, 370);
-- Draw username
gfx.FontSize(40)
gfx.Text(username, 310, 413);
-- Draw IR server name
gfx.FontSize(28)
gfx.Text(IRserverName, 310, 453);
-- Draw dan badge
gfx.BeginPath();
gfx.ImageRect(311, 490, 107 * 1.25, 29 * 1.25, danBadgeImage, 1, 0);
end
local scoreNumber = Numbers.load_number_image("score_num");
function drawChartResult(deltaTime, x, y, chartResult)
gfx.Save()
gfx.LoadSkinFont('NotoSans-Regular.ttf')
gfx.FontSize(28)
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_MIDDLE)
gfx.BeginPath()
gfx.GlobalAlpha(1);
gfx.Text(chartResult.title, x+160,y+32);
DiffRectangle.render(deltaTime, x+287.5, y+67, 0.85, chartResult.difficulty+1, chartResult.level)
local score = chartResult.score or 0;
Numbers.draw_number(x + 500, y+80, 1.0, math.floor(score / 10000), 4, scoreNumber, true, 0.30, 1.12)
Numbers.draw_number(x + 655, y+85, 1.0, score, 4, scoreNumber, true, 0.22, 1.12)
local gradeImageKey = string.gsub(chartResult.grade, '+', '_P');
local gradeImage = gradeImages[gradeImageKey] or gradeImages.D
gfx.BeginPath()
gfx.ImageRect(x+800, y+12, 79, 79, gradeImage, 1, 0)
if chartResult.badge then
local badgeImage = badgeImages[chartResult.badge+1];
gfx.BeginPath()
gfx.ImageRect(x+900, y+16, 79*1.05, 69*1.05, badgeImage, 1, 0)
end
gfx.Restore()
end
function drawScorePanelContent(deltaTime)
-- game.Log("Drawing scores...", game.LOGGER_INFO) -- debug
for i, chart in ipairs(result.charts) do
-- if chart.score == nil then
-- game.Log("Score does not exist? Quitting loop...", game.LOGGER_WARNING)
-- break
-- end
drawChartResult(deltaTime, 0, 836+(165*(i-1)), chart);
end
end
function drawDecorations()
gfx.BeginPath()
gfx.ImageRect(118, 846.5, 43*0.855, 429*0.855, notchesImage, 1, 0)
gfx.BeginPath()
gfx.ImageRect(400, 807, 367*0.857, 429*0.857, trackBarsImage, 1, 0)
end
function drawCompletion()
local completitionImage = completionFailImage;
if result.passed then
completitionImage = completionPassImage;
end
gfx.BeginPath()
gfx.ImageRect(63, 1331, 766*0.85, 130*0.85, completitionImage, 1, 0)
if (percRequired == nil) then
return
end
Numbers.draw_number(925, 1370, 1.0, percGet, 3, scoreNumber, true, 0.3, 1.12)
gfx.BeginPath();
gfx.Rect(741, 1402, 278*math.min(1, percGet / percRequired), 6);
gfx.FillColor(255, 128, 0, 255);
gfx.Fill()
end
function result_set()
if (result.requirement_text == nil) then
return
end
local reqTextWords = common.split(result.requirement_text, ' ');
for index, word in ipairs(reqTextWords) do
if string.find(word, '%%') ~= nil then -- %% = %, because % is an escape char
local percNumber = tonumber(string.gsub(word, '%%', ''), 10)
percRequired = percNumber;
end
end
if (percRequired == nil) then
return
end
game.Log(percRequired, game.LOGGER_ERROR);
local a = 0;
for i, chart in ipairs(result.charts) do
a = a + chart.percent;
game.Log('#' .. i .. ' got ' .. chart.percent .. '% // ACC at ' .. a, game.LOGGER_ERROR);
end
percGet = a / #result.charts;
end
local IR_HeartbeatResponse = function(res)
if res.statusCode == IRData.States.Success then
IRserverName = res.body.serverName .. " " .. res.body.irVersion;
else
game.Log("Can't connect to IR!", game.LOGGER_WARNING)
end
end
local IR_Handle = function()
if not irHeartbeatRequested then
IR.Heartbeat(IR_HeartbeatResponse)
irHeartbeatRequested = true;
end
end
local function drawResultScreen(x, y, w, h, deltaTime)
gfx.BeginPath()
gfx.Translate(x, y);
gfx.Scale(w / 1080, h / 1920);
gfx.Scissor(0, 0, 1080, 1920);
handleSfx()
IR_Handle()
drawBackground()
drawDecorations()
drawPlayerInfo()
drawScorePanelContent(deltaTime)
drawCompletion()
drawHeader()
Footer.draw(deltaTime);
gfx.ResetTransform()
end
function render(deltaTime)
-- detect resolution change
local resx, resy = game.GetResolution()
if resx ~= resX or resy ~= resY then
resolutionChange(resx, resy)
end
gfx.BeginPath()
local bgImageWidth, bgImageHeight = gfx.ImageSize(backgroundImage)
gfx.Rect(0, 0, resX, resY)
gfx.FillPaint(gfx.ImagePattern(0, 0, bgImageWidth, bgImageHeight, 0, backgroundImage, 0.2))
gfx.Fill()
drawResultScreen((resX - fullX) / 2, 0, fullX, fullY, deltaTime)
end
local Easing = require("common.easing");
local Footer = require("components.footer");
local DiffRectangle = require('components.diff_rectangle');
local common = require('common.common');
local Numbers = require('common.numbers')
local VolforceWindow = require("components.volforceWindow")
-- Window variables
local resX, resY
-- Aspect Ratios
local landscapeWidescreenRatio = 16 / 9
local landscapeStandardRatio = 4 / 3
local portraitWidescreenRatio = 9 / 16
-- Portrait sizes
local fullX, fullY
local desw = 1080
local desh = 1920
local function resolutionChange(x, y)
resX = x
resY = y
fullX = portraitWidescreenRatio * y
fullY = y
end
local bgSfxPlayed = false;
local BAR_ALPHA = 191;
local HEADER_HEIGHT = 100
local backgroundImage = gfx.CreateSkinImage("bg_pattern.png", gfx.IMAGE_REPEATX | gfx.IMAGE_REPEATY)
local resultBgImage = gfx.CreateSkinImage("challenge_result/bg.png", 0);
local playerInfoOverlayBgImage = gfx.CreateSkinImage("challenge_result/player_info_overlay_bg.png", 0);
local headerTitleImage = gfx.CreateSkinImage("challenge_result/header/title.png", 0);
local username = game.GetSkinSetting("username");
local appealCardImage = gfx.CreateSkinImage("crew/appeal_card.png", 0);
local danBadgeImage = gfx.CreateSkinImage("dan/inf.png", 0);
local crewImage = gfx.CreateSkinImage("crew/portrait.png", 0);
local notchesImage = gfx.CreateSkinImage("challenge_result/notches.png", 0);
local trackBarsImage = gfx.CreateSkinImage("challenge_result/track_bars.png", 0);
local completionFailImage = gfx.CreateSkinImage("challenge_result/pass_states/fail.png", 0);
local completionPassImage = gfx.CreateSkinImage("challenge_result/pass_states/pass.png", 0);
local irHeartbeatRequested = false;
local IRserverName = "";
local badgeImages = {
gfx.CreateSkinImage("song_select/medal/nomedal.png", 1),
gfx.CreateSkinImage("song_select/medal/played.png", 1),
gfx.CreateSkinImage("song_select/medal/clear.png", 1),
gfx.CreateSkinImage("song_select/medal/hard.png", 1),
gfx.CreateSkinImage("song_select/medal/uc.png", 1),
gfx.CreateSkinImage("song_select/medal/puc.png", 1),
}
local gradeImages = {
S = gfx.CreateSkinImage("common/grades/S.png", 0),
AAA_P = gfx.CreateSkinImage("common/grades/AAA+.png", 0),
AAA = gfx.CreateSkinImage("common/grades/AAA.png", 0),
AA_P = gfx.CreateSkinImage("common/grades/AA+.png", 0),
AA = gfx.CreateSkinImage("common/grades/AA.png", 0),
A_P = gfx.CreateSkinImage("common/grades/A+.png", 0),
A = gfx.CreateSkinImage("common/grades/A.png", 0),
B = gfx.CreateSkinImage("common/grades/B.png", 0),
C = gfx.CreateSkinImage("common/grades/C.png", 0),
D = gfx.CreateSkinImage("common/grades/D.png", 0),
none = gfx.CreateSkinImage("common/grades/none.png", 0),
}
local percRequired = 1;
local percGet = 0;
-- AUDIO
game.LoadSkinSample("challenge_result.wav")
function resetLayoutInformation()
resx, resy = game.GetResolution()
desw = 1080
desh = 1920
scale = resx / desw
end
local function handleSfx()
if not bgSfxPlayed then
common.stopMusic();
game.PlaySample("challenge_result.wav", true)
bgSfxPlayed = true
end
if game.GetButton(game.BUTTON_STA) then
game.StopSample("challenge_result.wav")
end
if game.GetButton(game.BUTTON_BCK) then
game.StopSample("challenge_result.wav")
end
end
function drawBackground()
gfx.BeginPath()
gfx.ImageRect(0, 0, desw, desh, resultBgImage, 1, 0);
end
function drawHeader()
gfx.BeginPath();
gfx.FillColor(0, 0, 0, BAR_ALPHA);
gfx.Rect(0, 0, desw, HEADER_HEIGHT);
gfx.Fill();
gfx.ClosePath()
gfx.ImageRect(desw / 2 - 209, HEADER_HEIGHT / 2 - 52, 419, 105, headerTitleImage, 1, 0)
end
function drawPlayerInfo()
-- Draw crew
gfx.BeginPath()
gfx.ImageRect(460, 215, 522, 362, crewImage, 1, 0);
-- Draw the info bg
gfx.BeginPath()
gfx.ImageRect(300, 352, 374 * 0.85, 222 * 0.85, playerInfoOverlayBgImage, 1, 0);
-- Draw appeal card
gfx.BeginPath();
gfx.ImageRect(145, 364, 103 * 1.25, 132 * 1.25, appealCardImage, 1, 0);
-- Draw description
gfx.FontSize(28)
gfx.LoadSkinFont("Digital-Serial-Bold.ttf")
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_MIDDLE)
gfx.Text("Hellooooooo", 310, 370);
-- Draw username
gfx.FontSize(40)
gfx.Text(username, 310, 413);
-- Draw IR server name
gfx.FontSize(28)
gfx.Text(IRserverName, 310, 453);
-- Draw dan badge
gfx.BeginPath();
gfx.ImageRect(311, 490, 107 * 1.25, 29 * 1.25, danBadgeImage, 1, 0);
end
local scoreNumber = Numbers.load_number_image("score_num");
function drawChartResult(deltaTime, x, y, chartResult)
gfx.Save()
gfx.LoadSkinFont('NotoSans-Regular.ttf')
gfx.FontSize(28)
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_MIDDLE)
gfx.BeginPath()
gfx.GlobalAlpha(1);
gfx.Text(chartResult.title, x+160,y+32);
DiffRectangle.render(deltaTime, x+287.5, y+67, 0.85, chartResult.difficulty, chartResult.level)
local score = chartResult.score or 0;
Numbers.draw_number(x + 500, y+80, 1.0, math.floor(score / 10000), 4, scoreNumber, true, 0.30, 1.12)
Numbers.draw_number(x + 655, y+85, 1.0, score, 4, scoreNumber, true, 0.22, 1.12)
local gradeImageKey = string.gsub(chartResult.grade, '+', '_P');
local gradeImage = gradeImages[gradeImageKey] or gradeImages.D
gfx.BeginPath()
gfx.ImageRect(x+800, y+12, 79, 79, gradeImage, 1, 0)
if chartResult.badge then
local badgeImage = badgeImages[chartResult.badge+1];
gfx.BeginPath()
gfx.ImageRect(x+900, y+16, 79*1.05, 69*1.05, badgeImage, 1, 0)
end
gfx.Restore()
end
function drawScorePanelContent(deltaTime)
-- game.Log("Drawing scores...", game.LOGGER_INFO) -- debug
for i, chart in ipairs(result.charts) do
-- if chart.score == nil then
-- game.Log("Score does not exist? Quitting loop...", game.LOGGER_WARNING)
-- break
-- end
drawChartResult(deltaTime, 0, 836+(165*(i-1)), chart);
end
end
function drawDecorations()
gfx.BeginPath()
gfx.ImageRect(118, 846.5, 43*0.855, 429*0.855, notchesImage, 1, 0)
gfx.BeginPath()
gfx.ImageRect(400, 807, 367*0.857, 429*0.857, trackBarsImage, 1, 0)
end
function drawCompletion()
local completitionImage = completionFailImage;
if result.passed then
completitionImage = completionPassImage;
end
gfx.BeginPath()
gfx.ImageRect(63, 1331, 766*0.85, 130*0.85, completitionImage, 1, 0)
Numbers.draw_number(925, 1370, 1.0, percGet, 3, scoreNumber, true, 0.3, 1.12)
gfx.BeginPath();
gfx.Rect(741, 1402, 278*math.min(1, percGet / percRequired), 6);
gfx.FillColor(255, 128, 0, 255);
gfx.Fill()
end
function result_set()
local reqTextWords = common.splitString(result.requirement_text, ' ');
for index, word in ipairs(reqTextWords) do
if string.find(word, '%%') ~= nil then -- %% = %, because % is an escape char
local percNumber = tonumber(string.gsub(word, '%%', ''), 10)
percRequired = percNumber;
end
end
game.Log(percRequired, game.LOGGER_ERROR);
local a = 0;
for i, chart in ipairs(result.charts) do
a = a + chart.percent;
game.Log('#' .. i .. ' got ' .. chart.percent .. '% // ACC at ' .. a, game.LOGGER_ERROR);
end
percGet = a / #result.charts;
end
local IR_HeartbeatResponse = function(res)
if res.statusCode == IRData.States.Success then
IRserverName = res.body.serverName .. " " .. res.body.irVersion;
else
game.Log("Can't connect to IR!", game.LOGGER_WARNING)
end
end
local IR_Handle = function()
if not irHeartbeatRequested then
IR.Heartbeat(IR_HeartbeatResponse)
irHeartbeatRequested = true;
end
end
local function drawResultScreen(x, y, w, h, deltaTime)
gfx.BeginPath()
gfx.Translate(x, y);
gfx.Scale(w / 1080, h / 1920);
gfx.Scissor(0, 0, 1080, 1920);
handleSfx()
IR_Handle()
drawBackground()
drawDecorations()
drawPlayerInfo()
drawScorePanelContent(deltaTime)
drawCompletion()
drawHeader()
Footer.draw(deltaTime);
gfx.ResetTransform()
end
function render(deltaTime)
-- detect resolution change
local resx, resy = game.GetResolution()
if resx ~= resX or resy ~= resY then
resolutionChange(resx, resy)
end
gfx.BeginPath()
local bgImageWidth, bgImageHeight = gfx.ImageSize(backgroundImage)
gfx.Rect(0, 0, resX, resY)
gfx.FillPaint(gfx.ImagePattern(0, 0, bgImageWidth, bgImageHeight, 0, backgroundImage, 0.2))
gfx.Fill()
drawResultScreen((resX - fullX) / 2, 0, fullX, fullY, deltaTime)
end

View File

@ -1,107 +1,107 @@
options = {}
yscale = 0.0
selectedIndex = 0
resx, resy = game.GetResolution()
width = 500
titleText = ""
scale = 1.0
function make_option(name)
return function()
menu.Confirm(name)
end
end
function open()
yscale = 0.0
options = {}
selectedIndex = 0
resx, resy = game.GetResolution()
index = 1
if #dialog.collections == 0 then
options[index] = {"Favourites", make_option("Favourites"), {255,255,255}}
end
for i,value in ipairs(dialog.collections) do
options[i] = {value.name, make_option(value.name), {255,255,255}}
if value.exists then options[i][3] = {255,0,0} end
end
table.insert(options, {"New Collection", menu.ChangeState, {0, 255, 128}})
table.insert(options, {"Cancel", menu.Cancel, {200,200,200}})
gfx.FontFace("fallback")
gfx.FontSize(50)
titleText = string.format("Add %s to collection:", dialog.title)
xmi,ymi,xma,yma = gfx.TextBounds(0,0, titleText)
width = xma - xmi + 50
width = math.max(500, width)
scale = math.min(resy / 1280, 1.0)
scale = math.min(scale, resx / width)
end
function render(deltaTime)
if dialog.closing then
yscale = math.min(yscale - deltaTime * 4, 1.0)
else
yscale = math.min(yscale + deltaTime * 4, 1.0)
end
gfx.Translate(resx / 2, resy / 2)
gfx.Scale(scale, yscale * scale)
gfx.BeginPath()
gfx.Rect(-width/2, -250, width, 500)
gfx.FillColor(50,50,50)
gfx.Fill()
gfx.BeginPath()
gfx.FillColor(255,255,255)
gfx.FontFace("fallback")
gfx.FontSize(50)
gfx.TextAlign(gfx.TEXT_ALIGN_TOP + gfx.TEXT_ALIGN_CENTER)
gfx.Text(titleText, 0, -240)
if dialog.isTextEntry then
gfx.TextAlign(gfx.TEXT_ALIGN_MIDDLE + gfx.TEXT_ALIGN_CENTER)
gfx.BeginPath()
gfx.Rect(-width/2 + 20, -30, width - 40, 60)
gfx.FillColor(25,25,25)
gfx.Fill()
gfx.BeginPath()
gfx.FillColor(255,255,255)
gfx.Text(dialog.newName, 0, 0)
else
local yshift = 20
gfx.TextAlign(gfx.TEXT_ALIGN_MIDDLE + gfx.TEXT_ALIGN_LEFT)
for i, option in ipairs(options) do
local y = yshift + 60 * ((i-1) - selectedIndex)
if y > -190 and y < 220 then
gfx.FillColor(option[3][1], option[3][2], option[3][3])
gfx.Text(option[1], 40 - width/2, y)
end
end
gfx.BeginPath()
gfx.MoveTo(20 - width/2, -10 + yshift)
gfx.LineTo(30 - width/2, 0 + yshift)
gfx.LineTo(20 - width/2, 10 + yshift)
gfx.FillColor(255,255,255)
gfx.Fill()
end
if dialog.closing == true and yscale <= 0.0 then
return false
else
return true
end
end
function button_pressed(button)
if button == game.BUTTON_BCK then
menu.Cancel()
elseif button == game.BUTTON_STA then
options[selectedIndex+1][2]()
end
end
function advance_selection(value)
selectedIndex = (selectedIndex + value) % #options
options = {}
yscale = 0.0
selectedIndex = 0
resx, resy = game.GetResolution()
width = 500
titleText = ""
scale = 1.0
function make_option(name)
return function()
menu.Confirm(name)
end
end
function open()
yscale = 0.0
options = {}
selectedIndex = 0
resx, resy = game.GetResolution()
index = 1
if #dialog.collections == 0 then
options[index] = {"Favourites", make_option("Favourites"), {255,255,255}}
end
for i,value in ipairs(dialog.collections) do
options[i] = {value.name, make_option(value.name), {255,255,255}}
if value.exists then options[i][3] = {255,0,0} end
end
table.insert(options, {"New Collection", menu.ChangeState, {0, 255, 128}})
table.insert(options, {"Cancel", menu.Cancel, {200,200,200}})
gfx.FontFace("fallback")
gfx.FontSize(50)
titleText = string.format("Add %s to collection:", dialog.title)
xmi,ymi,xma,yma = gfx.TextBounds(0,0, titleText)
width = xma - xmi + 50
width = math.max(500, width)
scale = math.min(resy / 1280, 1.0)
scale = math.min(scale, resx / width)
end
function render(deltaTime)
if dialog.closing then
yscale = math.min(yscale - deltaTime * 4, 1.0)
else
yscale = math.min(yscale + deltaTime * 4, 1.0)
end
gfx.Translate(resx / 2, resy / 2)
gfx.Scale(scale, yscale * scale)
gfx.BeginPath()
gfx.Rect(-width/2, -250, width, 500)
gfx.FillColor(50,50,50)
gfx.Fill()
gfx.BeginPath()
gfx.FillColor(255,255,255)
gfx.FontFace("fallback")
gfx.FontSize(50)
gfx.TextAlign(gfx.TEXT_ALIGN_TOP + gfx.TEXT_ALIGN_CENTER)
gfx.Text(titleText, 0, -240)
if dialog.isTextEntry then
gfx.TextAlign(gfx.TEXT_ALIGN_MIDDLE + gfx.TEXT_ALIGN_CENTER)
gfx.BeginPath()
gfx.Rect(-width/2 + 20, -30, width - 40, 60)
gfx.FillColor(25,25,25)
gfx.Fill()
gfx.BeginPath()
gfx.FillColor(255,255,255)
gfx.Text(dialog.newName, 0, 0)
else
local yshift = 20
gfx.TextAlign(gfx.TEXT_ALIGN_MIDDLE + gfx.TEXT_ALIGN_LEFT)
for i, option in ipairs(options) do
local y = yshift + 60 * ((i-1) - selectedIndex)
if y > -190 and y < 220 then
gfx.FillColor(option[3][1], option[3][2], option[3][3])
gfx.Text(option[1], 40 - width/2, y)
end
end
gfx.BeginPath()
gfx.MoveTo(20 - width/2, -10 + yshift)
gfx.LineTo(30 - width/2, 0 + yshift)
gfx.LineTo(20 - width/2, 10 + yshift)
gfx.FillColor(255,255,255)
gfx.Fill()
end
if dialog.closing == true and yscale <= 0.0 then
return false
else
return true
end
end
function button_pressed(button)
if button == game.BUTTON_BCK then
menu.Cancel()
elseif button == game.BUTTON_STA then
options[selectedIndex+1][2]()
end
end
function advance_selection(value)
selectedIndex = (selectedIndex + value) % #options
end

View File

@ -1,6 +1,156 @@
-- NOTE(local): DO NOT PUT ANYTHING HERE PLEASE THANKS
-- IF YOU DO THE GAME WILL IMMEDIATELY CRASH AND IT'S NOT THE BEST
-- THANK YOU HAVE A GOOD LUA
gfx.LoadSkinFont("segoeui.ttf")
function useFootGun() while true do end end
-- Memo class
-------------
Memo = {}
Memo.new = function()
local this = {
cache = {}
}
setmetatable(this, {__index = Memo})
return this
end
Memo.memoize = function(this, key, generator)
local value = this.cache[key]
if not value then
value = generator()
this.cache[key] = value
end
return value
end
-- Image class
--------------
Image = {
ANCHOR_LEFT = 1,
ANCHOR_CENTER = 2,
ANCHOR_RIGHT = 4,
ANCHOR_TOP = 8,
ANCHOR_BOTTOM = 16
}
Image.skin = function(filename, imageFlags)
imageFlags = imageFlags or 0
local image = gfx.CreateSkinImage(filename, imageFlags)
return Image.wrap(image)
end
Image.wrap = function(image)
local this = {
image = image
}
local w, h = gfx.ImageSize(this.image)
this.w = w
this.h = h
setmetatable(this, {__index = Image})
return this
end
Image.draw = function(this, params)
local x = params.x
local y = params.y
local w = params.w or this.w
local h = params.h or this.h
local alpha = params.alpha or 1
local angle = params.angle or 0
local anchor_h = params.anchor_h or Image.ANCHOR_CENTER
local anchor_v = params.anchor_v or Image.ANCHOR_CENTER
local scale = params.scale or 1;
w = w * scale;
h = h * scale;
if anchor_h == Image.ANCHOR_CENTER then
x = x - w / 2
elseif anchor_h == Image.ANCHOR_RIGHT then
x = x - w
end
if anchor_v == Image.ANCHOR_CENTER then
y = y - h / 2
elseif anchor_v == Image.ANCHOR_BOTTOM then
y = y - h
end
gfx.BeginPath()
gfx.ImageRect(x, y, w, h, this.image, alpha, angle)
end
-- ImageFont class
------------------
ImageFont = {}
ImageFont.new = function(path, chars)
local this = {
images = {}
}
-- load character images
for i = 1, chars:len() do
local c = chars:sub(i, i)
local n = c
if c == "." then
n = "dot"
end
local image = Image.skin(string.format("%s/%s.png", path, n), 0)
this.images[c] = image
end
-- use size of first char as font size
local w, h = gfx.ImageSize(this.images[chars:sub(1, 1)].image)
this.w = w
this.h = h
setmetatable(this, {__index = ImageFont})
return this
end
ImageFont.draw = function(this, text, x, y, alpha, hFlag, vFlag)
local totalW = text:len() * this.w
-- adjust horizontal alignment
if hFlag == gfx.TEXT_ALIGN_CENTER then
x = x - totalW / 2
elseif hFlag == gfx.TEXT_ALIGN_RIGHT then
x = x - totalW
end
-- adjust vertical alignment
if vFlag == gfx.TEXT_ALIGN_MIDDLE then
y = y - this.h / 2
elseif vFlag == gfx.TEXT_ALIGN_BOTTOM then
y = y - this.h
end
for i = 1, text:len() do
local c = text:sub(i, i)
local image = this.images[c]
if image ~= nil then
gfx.BeginPath()
gfx.ImageRect(x, y, this.w, this.h, image.image, alpha, 0)
end
x = x + this.w
end
end
function GetDisplayDifficulty(jacketPath, difficulty)
local strippedPath = string.match(jacketPath:lower(), "[/\\][^\\/]+$")
if difficulty == 3 and strippedPath then
if string.find(strippedPath, "inf") ~= nil then
return 5
elseif string.find(strippedPath, "grv") ~= nil then
return 6
elseif string.find(strippedPath, "hvn") ~= nil then
return 7
elseif string.find(strippedPath, "vvd") ~= nil then
return 8
end
end
return difficulty+1
end
function split(s, delimiter)
result = {};
for match in (s..delimiter):gmatch("(.-)"..delimiter) do
table.insert(result, match);
end
return result;
end

View File

@ -1,26 +0,0 @@
local Charting = { };
function Charting.GetDisplayDifficulty(jacketPath, difficulty)
if jacketPath == nil then
return difficulty
end
local strippedPath = string.match(jacketPath:lower(), "[/\\][^\\/]+$")
if difficulty == 3 and strippedPath then
if string.find(strippedPath, "inf") ~= nil then
return 5
elseif string.find(strippedPath, "grv") ~= nil then
return 6
elseif string.find(strippedPath, "hvn") ~= nil then
return 7
elseif string.find(strippedPath, "vvd") ~= nil then
return 8
elseif string.find(strippedPath, "xcd") ~= nil then
return 9
end
end
return difficulty + 1
end
return Charting;

View File

@ -1,37 +0,0 @@
---Member lookup helper function
---@param key string
---@param bases any
---@return any
local function search(key, bases)
for _, base in ipairs(bases) do
local v = base[key] -- try `i'-th superclass
if v then return v end
end
end
---Create polimorphic class
---@generic BaseT, T
---@param cls T # class metatable
---@param o? table # initial parameters
---@param ... BaseT # base class metatables (if any)
---@return T # class instance
function CreateInstance(cls, o, ...)
o = o or {}
local nargs = select("#", ...)
local vargs = { select(1, ...) }
cls.__index = cls
if nargs == 1 then
-- single inheritance
local base = vargs[1]
setmetatable(cls, {__index = base})
o = base.new(o)
elseif nargs > 1 then
-- multiple inheritance (note: slow(er) member lookup)
setmetatable(cls, {__index = function(t, k) return search(k, vargs) end})
for _, base in ipairs(vargs) do
o = base.new(o)
end
end
setmetatable(o, cls)
return o
end

34
scripts/common/common.lua Normal file
View File

@ -0,0 +1,34 @@
local stopMusic = function ()
local musicPlaying = game.GetSkinSetting('_musicPlaying');
if musicPlaying and musicPlaying ~= '' then
game.StopSample(musicPlaying);
game.SetSkinSetting("_musicPlaying", "")
end
end
local function splitString(inputstr, sep)
if sep == nil then
sep = "%s"
end
local t={}
for str in string.gmatch(inputstr, "([^"..sep.."]+)") do
table.insert(t, str)
end
return t
end
local function filter(tableIn, predicate)
local out = {}
for _, val in ipairs(tableIn) do
if predicate(val) then
table.insert(out, val)
end
end
return out
end
return {
stopMusic = stopMusic,
splitString = splitString,
filter = filter
}

View File

@ -1,64 +0,0 @@
local dimtable = {
design = {width = 1080, height = 1920},
screen = {width = nil, height = nil},
view = {width = nil, height = nil},
ratio = {landscapeUW = 21 / 9, landscapeWide = 16 / 9, landscapeStd = 4 / 3, portrait = 9 / 16},
}
dimtable.transformToScreenSpace = function()
gfx.Translate((dimtable.screen.width - dimtable.view.width) / 2, 0);
gfx.Scale(dimtable.view.width / dimtable.design.width, dimtable.view.height / dimtable.design.height);
gfx.Scissor(0, 0, dimtable.design.width, dimtable.design.height);
end
dimtable.updateResolution = function(ratio)
if not ratio then ratio = dimtable.ratio.portrait end
local screenWidth, screenHeight = game.GetResolution()
if screenWidth ~= dimtable.screen.width or screenHeight ~= dimtable.screen.height then
dimtable.screen.width, dimtable.screen.height = screenWidth, screenHeight
dimtable.view.width, dimtable.view.height = ratio * dimtable.screen.height, dimtable.screen.height
end
end
---Convert screenspace coordinates to viewspace coordinates
---@param screenX number
---@param screenY number
---@param offsetX? number Viewport offset from the left side (defaults to the portrait viewport offset)
---@param offsetY? number Viewport offset from the top side (defaults to 0)
---@return number, number
dimtable.toViewSpace = function(screenX, screenY, offsetX, offsetY)
offsetX = offsetX or (dimtable.screen.width - dimtable.view.width) / 2
offsetY = offsetY or 0
local viewX, viewY, scaleX, scaleY
scaleX = dimtable.design.width / dimtable.view.width
scaleY = dimtable.design.height / dimtable.view.height
viewX = (screenX - offsetX) * scaleX
viewY = (screenY - offsetY) * scaleY
return viewX, viewY
end
---Set's up scaled transforms based on the current resolution.
---@param x number
---@param y number
---@param rotation number
---@return number, boolean # The scale applied to the transform and the current landscape state
function dimtable.setUpTransforms(x, y, rotation)
local isLandscape = dimtable.view.width > dimtable.view.height;
local designWidth = isLandscape and dimtable.design.height or dimtable.design.width
local scale = dimtable.view.width / designWidth
gfx.ResetTransform();
gfx.Translate(x, y);
gfx.Rotate(rotation);
gfx.Scale(scale, scale);
return scale, isLandscape;
end
return dimtable

View File

@ -1,105 +0,0 @@
require("common.globals")
--file reader utility functions
---Get game path
---@return string, string
local function getGamePath()
return debug.getinfo(1,"S").source:sub(2):match("(.*)([\\/])skins") -- this is very hacky :)
end
local function readBytes(_file)
local out = {}
repeat
local buffer = _file:read(4*1024)
for c in (buffer or ''):gmatch(".") do
table.insert(out, c:byte())
end
until not buffer
return out
end
---Read a file in the game folder by lines
---@param path string relative path to game file
---@param mode? openmode default "r"
---@return nil|string[]
function ReadGameFileLines(path, mode)
mode = mode or "r"
local gamepath, sep = getGamePath()
local lines = {}
local f = io.open(gamepath .. sep .. path, mode)
if not f then return nil end
for line in f:lines("l") do
table.insert(lines, line)
end
f:close()
return lines
end
---Read a file in the game folder
---@param path string # relative path to game file
---@param mode? openmode # default "r"
---@return nil|string|integer[]
function ReadGameFile(path, mode)
mode = mode or "r"
local gamepath, sep = getGamePath()
local out
local f = io.open(gamepath .. sep .. path, mode)
if not f then return nil end
if mode:match(".*b") then
out = readBytes(f)
else
out = f:read("a")
end
f:close()
return out
end
---Find patterns in file
---@param path string # relative path to game file
---@param pattern string # search pattern
---@return table # {{group1, group2, ...}, ...}
function FindPatterns(path, pattern)
local matches = {}
for _, line in ipairs(ReadGameFileLines(path, "r")) do
if line:match(pattern) then
table.insert(matches, {line:match(pattern)})
end
end
return matches
end
--- Check if a file or directory exists in this path
---@param file string # relative path to game file
---@return boolean # file exists
---@return string # error message
function IsFileExists(file)
local gamepath, sep = getGamePath()
file = gamepath .. sep .. file
local ok, err, code = os.rename(file, file)
if not ok then
game.Log("err: "..err..", code: "..code, game.LOGGER_DEBUG)
if code == 13 then
-- Permission denied, but it exists
return true
end
end
return ok, err
end
--- Check if a directory exists in this path
---@param path string # relative path to game directory
---@return boolean # directory exists
function IsDir(path)
-- "/" works on both Unix and Windows
return IsFileExists(path .. "/")
end

View File

@ -1,11 +0,0 @@
require("common.filereader")
GameConfig = {}
function RefreshConfig()
for _, match in ipairs(FindPatterns("Main.cfg", "(%w*)%s*=%s*\"?([^\"%s]*)\"?")) do
GameConfig[match[1]] = match[2]
end
end
RefreshConfig()

View File

@ -1,2 +0,0 @@
---Drewol, what are you doing? Why is there no game.LOGGER_DEBUG?
game.LOGGER_DEBUG = 0

View File

@ -1,11 +0,0 @@
local stopMusic = function ()
local musicPlaying = game.GetSkinSetting('_musicPlaying');
if musicPlaying and musicPlaying ~= '' then
game.StopSample(musicPlaying);
game.SetSkinSetting("_musicPlaying", "")
end
end
return {
stopMusic = stopMusic
}

View File

@ -1,132 +0,0 @@
local function split(s, delimiter)
local result = {};
for match in (s..delimiter):gmatch("(.-)"..delimiter) do
table.insert(result, match);
end
return result;
end
local function filter(tableIn, predicate)
local out = {}
for _, val in ipairs(tableIn) do
if predicate(val) then
table.insert(out, val)
end
end
return out
end
local function clamp(x, min, max)
if x < min then
x = min
end
if x > max then
x = max
end
return x
end
local function round(num)
return num + (2^52 + 2^51) - (2^52 + 2^51)
end
local function sign(x)
return (
(x > 0) and 1
or
(x < 0) and -1
or
0
)
end
local function roundToZero(x)
if x < 0 then
return math.ceil(x)
elseif x > 0 then
return math.floor(x)
else
return 0
end
end
local function areaOverlap(x, y, areaX, areaY, areaW, areaH)
return x > areaX and y > areaY and x < areaX + areaW and y < areaY + areaH
end
local function lerp(x, x0, y0, x1, y1)
return y0 + (x - x0) * (y1 - y0) / (x1 - x0)
end
local function mix(x, y, a)
return (1 - a) * x + a * y
end
--modulo operation for index value
local function modIndex(index, mod)
return (index - 1) % mod + 1
end
local function firstAlphaNum(s)
for i = 1, string.len(s) do
local byte = string.byte(s, i);
if ((byte >= 65 and byte <= 90) or (byte >= 97 and byte <= 122) or (byte >= 48 and byte <= 57)) then
return string.sub(s, i, i);
end
end
return '';
end
local function dump(o)
if type(o) == 'table' then
local s = '{ '
for k,v in pairs(o) do
if type(k) ~= 'number' then k = '"'..k..'"' end
s = s .. '['..k..'] = ' .. dump(v) .. ','
end
return s .. '} '
else
return tostring(o)
end
end
local function all(t, predicate)
predicate = predicate or function(e) return e end
for _, e in ipairs(t) do
if not predicate(e) then
return false
end
end
return true
end
local function any(t, predicate)
predicate = predicate or function(e) return e end
for _, e in ipairs(t) do
if predicate(e) then
return true
end
end
return false
end
return {
split = split,
filter = filter,
clamp = clamp,
round = round,
sign = sign,
roundToZero = roundToZero,
areaOverlap = areaOverlap,
lerp = lerp,
mix = mix,
modIndex = modIndex,
firstAlphaNum = firstAlphaNum,
dump = dump,
all = all,
any = any
}

View File

@ -1,21 +1,6 @@
local MAJOR = 0
local MINOR = 3
local PATCH = 0
local function getLongVersion()
return "USC:E:G:S:" .. MAJOR .. MINOR .. PATCH
end
---Get version string
---@return string
local function getVersion()
return table.concat({MAJOR, MINOR, PATCH}, ".")
end
return {
MAJOR = MAJOR,
MINOR = MINOR,
PATCH = PATCH,
getLongVersion = getLongVersion,
getVersion = getVersion
}
MAJOR = 0,
MINOR = 2,
PATCH = 2
}

View File

@ -1,3 +1,5 @@
local resx, resy = game.GetResolution()
local desw, desh = 1080,1920;
local scale = 1;

View File

@ -7,8 +7,7 @@ local difficultyLabelImages = {
gfx.CreateSkinImage("diff/5 infinite.png", 0),
gfx.CreateSkinImage("diff/6 gravity.png", 0),
gfx.CreateSkinImage("diff/7 heavenly.png", 0),
gfx.CreateSkinImage("diff/8 vivid.png", 0),
gfx.CreateSkinImage("diff/9 exceed.png", 0),
gfx.CreateSkinImage("diff/8 vivid.png", 0)
}
local difficultyLabelTexts = {
@ -16,20 +15,12 @@ local difficultyLabelTexts = {
"ADV",
"EXH",
"MXM",
"INF",
"GRV",
"HVN",
"VVD",
"EXC"
"VVD"
}
function render(deltatime, x, y, scale, diff, level)
local difficultyLabelImage = difficultyLabelImages[diff]
if difficultyLabelImage == nil then
game.Log("Unknown chart difficulty index "..diff..", fallback to MXM", game.LOGGER_WARNING)
difficultyLabelImage = difficultyLabelImages[4]
end
gfx.Save()
gfx.Translate(x,y);
gfx.Scale(scale,scale)
@ -38,7 +29,7 @@ function render(deltatime, x, y, scale, diff, level)
gfx.BeginPath();
gfx.ImageRect(0, 0, 140, 31 ,
difficultyLabelImages[diff] or
difficultyLabelImages[diff+1] or
difficultyLabelImages[4], 1, 0);
@ -51,7 +42,7 @@ function render(deltatime, x, y, scale, diff, level)
gfx.FontSize(22)
gfx.Scale(1.2,1); -- Make the diff text more W I D E
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_MIDDLE)
gfx.Text(difficultyLabelTexts[diff], 18, 17);
gfx.Text(difficultyLabelTexts[diff+1], 18, 17);
@ -69,4 +60,4 @@ end
return {
render = render
}
}

View File

@ -1,154 +1,91 @@
require("common.globals")
require("common.gameconfig")
local version = require("common.version")
local Dim = require("common.dimensions")
local Num = require("components.numbers")
local version = require('common.version')
local BAR_ALPHA = 191
local resx, resy = game.GetResolution()
local desw, desh = 1080,1920;
local scale = 1;
local BAR_ALPHA = 191;
local FOOTER_HEIGHT = 128
local footerY = Dim.design.height - FOOTER_HEIGHT
local footerY = desh - FOOTER_HEIGHT;
-- Images
local footerRightImage = gfx.CreateSkinImage("components/bars/footer_right.png", 0)
local timeImage = gfx.CreateSkinImage("components/bars/time.png", 0)
local creditImage = gfx.CreateSkinImage("components/bars/credit.png", 0)
local timeNumbers = Num.load_number_image("components/bars/time_num")
local timeColon = gfx.CreateSkinImage("components/bars/time_colon.png", 0)
local footerRightImage = gfx.CreateSkinImage("components/bars/footer_right.png", 0);
-- Animation related
local entryTransitionScale = 0
local entryTransitionFooterYOffset = 0
local entryTransitionScale = 0;
local entryTransitionFooterYOffset = 0;
local legend = {{control = "START", text = "Confirm selection"}, {control = "KNOB", text = "Scroll"}}
local legend = {
{
control = 'START',
text = 'Confirm selection'
},
{
control = 'KNOB',
text = 'Scroll'
},
}
local timeOut = tonumber(GameConfig["DemoIdleTime"]) or 0
local demoIdleTime = timeOut
local enableTimer = game.GetSkinSetting("songselect_enableTimer") or false
local freezeTimer = game.GetSkinSetting("songselect_freezeTimer") or -1
local function resetTimer()
timeOut = demoIdleTime or 0
local set = function ()
end
local function set(o)
o = o or {
enableTimer=game.GetSkinSetting("songselect_enableTimer") or false,
freezeTimer=game.GetSkinSetting("songselect_freezeTimer") or -1
}
enableTimer = o.enableTimer
freezeTimer = o.freezeTimer
function resetLayoutInformation()
resx, resy = game.GetResolution()
desw = 1080
desh = 1920
scale = resx / desw
end
local function drawTimer(time_s, show_minutes)
if show_minutes then
gfx.BeginPath()
local xpos, ypos = Dim.design.width - 500, footerY + 55
local w, h = gfx.ImageSize(timeImage)
gfx.ImageRect(xpos, ypos, w, h, timeImage, 1, 0)
local drawFooter = function ()
gfx.BeginPath();
gfx.FillColor(0,0,0,BAR_ALPHA);
gfx.Rect(0,footerY,desw, FOOTER_HEIGHT);
gfx.Fill();
gfx.BeginPath();
gfx.ImageRect(desw-275, footerY-25, 328*0.85, 188*0.85, footerRightImage, 1, 0);
-- Draw minutes:seconds display
local minutes, seconds = time_s // 60, time_s % 60
local tens, ones = math.floor(minutes // 10), math.floor(minutes % 10)
w, h = 90, 90
xpos, ypos = xpos + 55, footerY - 16
gfx.BeginPath()
gfx.ImageRect(xpos, ypos, w, h, timeNumbers[tens + 1], 1, 0)
gfx.BeginPath()
gfx.ImageRect(xpos + w, ypos, w, h, timeNumbers[ones + 1], 1, 0)
xpos = xpos + 2 * w
gfx.BeginPath()
gfx.ImageRect(xpos - 29, ypos, w, h, timeColon, 1, 0)
xpos = xpos + 32
tens, ones = math.floor(seconds // 10), math.floor(seconds % 10)
gfx.BeginPath()
gfx.ImageRect(xpos, ypos, w, h, timeNumbers[tens + 1], 1, 0)
gfx.BeginPath()
gfx.ImageRect(xpos + w, ypos, w, h, timeNumbers[ones + 1], 1, 0)
else
gfx.BeginPath()
local xpos, ypos = Dim.design.width - 270, footerY + 55
local w, h = gfx.ImageSize(timeImage)
gfx.ImageRect(xpos, ypos, w, h, timeImage, 1, 0)
-- Draw only seconds
local tens, ones = math.floor(time_s // 10), math.floor(time_s % 10)
w, h = 90, 90
xpos, ypos = xpos + 55, footerY - 16
gfx.BeginPath()
gfx.ImageRect(xpos, ypos, w, h, timeNumbers[tens + 1], 1, 0)
gfx.BeginPath()
gfx.ImageRect(xpos + w, ypos, w, h, timeNumbers[ones + 1], 1, 0)
end
end
local function drawFooter()
gfx.BeginPath()
gfx.FillColor(0, 0, 0, BAR_ALPHA)
gfx.Rect(0, footerY, Dim.design.width, FOOTER_HEIGHT)
gfx.Fill()
-- Timer
local showTimer = enableTimer and (freezeTimer ~= -1 or GameConfig["EventMode"] == "True")
if showTimer then
-- Show dynamic timer
if freezeTimer ~= -1 then
drawTimer(freezeTimer, freezeTimer > 59)
else
drawTimer(timeOut, demoIdleTime and demoIdleTime > 59)
end
gfx.BeginPath()
local w, h = gfx.ImageSize(creditImage)
gfx.ImageRect(Dim.design.width - 190, footerY + 95, w, h, creditImage, 1, 0)
else
-- Show static image
gfx.BeginPath()
gfx.ImageRect(Dim.design.width - 275, footerY - 25, 328 * 0.85, 188 * 0.85, footerRightImage, 1, 0)
end
-- Version String
gfx.BeginPath()
gfx.LoadSkinFont("Digital-Serial-Bold.ttf")
gfx.BeginPath();
gfx.LoadSkinFont("Digital-Serial-Bold.ttf");
gfx.FontSize(20)
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_TOP)
gfx.FillColor(255, 255, 255, 255)
gfx.Text("EXPERIMENTALGEAR " .. version.MAJOR .. "." .. version.MINOR .. "." .. version.PATCH .. "", 8, 1895)
gfx.FillColor(255, 255, 255, 255);
gfx.Text('EXPERIMENTALGEAR ' .. version.MAJOR .. '.' .. version.MINOR .. '.' .. version.PATCH .. '', 8, 1895);
end
local function progressTransitions(deltaTime)
entryTransitionScale = entryTransitionScale + deltaTime / 0.3
if (entryTransitionScale > 1) then entryTransitionScale = 1 end
local progressTransitions = function (deltaTime)
entryTransitionScale = entryTransitionScale + deltaTime / 0.3;
if (entryTransitionScale > 1) then
entryTransitionScale = 1;
end
entryTransitionFooterYOffset = FOOTER_HEIGHT * (1 - entryTransitionScale)
footerY = Dim.design.height - FOOTER_HEIGHT + entryTransitionFooterYOffset
timeOut = math.max(timeOut - deltaTime, 0)
entryTransitionFooterYOffset = FOOTER_HEIGHT*(1-entryTransitionScale)
footerY = desh-FOOTER_HEIGHT+entryTransitionFooterYOffset;
end
local function draw(deltaTime, params)
if params and params.noEnterTransition then
entryTransitionScale = 1
local draw = function (deltaTime, params)
if (params) then
if params.noEnterTransition then
entryTransitionScale = 1;
end
end
gfx.Save()
gfx.ResetTransform()
gfx.LoadSkinFont("NotoSans-Regular.ttf");
Dim.updateResolution()
Dim.transformToScreenSpace()
gfx.LoadSkinFont("NotoSans-Regular.ttf")
drawFooter()
progressTransitions(deltaTime)
drawFooter();
progressTransitions(deltaTime);
gfx.Restore()
end
return {set = set, draw = draw, resetTimer = resetTimer}
return {
set = set,
draw = draw
};

View File

@ -1,28 +1,34 @@
local Dim = require("common.dimensions")
local desw = 1080;
local desh = 1920;
local scale = 1;
local BAR_ALPHA = 191
local BAR_ALPHA = 191;
local HEADER_HEIGHT = 100
local headerY = 0;
local animationHeaderGlowScale = 0
local animationHeaderGlowAlpha = 0
local animationHeaderGlowScale = 0;
local animationHeaderGlowAlpha = 0;
-- Images
local headerTitleImage = gfx.CreateSkinImage("challenge_select/skill_analyzer.png", gfx.IMAGE_GENERATE_MIPMAPS)
local function drawHeader()
gfx.BeginPath()
gfx.FillColor(0, 0, 0, BAR_ALPHA)
gfx.Rect(0, 0, Dim.design.width, HEADER_HEIGHT)
gfx.Fill()
local drawHeader = function ()
gfx.BeginPath();
gfx.FillColor(0,0,0,BAR_ALPHA);
gfx.Rect(0,0,desw, HEADER_HEIGHT);
gfx.Fill();
gfx.ClosePath()
local headerImageWidth, headerImageHeight = gfx.ImageSize(headerTitleImage)
gfx.ImageRect((Dim.design.width - headerImageWidth) / 2, (HEADER_HEIGHT - headerImageHeight) / 2 - 12, -- asset png is not centered on the y axis
headerImageWidth, headerImageHeight, headerTitleImage, 1, 0)
gfx.ImageRect(
(desw - headerImageWidth) / 2, (HEADER_HEIGHT - headerImageHeight) / 2 - 12, -- asset png is not centered on the y axis
headerImageWidth, headerImageHeight,
headerTitleImage, 1, 0
)
end
local function progressTransitions(deltatime)
local progressTransitions = function (deltatime)
-- HEADER GLOW ANIMATION
if animationHeaderGlowScale < 1 then
animationHeaderGlowScale = animationHeaderGlowScale + deltatime / 1 -- transition should last for that time in seconds
@ -31,29 +37,24 @@ local function progressTransitions(deltatime)
end
if animationHeaderGlowScale < 0.5 then
animationHeaderGlowAlpha = animationHeaderGlowScale * 2
animationHeaderGlowAlpha = animationHeaderGlowScale * 2;
else
animationHeaderGlowAlpha = 1 - ((animationHeaderGlowScale - 0.5) * 2)
animationHeaderGlowAlpha = 1-((animationHeaderGlowScale-0.5) * 2);
end
animationHeaderGlowAlpha = animationHeaderGlowAlpha * 0.4
animationHeaderGlowAlpha = animationHeaderGlowAlpha*0.4
end
local function draw(deltatime)
local draw = function (deltatime)
gfx.Save()
gfx.LoadSkinFont("NotoSans-Regular.ttf");
gfx.ResetTransform()
Dim.updateResolution()
Dim.transformToScreenSpace()
gfx.LoadSkinFont("NotoSans-Regular.ttf")
drawHeader()
progressTransitions(deltatime)
drawHeader();
--progressTransitions(deltatime);
gfx.Restore()
end
return {draw = draw}
return {
draw = draw
};

View File

@ -1,28 +1,35 @@
local Dim = require("common.dimensions")
local BAR_ALPHA = 191
local resx, resy = game.GetResolution();
local desw = 1080;
local desh = 1920;
local scale = 1;
local BAR_ALPHA = 191;
local HEADER_HEIGHT = 100
local headerY = 0;
local animationHeaderGlowScale = 0
local animationHeaderGlowAlpha = 0
local animationHeaderGlowScale = 0;
local animationHeaderGlowAlpha = 0;
-- Images
local headerTitleImage = gfx.CreateSkinImage("song_select/header/title.png", 1)
local headerGlowTitleImage = gfx.CreateSkinImage("song_select/header/title_glow.png", 1)
local drawHeader = function()
gfx.BeginPath()
gfx.FillColor(0, 0, 0, BAR_ALPHA)
gfx.Rect(0, 0, Dim.design.width, HEADER_HEIGHT)
gfx.Fill()
local drawHeader = function ()
gfx.BeginPath();
gfx.FillColor(0,0,0,BAR_ALPHA);
gfx.Rect(0,0,desw, HEADER_HEIGHT);
gfx.Fill();
gfx.ClosePath()
gfx.ImageRect(42, 14, 423 * 0.85, 80 * 0.85, headerTitleImage, 1, 0)
gfx.ImageRect(42, 14, 423 * 0.85, 80 * 0.85, headerGlowTitleImage, animationHeaderGlowAlpha, 0)
gfx.ImageRect(42, 14, 423*0.85, 80*0.85, headerTitleImage, 1, 0)
gfx.ImageRect(42, 14, 423*0.85, 80*0.85, headerGlowTitleImage, animationHeaderGlowAlpha, 0)
end
local progressTransitions = function(deltatime)
local progressTransitions = function (deltatime)
-- HEADER GLOW ANIMATION
if animationHeaderGlowScale < 1 then
animationHeaderGlowScale = animationHeaderGlowScale + deltatime / 1 -- transition should last for that time in seconds
@ -31,29 +38,24 @@ local progressTransitions = function(deltatime)
end
if animationHeaderGlowScale < 0.5 then
animationHeaderGlowAlpha = animationHeaderGlowScale * 2
animationHeaderGlowAlpha = animationHeaderGlowScale * 2;
else
animationHeaderGlowAlpha = 1 - ((animationHeaderGlowScale - 0.5) * 2)
animationHeaderGlowAlpha = 1-((animationHeaderGlowScale-0.5) * 2);
end
animationHeaderGlowAlpha = animationHeaderGlowAlpha * 0.4
animationHeaderGlowAlpha = animationHeaderGlowAlpha*0.4
end
local draw = function(deltatime)
local draw = function (deltatime)
gfx.Save()
gfx.LoadSkinFont("NotoSans-Regular.ttf");
gfx.ResetTransform()
Dim.updateResolution()
Dim.transformToScreenSpace()
gfx.LoadSkinFont("NotoSans-Regular.ttf")
drawHeader()
progressTransitions(deltatime)
drawHeader();
progressTransitions(deltatime);
gfx.Restore()
end
return {draw = draw}
return {
draw = draw
};

View File

@ -1,68 +0,0 @@
require("common.class")
local Field = require("components.pager.field")
---@class ContainerField: Field
---@field content Field[]
local ContainerField = {
__tostring = function() return "ContainerField" end,
}
---Create a new ContainerField instance
---@param o? table # initial parameters
---@return ContainerField
function ContainerField.new(o)
o = o or {}
--set instance members
o.content = o.content or {}
local this = CreateInstance(ContainerField, o, Field)
this:refreshFields()
return this
end
---Add content to container
---@param field Field
function ContainerField:addField(field)
field.parent = self
table.insert(self.content, field)
end
---Refresh content parameters
function ContainerField:refreshFields()
for _, child in ipairs(self.content) do
child.parent = self
end
end
---@param deltaTime number # frametime in seconds
function ContainerField:drawBackground(deltaTime) end
---@param deltaTime number # frametime in seconds
function ContainerField:drawContent(deltaTime)
for _, child in ipairs(self.content) do
child:render(deltaTime)
end
end
---@param deltaTime number # frametime in seconds
function ContainerField:drawForeground(deltaTime) end
---@param deltaTime number # frametime in seconds
function ContainerField:render(deltaTime)
gfx.Save()
gfx.Translate(self.posX, self.posY)
gfx.Scissor(0, 0, self.aabbW, self.aabbH)
self:drawBackground(deltaTime)
self:drawContent(deltaTime)
self:drawForeground(deltaTime)
gfx.Restore()
end
return ContainerField

View File

@ -1,113 +0,0 @@
require("common.class")
---@class Field
---@field parent Page|ContainerField
---@field posX number
---@field posY number
---@field aabbW number
---@field aabbH number
local Field = {
__tostring = function() return "Field" end,
}
---Create a new Field instance
---@param o? table # initial parameters
---@return Field
function Field.new(o)
o = o or {}
--set instance members
o.parent = o.parent or nil
o.posX = o.posX or 0
o.posY = o.posY or 0
o.aabbW = o.aabbW or 0
o.aabbH = o.aabbH or 0
return CreateInstance(Field, o)
end
---Get the containing top-level parent page
---@return Field|Page
function Field:getParentPage()
if self.parent and self.parent.getParentPage then
return self.parent:getParentPage()
else
return self.parent
end
end
---@param obj? any # message object for the field
function Field:activate(obj) end
---@param obj? any # message object for the field
function Field:focus(obj) end
---@param obj? any # message object for the field
function Field:deactivate(obj) end
---@param button integer # options are under the `game` table prefixed with `BUTTON`
---@return boolean # true if further button input processing should be stopped, otherwise false
function Field:handleButtonInput(button)
return false
end
---@param knob integer # `0` = Left, `1` = Right
---@param delta number # in radians, `-2*pi` to `0` (turning CCW) and `0` to `2*pi` (turning CW)
---@return boolean # true if further button input processing should be stopped, otherwise false
function Field:handleKnobInput(knob, delta)
return false
end
---@param deltaTime number # frametime in seconds
function Field:drawContent(deltaTime)
-- dummy field content
gfx.ResetScissor()
local offX = -50
local offY = -50
local aabbW = 100
local aabbH = 100
gfx.BeginPath()
gfx.FillColor(255, 0, 128, 192)
gfx.StrokeColor(0, 0, 0)
gfx.StrokeWidth(2)
gfx.Rect(offX, offY, aabbW, aabbH)
gfx.Fill()
gfx.Stroke()
gfx.BeginPath()
gfx.MoveTo(offX, 0)
gfx.LineTo(offX + aabbW, 0)
gfx.MoveTo(0, offY)
gfx.LineTo(0, offY + aabbH)
gfx.StrokeColor(0, 0, 0, 64)
gfx.StrokeWidth(2)
gfx.Stroke()
local fontSize = 18
local fontMargin = 4
gfx.BeginPath()
gfx.FontSize(fontSize)
gfx.LoadSkinFont("dfmarugoth.ttf")
gfx.FillColor(0, 0, 0)
gfx.TextAlign(gfx.TEXT_ALIGN_CENTER | gfx.TEXT_ALIGN_MIDDLE)
gfx.Text("TEXTURE", 0, -fontSize / 2 - fontMargin)
gfx.Text("MISSING", 0, fontSize / 2 + fontMargin)
end
---@param deltaTime number # frametime in seconds
function Field:render(deltaTime)
gfx.Save()
gfx.Translate(self.posX, self.posY)
gfx.Scissor(0, 0, self.aabbW, self.aabbH)
self:drawContent(deltaTime)
gfx.Restore()
end
return Field

View File

@ -1,44 +0,0 @@
require("common.class")
local Field = require("components.pager.field")
---@class LinkField: Field
---@field link Page
local LinkField = {
__tostring = function() return "LinkField" end
}
---Create a new LinkField instance
---@param o? table # initial parameters
---@return LinkField
function LinkField.new(o)
o = o or {}
o.link = o.link or nil
return CreateInstance(LinkField, o, Field)
end
---@param button integer # options are under the `game` table prefixed with `BUTTON`
---@return boolean # true if further button input processing should be stopped, otherwise false
function LinkField:handleButtonInput(button)
if not self.link then
game.Log(tostring(self) .. " does not have a valid link", game.LOGGER_ERROR)
return false
end
if button == game.BUTTON_STA then
local parentPage = self:getParentPage()
if parentPage and parentPage.viewHandler then
game.Log(tostring(self) .. " viewHandler:navigate(" .. tostring(self.link) .. ") called", game.LOGGER_INFO)
parentPage.viewHandler:navigate(self.link)
return true
else
local target = (parentPage and parentPage.viewHandler or "PageView")
game.Log(tostring(self) .. " can't access " .. tostring(target) .. " instance", game.LOGGER_ERROR)
end
end
return false
end
return LinkField

View File

@ -1,72 +0,0 @@
require("common.globals")
require("common.class")
---@class Page
---@field content Field[]
---@field viewHandler nil|PageView
local Page = {
__tostring = function() return "Page" end,
}
---Create a new Page instance
---@param o? table # initial parameters
---@return Page
function Page.new(o)
o = o or {}
--set instance members
o.content = o.content or {}
o.viewHandler = o.viewHandler or nil
return CreateInstance(Page, o)
end
---Add field to page
---@param field Field
function Page:addField(field)
field.parent = self
table.insert(self.content, field)
end
---Refresh content values
function Page:refreshFields()
for _, field in ipairs(self.content) do
field.parent = self
end
end
---@param button integer # options are under the `game` table prefixed with `BUTTON`
function Page:handleButtonInput(button)
if button == game.BUTTON_BCK then
if self.viewHandler then
self.viewHandler:back()
end
end
end
---@param knob integer # `0` = Left, `1` = Right
---@param delta number # in radians, `-2*pi` to `0` (turning CCW) and `0` to `2*pi` (turning CW)
function Page:handleKnobInput(knob, delta) end
---@param deltaTime number # frametime in seconds
function Page:drawBackground(deltaTime) end
---@param deltaTime number # frametime in seconds
function Page:drawContent(deltaTime)
for _, child in ipairs(self.content) do
child:render(deltaTime)
end
end
---@param deltaTime number # frametime in seconds
function Page:drawForeground(deltaTime) end
---@param deltaTime number # frametime in seconds
function Page:render(deltaTime)
self:drawBackground(deltaTime)
self:drawContent(deltaTime)
self:drawForeground(deltaTime)
end
return Page

View File

@ -1,84 +0,0 @@
require("common.class")
---@class PageView
---@field pageStack Page[]
local PageView = {
__tostring = function() return "PageView" end
}
local function pushStack(t, o)
table.insert(t, 1, o)
end
local function popStack(t)
return table.remove(t, 1)
end
---Create a new PageView instance
---@param rootPage Page
---@return PageView
function PageView.new(rootPage)
local o = {}
--set viewHandler as this instance for rootPage
rootPage.viewHandler = o
--set instance members
o.pageStack = {}
pushStack(o.pageStack, rootPage)
return CreateInstance(PageView, o)
end
---Get page from pageStack
---@param index? integer # defaults to 1 (top of the stack)
---@return Page
function PageView:get(index)
index = index or 1
return self.pageStack[index]
end
---Navigate to page
---@param page Page # page to put on top of the pageStack
function PageView:navigate(page)
page.viewHandler = self
pushStack(self.pageStack, page)
end
---Replace the current pageStack with a new root page
---@param rootPage Page
function PageView:replace(rootPage)
self:clear()
self:navigate(rootPage)
end
---Navigate to the previous page
function PageView:back()
if not self:get() then
game.Log(self .. ":back() : pageStack empty, cannot go back", game.LOGGER_WARNING)
return
end
self:get().viewHandler = nil
popStack(self.pageStack)
end
---Clear the pageStack
function PageView:clear()
--clear pageStack
while self:get() do
self:back()
end
end
---@param deltaTime number # frametime in seconds
function PageView:render(deltaTime)
if self:get() then
self:get():render(deltaTime)
end
end
return PageView

View File

@ -1,619 +0,0 @@
--[[
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

@ -1,23 +0,0 @@
local Dim = require("common.dimensions")
local backgroundImage = gfx.CreateSkinImage("bg_pattern.png", gfx.IMAGE_REPEATX | gfx.IMAGE_REPEATY)
local bgImageWidth, bgImageHeight = gfx.ImageSize(backgroundImage)
local patternAngle = 0
local patternAlpha = 0.2
function render()
gfx.Save()
gfx.ResetTransform()
gfx.BeginPath()
gfx.Rect(0, 0, Dim.screen.width, Dim.screen.height)
gfx.FillPaint(gfx.ImagePattern(0, 0, bgImageWidth, bgImageHeight, patternAngle, backgroundImage, patternAlpha))
gfx.Fill()
gfx.Restore()
end
return {
render = render
}

View File

@ -1,429 +1,429 @@
json = require "common.json"
local header = {}
header["user-agent"] = "unnamed_sdvx_clone"
local jacketFallback = gfx.CreateSkinImage("song_select/loading.png", 0)
local diffColors = {{50,50,127}, {50,127,50}, {127,50,50}, {127, 50, 127}}
local entryW = 770
local entryH = 320
local resX,resY = game.GetResolution()
local xCount = math.max(1, math.floor(resX / entryW))
local yCount = math.max(1, math.floor(resY / entryH))
local xOffset = (resX - xCount * entryW) / 2
local cursorPos = 0
local cursorPosX = 0
local cursorPosY = 0
local displayCursorPosX = 0
local displayCursorPosY = 0
local nextUrl = "https://ksm.dev/app/songs"
local screenState = 0 --0 = normal, 1 = level, 2 = sorting
local loading = true
local downloaded = {}
local songs = {}
local selectedLevels = {}
local selectedSorting = "Uploaded"
local lastPlaying = nil
for i = 1, 20 do
selectedLevels[i] = false
end
local cachepath = path.Absolute("skins/" .. game.GetSkin() .. "/nautica.json")
local levelcursor = 0
local sortingcursor = 0
local sortingOptions = {"Uploaded", "Oldest"}
local needsReload = false
function addsong(song)
if song.jacket_url ~= nil then
song.jacket = gfx.LoadWebImageJob(song.jacket_url, jacketFallback, 250, 250)
else
song.jacket = jacketFallback
end
if downloaded[song.id] then
song.status = "Downloaded"
end
table.insert(songs, song)
end
local yOffset = 0
local backgroundImage = gfx.CreateSkinImage("bg.png", 1);
dlcache = io.open(cachepath, "r")
if dlcache then
downloaded = json.decode(dlcache:read("*all"))
dlcache:close()
end
function encodeURI(str)
if (str) then
str = string.gsub(str, "\n", "\r\n")
str = string.gsub(str, "([^%w ])",
function (c)
local dontChange = "-/_:."
for i = 1, #dontChange do
if c == dontChange:sub(i,i) then return c end
end
return string.format ("%%%02X", string.byte(c))
end)
str = string.gsub(str, " ", "%%20")
end
return str
end
function gotSongsCallback(response)
if response.status ~= 200 then
error()
return
end
local jsondata = json.decode(response.text)
for i,song in ipairs(jsondata.data) do
addsong(song)
end
nextUrl = jsondata.links.next
loading = false
end
Http.GetAsync(nextUrl, header, gotSongsCallback)
function render_song(song, x,y)
gfx.Save()
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_TOP)
gfx.Translate(x,y)
gfx.Scissor(0,0,750,300)
gfx.BeginPath()
gfx.FillColor(0,0,0,140)
gfx.Rect(0,0,750,300)
gfx.Fill()
gfx.FillColor(255,255,255)
gfx.FontSize(30)
gfx.Text(song.title, 2,2)
gfx.FontSize(24)
gfx.Text(song.artist, 2,26)
if song.jacket_url ~= nil and song.jacket == jacketFallback then
song.jacket = gfx.LoadWebImageJob(song.jacket_url, jacketFallback, 250, 250)
end
gfx.BeginPath()
gfx.ImageRect(0, 50, 250, 250, song.jacket, 1, 0)
gfx.BeginPath()
gfx.Rect(250,50,500,250)
gfx.FillColor(55,55,55,128)
gfx.Fill()
for i, diff in ipairs(song.charts) do
local col = diffColors[diff.difficulty]
local diffY = 50 + 250/4 * (diff.difficulty - 1)
gfx.BeginPath()
gfx.Rect(250,diffY, 500, 250 / 4)
gfx.FillColor(col[1], col[2], col[3])
gfx.Fill()
gfx.FillColor(255,255,255)
gfx.FontSize(40)
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_MIDDLE)
gfx.Text(string.format("%d Effected by %s", diff.level, diff.effector), 255, diffY + 250 / 8)
end
if downloaded[song.id] then
gfx.BeginPath()
gfx.Rect(0,0,750,300)
gfx.FillColor(0,0,0,127)
gfx.Fill()
gfx.TextAlign(gfx.TEXT_ALIGN_CENTER + gfx.TEXT_ALIGN_MIDDLE)
gfx.FontSize(60)
gfx.FillColor(255,255,255)
gfx.Text(downloaded[song.id], 375, 150)
elseif song.status then
gfx.BeginPath()
gfx.Rect(0,0,750,300)
gfx.FillColor(0,0,0,127)
gfx.Fill()
gfx.TextAlign(gfx.TEXT_ALIGN_CENTER + gfx.TEXT_ALIGN_MIDDLE)
gfx.FontSize(60)
gfx.FillColor(255,255,255)
gfx.Text(song.status, 375, 150)
end
gfx.ResetScissor()
gfx.Restore()
end
function load_more()
if nextUrl ~= nil and not loading then
Http.GetAsync(nextUrl, header, gotSongsCallback)
loading = true
end
end
function render_cursor()
local x = displayCursorPosX * entryW
local y = displayCursorPosY * entryH
gfx.BeginPath()
gfx.Rect(x,y,750,300)
gfx.StrokeColor(255,128,0)
gfx.StrokeWidth(5)
gfx.Stroke()
end
function render_loading()
if not loading then return end
gfx.Save()
gfx.ResetTransform()
gfx.BeginPath()
gfx.MoveTo(resX, resY)
gfx.LineTo(resX - 350, resY)
gfx.LineTo(resX - 300, resY - 50)
gfx.LineTo(resX, resY - 50)
gfx.ClosePath()
gfx.FillColor(33,33,33)
gfx.Fill()
gfx.FillColor(255,255,255)
gfx.TextAlign(gfx.TEXT_ALIGN_RIGHT, gfx.TEXT_ALIGN_BOTTOM)
gfx.FontSize(70)
gfx.Text("LOADING...", resX - 20, resY - 3)
gfx.Restore()
end
function render_hotkeys()
gfx.Save()
gfx.ResetTransform()
gfx.BeginPath()
gfx.FillColor(0,0,0,240)
gfx.Rect(0,resY - 50, resX, 50)
gfx.Fill()
gfx.FontSize(30)
gfx.FillColor(255,255,255)
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT, gfx.TEXT_ALIGN_BOTTOM)
gfx.Text("FXR: Sorting", resX/2 + 20, resY - 10)
gfx.TextAlign(gfx.TEXT_ALIGN_RIGHT, gfx.TEXT_ALIGN_BOTTOM)
gfx.Text("FXL: Levels", resX/2 - 20, resY - 10)
gfx.Restore()
end
function render_info()
gfx.Save()
gfx.ResetTransform()
gfx.BeginPath()
gfx.MoveTo(0, resY)
gfx.LineTo(350, resY)
gfx.LineTo(300, resY - 50)
gfx.LineTo(0, resY - 50)
gfx.ClosePath()
gfx.FillColor(33,33,33)
gfx.Fill()
gfx.FillColor(255,255,255)
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT, gfx.TEXT_ALIGN_BOTTOM)
gfx.FontSize(70)
gfx.Text("Nautica", 3, resY - 3)
local xmin,ymin,xmax,ymax = gfx.TextBounds(3, resY - 3, "Nautica")
gfx.FontSize(20)
gfx.Text("https://ksm.dev/", xmax + 13, resY - 3)
gfx.Restore()
end
function render(deltaTime)
gfx.BeginPath()
gfx.ImageRect(0, 0, resX, resY, backgroundImage, 1, 0);
gfx.LoadSkinFont("NotoSans-Regular.ttf");
displayCursorPosX = displayCursorPosX - (displayCursorPosX - cursorPosX) * deltaTime * 10
displayCursorPosY = displayCursorPosY - (displayCursorPosY - cursorPosY) * deltaTime * 10
if displayCursorPosY - yOffset > yCount - 1 then --scrolling down
yOffset = yOffset - (yOffset - displayCursorPosY) - yCount + 1
elseif displayCursorPosY - yOffset < 0 then
yOffset = yOffset - (yOffset - displayCursorPosY)
end
gfx.Translate(xOffset, 50 - yOffset * entryH)
for i, song in ipairs(songs) do
if math.abs(cursorPos - i) <= xCount * yCount + xCount then
i = i - 1
local x = entryW * (i % xCount)
local y = math.floor(i / xCount) * entryH
render_song(song, x, y)
if math.abs(#songs - i) < 4 then load_more() end
end
end
render_cursor()
if needsReload then reload_songs() end
if screenState == 1 then render_level_filters()
elseif screenState == 2 then render_sorting_selection()
end
render_hotkeys()
render_loading()
render_info()
end
function archive_callback(entries, id)
game.Log("Listing entries for " .. id, 0)
local songsfolder = dlScreen.GetSongsPath()
res = {}
folders = { songsfolder .. "/nautica/" }
local hasFolder = false
for i, entry in ipairs(entries) do
for j = 1, #entry do
if entry:sub(j,j) == '/' then
hasFolder = true
table.insert(folders, songsfolder .. "/nautica/" .. entry:sub(1,j))
end
end
game.Log(entry, 0)
res[entry] = songsfolder .. "/nautica/" .. entry
end
if not hasFolder then
for i, entry in ipairs(entries) do
res[entry] = songsfolder .. "/nautica/" .. id .. "/" .. entry
end
table.insert(folders, songsfolder .. "/nautica/" .. id .. "/")
end
downloaded[id] = "Downloaded"
res[".folders"] = table.concat(folders, "|")
return res
end
function reload_songs()
needsReload = true
if loading then return end
local useLevels = false
local levelarr = {}
for i,value in ipairs(selectedLevels) do
if value then
useLevels = true
table.insert(levelarr, i)
end
end
nextUrl = string.format("https://ksm.dev/app/songs?sort=%s", selectedSorting:lower())
if useLevels then
nextUrl = nextUrl .. "&levels=" .. table.concat(levelarr, ",")
end
songs = {}
cursorPos = 0
cursorPosX = 0
cursorPosY = 0
displayCursorPosX = 0
displayCursorPosY = 0
load_more()
game.Log(nextUrl, 0)
needsReload = false
end
function button_pressed(button)
if button == game.BUTTON_STA then
if screenState == 0 then
local song = songs[cursorPos + 1]
if song == nil then return end
dlScreen.DownloadArchive(encodeURI(song.cdn_download_url), header, song.id, archive_callback)
downloaded[song.id] = "Downloading..."
elseif screenState == 1 then
if selectedLevels[levelcursor + 1] then
selectedLevels[levelcursor + 1] = false
else
selectedLevels[levelcursor + 1] = true
end
reload_songs()
elseif screenState == 2 then
selectedSorting = sortingOptions[sortingcursor + 1]
reload_songs()
end
elseif button == game.BUTTON_BTA then
if screenState == 0 then
local song = songs[cursorPos + 1]
if song == nil then return end
dlScreen.PlayPreview(encodeURI(song.preview_url), header, song.id)
song.status = "Playing"
if lastPlaying ~=nil then
lastPlaying.status = nil
end
lastPlaying = song
end
elseif button == game.BUTTON_FXL then
if screenState ~= 1 then
screenState = 1
else
screenState = 0
end
elseif button == game.BUTTON_FXR then
if screenState ~= 2 then
screenState = 2
else
screenState = 0
end
end
end
function key_pressed(key)
if key == 27 then --escape pressed
dlcache = io.open(cachepath, "w")
dlcache:write(json.encode(downloaded))
dlcache:close()
dlScreen.Exit()
end
end
function advance_selection(steps)
if screenState == 0 and #songs > 0 then
cursorPos = (cursorPos + steps) % #songs
cursorPosX = cursorPos % xCount
cursorPosY = math.floor(cursorPos / xCount)
if cursorPos > #songs - 6 then
load_more()
end
elseif screenState == 1 then
levelcursor = (levelcursor + steps) % 20
elseif screenState == 2 then
sortingcursor = (sortingcursor + steps) % #sortingOptions
end
end
function render_level_filters()
gfx.Save()
gfx.ResetTransform()
gfx.BeginPath()
gfx.Rect(0,0, resX, resY)
gfx.FillColor(0,0,0,200)
gfx.Fill()
gfx.FillColor(255,255,255)
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_TOP)
gfx.FontSize(60)
gfx.Text("Level filters:", 10, 10)
gfx.BeginPath()
gfx.Rect(resX/2 - 30, resY/2 - 22, 60, 44)
gfx.StrokeColor(255,128,0)
gfx.StrokeWidth(2)
gfx.Stroke()
gfx.FontSize(40)
gfx.TextAlign(gfx.TEXT_ALIGN_CENTER + gfx.TEXT_ALIGN_MIDDLE)
for i = 1, 20 do
y = (resY/2) + (i - (levelcursor + 1)) * 40
if selectedLevels[i] then gfx.FillColor(255,255,255) else gfx.FillColor(127,127,127) end
gfx.Text(tostring(i), resX/2, y)
end
gfx.Restore()
end
function render_sorting_selection()
gfx.Save()
gfx.ResetTransform()
gfx.BeginPath()
gfx.Rect(0,0, resX, resY)
gfx.FillColor(0,0,0,200)
gfx.Fill()
gfx.FillColor(255,255,255)
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_TOP)
gfx.FontSize(60)
gfx.Text("Sorting method:", 10, 10)
gfx.BeginPath()
gfx.Rect(resX/2 - 75, resY/2 - 22, 150, 44)
gfx.StrokeColor(255,128,0)
gfx.StrokeWidth(2)
gfx.Stroke()
gfx.FontSize(40)
gfx.TextAlign(gfx.TEXT_ALIGN_CENTER + gfx.TEXT_ALIGN_MIDDLE)
for i, opt in ipairs(sortingOptions) do
y = (resY/2) + (i - (sortingcursor + 1)) * 40
if selectedSorting == opt then gfx.FillColor(255,255,255) else gfx.FillColor(127,127,127) end
gfx.Text(opt, resX/2, y)
end
gfx.Restore()
json = require "json"
local header = {}
header["user-agent"] = "unnamed_sdvx_clone"
local jacketFallback = gfx.CreateSkinImage("song_select/loading.png", 0)
local diffColors = {{50,50,127}, {50,127,50}, {127,50,50}, {127, 50, 127}}
local entryW = 770
local entryH = 320
local resX,resY = game.GetResolution()
local xCount = math.max(1, math.floor(resX / entryW))
local yCount = math.max(1, math.floor(resY / entryH))
local xOffset = (resX - xCount * entryW) / 2
local cursorPos = 0
local cursorPosX = 0
local cursorPosY = 0
local displayCursorPosX = 0
local displayCursorPosY = 0
local nextUrl = "https://ksm.dev/app/songs"
local screenState = 0 --0 = normal, 1 = level, 2 = sorting
local loading = true
local downloaded = {}
local songs = {}
local selectedLevels = {}
local selectedSorting = "Uploaded"
local lastPlaying = nil
for i = 1, 20 do
selectedLevels[i] = false
end
local cachepath = path.Absolute("skins/" .. game.GetSkin() .. "/nautica.json")
local levelcursor = 0
local sortingcursor = 0
local sortingOptions = {"Uploaded", "Oldest"}
local needsReload = false
function addsong(song)
if song.jacket_url ~= nil then
song.jacket = gfx.LoadWebImageJob(song.jacket_url, jacketFallback, 250, 250)
else
song.jacket = jacketFallback
end
if downloaded[song.id] then
song.status = "Downloaded"
end
table.insert(songs, song)
end
local yOffset = 0
local backgroundImage = gfx.CreateSkinImage("bg.png", 1);
dlcache = io.open(cachepath, "r")
if dlcache then
downloaded = json.decode(dlcache:read("*all"))
dlcache:close()
end
function encodeURI(str)
if (str) then
str = string.gsub(str, "\n", "\r\n")
str = string.gsub(str, "([^%w ])",
function (c)
local dontChange = "-/_:."
for i = 1, #dontChange do
if c == dontChange:sub(i,i) then return c end
end
return string.format ("%%%02X", string.byte(c))
end)
str = string.gsub(str, " ", "%%20")
end
return str
end
function gotSongsCallback(response)
if response.status ~= 200 then
error()
return
end
local jsondata = json.decode(response.text)
for i,song in ipairs(jsondata.data) do
addsong(song)
end
nextUrl = jsondata.links.next
loading = false
end
Http.GetAsync(nextUrl, header, gotSongsCallback)
function render_song(song, x,y)
gfx.Save()
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_TOP)
gfx.Translate(x,y)
gfx.Scissor(0,0,750,300)
gfx.BeginPath()
gfx.FillColor(0,0,0,140)
gfx.Rect(0,0,750,300)
gfx.Fill()
gfx.FillColor(255,255,255)
gfx.FontSize(30)
gfx.Text(song.title, 2,2)
gfx.FontSize(24)
gfx.Text(song.artist, 2,26)
if song.jacket_url ~= nil and song.jacket == jacketFallback then
song.jacket = gfx.LoadWebImageJob(song.jacket_url, jacketFallback, 250, 250)
end
gfx.BeginPath()
gfx.ImageRect(0, 50, 250, 250, song.jacket, 1, 0)
gfx.BeginPath()
gfx.Rect(250,50,500,250)
gfx.FillColor(55,55,55,128)
gfx.Fill()
for i, diff in ipairs(song.charts) do
local col = diffColors[diff.difficulty]
local diffY = 50 + 250/4 * (diff.difficulty - 1)
gfx.BeginPath()
gfx.Rect(250,diffY, 500, 250 / 4)
gfx.FillColor(col[1], col[2], col[3])
gfx.Fill()
gfx.FillColor(255,255,255)
gfx.FontSize(40)
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_MIDDLE)
gfx.Text(string.format("%d Effected by %s", diff.level, diff.effector), 255, diffY + 250 / 8)
end
if downloaded[song.id] then
gfx.BeginPath()
gfx.Rect(0,0,750,300)
gfx.FillColor(0,0,0,127)
gfx.Fill()
gfx.TextAlign(gfx.TEXT_ALIGN_CENTER + gfx.TEXT_ALIGN_MIDDLE)
gfx.FontSize(60)
gfx.FillColor(255,255,255)
gfx.Text(downloaded[song.id], 375, 150)
elseif song.status then
gfx.BeginPath()
gfx.Rect(0,0,750,300)
gfx.FillColor(0,0,0,127)
gfx.Fill()
gfx.TextAlign(gfx.TEXT_ALIGN_CENTER + gfx.TEXT_ALIGN_MIDDLE)
gfx.FontSize(60)
gfx.FillColor(255,255,255)
gfx.Text(song.status, 375, 150)
end
gfx.ResetScissor()
gfx.Restore()
end
function load_more()
if nextUrl ~= nil and not loading then
Http.GetAsync(nextUrl, header, gotSongsCallback)
loading = true
end
end
function render_cursor()
local x = displayCursorPosX * entryW
local y = displayCursorPosY * entryH
gfx.BeginPath()
gfx.Rect(x,y,750,300)
gfx.StrokeColor(255,128,0)
gfx.StrokeWidth(5)
gfx.Stroke()
end
function render_loading()
if not loading then return end
gfx.Save()
gfx.ResetTransform()
gfx.BeginPath()
gfx.MoveTo(resX, resY)
gfx.LineTo(resX - 350, resY)
gfx.LineTo(resX - 300, resY - 50)
gfx.LineTo(resX, resY - 50)
gfx.ClosePath()
gfx.FillColor(33,33,33)
gfx.Fill()
gfx.FillColor(255,255,255)
gfx.TextAlign(gfx.TEXT_ALIGN_RIGHT, gfx.TEXT_ALIGN_BOTTOM)
gfx.FontSize(70)
gfx.Text("LOADING...", resX - 20, resY - 3)
gfx.Restore()
end
function render_hotkeys()
gfx.Save()
gfx.ResetTransform()
gfx.BeginPath()
gfx.FillColor(0,0,0,240)
gfx.Rect(0,resY - 50, resX, 50)
gfx.Fill()
gfx.FontSize(30)
gfx.FillColor(255,255,255)
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT, gfx.TEXT_ALIGN_BOTTOM)
gfx.Text("FXR: Sorting", resX/2 + 20, resY - 10)
gfx.TextAlign(gfx.TEXT_ALIGN_RIGHT, gfx.TEXT_ALIGN_BOTTOM)
gfx.Text("FXL: Levels", resX/2 - 20, resY - 10)
gfx.Restore()
end
function render_info()
gfx.Save()
gfx.ResetTransform()
gfx.BeginPath()
gfx.MoveTo(0, resY)
gfx.LineTo(350, resY)
gfx.LineTo(300, resY - 50)
gfx.LineTo(0, resY - 50)
gfx.ClosePath()
gfx.FillColor(33,33,33)
gfx.Fill()
gfx.FillColor(255,255,255)
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT, gfx.TEXT_ALIGN_BOTTOM)
gfx.FontSize(70)
gfx.Text("Nautica", 3, resY - 3)
local xmin,ymin,xmax,ymax = gfx.TextBounds(3, resY - 3, "Nautica")
gfx.FontSize(20)
gfx.Text("https://ksm.dev/", xmax + 13, resY - 3)
gfx.Restore()
end
function render(deltaTime)
gfx.BeginPath()
gfx.ImageRect(0, 0, resX, resY, backgroundImage, 1, 0);
gfx.LoadSkinFont("NotoSans-Regular.ttf");
displayCursorPosX = displayCursorPosX - (displayCursorPosX - cursorPosX) * deltaTime * 10
displayCursorPosY = displayCursorPosY - (displayCursorPosY - cursorPosY) * deltaTime * 10
if displayCursorPosY - yOffset > yCount - 1 then --scrolling down
yOffset = yOffset - (yOffset - displayCursorPosY) - yCount + 1
elseif displayCursorPosY - yOffset < 0 then
yOffset = yOffset - (yOffset - displayCursorPosY)
end
gfx.Translate(xOffset, 50 - yOffset * entryH)
for i, song in ipairs(songs) do
if math.abs(cursorPos - i) <= xCount * yCount + xCount then
i = i - 1
local x = entryW * (i % xCount)
local y = math.floor(i / xCount) * entryH
render_song(song, x, y)
if math.abs(#songs - i) < 4 then load_more() end
end
end
render_cursor()
if needsReload then reload_songs() end
if screenState == 1 then render_level_filters()
elseif screenState == 2 then render_sorting_selection()
end
render_hotkeys()
render_loading()
render_info()
end
function archive_callback(entries, id)
game.Log("Listing entries for " .. id, 0)
local songsfolder = dlScreen.GetSongsPath()
res = {}
folders = { songsfolder .. "/nautica/" }
local hasFolder = false
for i, entry in ipairs(entries) do
for j = 1, #entry do
if entry:sub(j,j) == '/' then
hasFolder = true
table.insert(folders, songsfolder .. "/nautica/" .. entry:sub(1,j))
end
end
game.Log(entry, 0)
res[entry] = songsfolder .. "/nautica/" .. entry
end
if not hasFolder then
for i, entry in ipairs(entries) do
res[entry] = songsfolder .. "/nautica/" .. id .. "/" .. entry
end
table.insert(folders, songsfolder .. "/nautica/" .. id .. "/")
end
downloaded[id] = "Downloaded"
res[".folders"] = table.concat(folders, "|")
return res
end
function reload_songs()
needsReload = true
if loading then return end
local useLevels = false
local levelarr = {}
for i,value in ipairs(selectedLevels) do
if value then
useLevels = true
table.insert(levelarr, i)
end
end
nextUrl = string.format("https://ksm.dev/app/songs?sort=%s", selectedSorting:lower())
if useLevels then
nextUrl = nextUrl .. "&levels=" .. table.concat(levelarr, ",")
end
songs = {}
cursorPos = 0
cursorPosX = 0
cursorPosY = 0
displayCursorPosX = 0
displayCursorPosY = 0
load_more()
game.Log(nextUrl, 0)
needsReload = false
end
function button_pressed(button)
if button == game.BUTTON_STA then
if screenState == 0 then
local song = songs[cursorPos + 1]
if song == nil then return end
dlScreen.DownloadArchive(encodeURI(song.cdn_download_url), header, song.id, archive_callback)
downloaded[song.id] = "Downloading..."
elseif screenState == 1 then
if selectedLevels[levelcursor + 1] then
selectedLevels[levelcursor + 1] = false
else
selectedLevels[levelcursor + 1] = true
end
reload_songs()
elseif screenState == 2 then
selectedSorting = sortingOptions[sortingcursor + 1]
reload_songs()
end
elseif button == game.BUTTON_BTA then
if screenState == 0 then
local song = songs[cursorPos + 1]
if song == nil then return end
dlScreen.PlayPreview(encodeURI(song.preview_url), header, song.id)
song.status = "Playing"
if lastPlaying ~=nil then
lastPlaying.status = nil
end
lastPlaying = song
end
elseif button == game.BUTTON_FXL then
if screenState ~= 1 then
screenState = 1
else
screenState = 0
end
elseif button == game.BUTTON_FXR then
if screenState ~= 2 then
screenState = 2
else
screenState = 0
end
end
end
function key_pressed(key)
if key == 27 then --escape pressed
dlcache = io.open(cachepath, "w")
dlcache:write(json.encode(downloaded))
dlcache:close()
dlScreen.Exit()
end
end
function advance_selection(steps)
if screenState == 0 and #songs > 0 then
cursorPos = (cursorPos + steps) % #songs
cursorPosX = cursorPos % xCount
cursorPosY = math.floor(cursorPos / xCount)
if cursorPos > #songs - 6 then
load_more()
end
elseif screenState == 1 then
levelcursor = (levelcursor + steps) % 20
elseif screenState == 2 then
sortingcursor = (sortingcursor + steps) % #sortingOptions
end
end
function render_level_filters()
gfx.Save()
gfx.ResetTransform()
gfx.BeginPath()
gfx.Rect(0,0, resX, resY)
gfx.FillColor(0,0,0,200)
gfx.Fill()
gfx.FillColor(255,255,255)
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_TOP)
gfx.FontSize(60)
gfx.Text("Level filters:", 10, 10)
gfx.BeginPath()
gfx.Rect(resX/2 - 30, resY/2 - 22, 60, 44)
gfx.StrokeColor(255,128,0)
gfx.StrokeWidth(2)
gfx.Stroke()
gfx.FontSize(40)
gfx.TextAlign(gfx.TEXT_ALIGN_CENTER + gfx.TEXT_ALIGN_MIDDLE)
for i = 1, 20 do
y = (resY/2) + (i - (levelcursor + 1)) * 40
if selectedLevels[i] then gfx.FillColor(255,255,255) else gfx.FillColor(127,127,127) end
gfx.Text(tostring(i), resX/2, y)
end
gfx.Restore()
end
function render_sorting_selection()
gfx.Save()
gfx.ResetTransform()
gfx.BeginPath()
gfx.Rect(0,0, resX, resY)
gfx.FillColor(0,0,0,200)
gfx.Fill()
gfx.FillColor(255,255,255)
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_TOP)
gfx.FontSize(60)
gfx.Text("Sorting method:", 10, 10)
gfx.BeginPath()
gfx.Rect(resX/2 - 75, resY/2 - 22, 150, 44)
gfx.StrokeColor(255,128,0)
gfx.StrokeWidth(2)
gfx.Stroke()
gfx.FontSize(40)
gfx.TextAlign(gfx.TEXT_ALIGN_CENTER + gfx.TEXT_ALIGN_MIDDLE)
for i, opt in ipairs(sortingOptions) do
y = (resY/2) + (i - (sortingcursor + 1)) * 40
if selectedSorting == opt then gfx.FillColor(255,255,255) else gfx.FillColor(127,127,127) end
gfx.Text(opt, resX/2, y)
end
gfx.Restore()
end

View File

@ -1,11 +1,5 @@
local VolforceWindow = require('components.volforceWindow')
local Dimensions = require 'common.dimensions';
do
local resx, resy = game.GetResolution();
Dimensions.updateResolution(resx / resy);
end
local Banner = require('gameplay.banner')
local CritLine = require('gameplay.crit_line')
@ -17,14 +11,31 @@ local Gauge = require('gameplay.gauge')
local Chain = require('gameplay.chain')
local LaserAlert = require('gameplay.laser_alert')
local HitFX = require 'gameplay.hitfx'
local EarlyLate = require 'gameplay.earlylate'
local TrackEnd = require('gameplay.track_end')
local json = require("common.json")
local json = require "json"
local showHitAnims = true;
-- Window variables
local resX, resY
-- Aspect Ratios
local landscapeWidescreenRatio = 16 / 9
local landscapeStandardRatio = 4 / 3
local portraitWidescreenRatio = 9 / 16
-- Portrait sizes
local fullX, fullY
local desw = 1080
local desh = 1920
local resolutionChange = function(x, y)
resX = x
resY = y
fullX = portraitWidescreenRatio * y
fullY = y
game.Log('resX:' .. resX .. ' // resY:' .. resY .. ' // fullX:' .. fullX .. ' // fullY:' .. fullY, game.LOGGER_ERROR);
end
local users = nil
@ -33,8 +44,11 @@ local chain = 0;
local score = 0;
function render(deltaTime)
-- detect resolution change
local resx, resy = game.GetResolution();
Dimensions.updateResolution(resx / resy);
if resx ~= resX or resy ~= resY then
resolutionChange(resx, resy)
end
Banner.render(deltaTime, users, gameplay.user_id);
@ -60,26 +74,15 @@ function render(deltaTime)
Chain.render(deltaTime, gameplay.comboState, chain, gameplay.critLine.x, gameplay.critLine.y);
LaserAlert.render(deltaTime);
EarlyLate.render(deltaTime)
end
function render_crit_base(deltaTime)
local cl = gameplay.critLine
CritLine.renderBase(deltaTime, cl.x, cl.y, -cl.rotation);
Console.render(deltaTime, cl.x, cl.y, -cl.rotation);
CritLine.renderBase(deltaTime, gameplay.critLine.x, gameplay.critLine.y, -gameplay.critLine.rotation, gameplay.critLine.cursors);
Console.render(deltaTime, gameplay.critLine.x, gameplay.critLine.y, -gameplay.critLine.rotation);
end
function render_crit_overlay(deltaTime)
local cl = gameplay.critLine
local centerX = cl.x
local centerY = cl.y
local rot = -cl.rotation
HitFX.renderButtons(deltaTime, centerX, centerY, rot);
HitFX.renderLasers(deltaTime, centerX, centerY, rot, cl.cursors);
CritLine.renderOverlay(deltaTime, centerX, centerY, rot, cl.cursors, gameplay.laserActive)
end
function render_intro(deltaTime)
@ -100,9 +103,6 @@ end
function update_score(newScore)
score = newScore
if (score == 0) then
maxChain = 0;
end
end
function update_combo(newCombo)
@ -118,22 +118,11 @@ function near_hit(wasLate)
end
function button_hit(button, rating, delta)
if (showHitAnims) then
if (rating == 1) then
HitFX.TriggerAnimation("Near", button + 1)
elseif (rating == 2) then
HitFX.TriggerAnimation("Crit", button + 1)
end
end
if 0 < rating and rating < 3 then
EarlyLate.TriggerAnimation(rating, delta)
end
end
function laser_slam_hit(slamLength, startPos, endPost, index)
if (showHitAnims) then
end
end
function laser_alert(isRight)
@ -164,9 +153,9 @@ end
-- Update the users in the scoreboard
function score_callback(response)
if response.status ~= 200 then
error()
return
if response.status ~= 200 then
error()
return
end
local jsondata = json.decode(response.text)
users = {}

View File

@ -1,4 +1,4 @@
local Numbers = require('components.numbers')
local Numbers = require('common.numbers')
local chainLabel = gfx.CreateSkinImage("gameplay/chain/label.png", 0)

View File

@ -1,87 +1,45 @@
local Dimensions = require 'common.dimensions'
local consoleBaseImage = gfx.CreateSkinImage("gameplay/console/base.png", 0)
local CONSOLE_W = 1352;
local CONSOLE_H = 712;
local function createConsoleImage(name)
return gfx.CreateSkinImage("gameplay/console/"..name..".png", 0)
-- Similar to crit line transforms, since the console needs to follow the lane rotation
local setUpTransforms = function (x,y,rotation)
local resx, resy = game.GetResolution()
local desw = 1080
local desh = 1920
local scale = resx / desw
gfx.Translate(x, y)
gfx.Rotate(rotation)
gfx.Scale(scale,scale)
end
local function renderConsoleImage(image, alpha)
gfx.BeginPath();
gfx.ImageRect(
-CONSOLE_W/2,
-CONSOLE_H/2+350,
CONSOLE_W,
CONSOLE_H,
image,
alpha,
0
);
end
local consoleBaseImage = createConsoleImage("base")
local buttonGlowImages = {
[0] = createConsoleImage("glow_bta"),
createConsoleImage("glow_btb"),
createConsoleImage("glow_btc"),
createConsoleImage("glow_btd"),
createConsoleImage("glow_fxl"),
createConsoleImage("glow_fxr"),
};
local knobGlowImages = {
[0] = createConsoleImage("glow_voll"),
createConsoleImage("glow_volr"),
};
local lastKnobState = {
[0] = -1,
-1
};
local doFlash = true;
local render = function (deltaTime, critLineCenterX, critLineCenterY, critLineRotation)
local resx, resy = game.GetResolution();
if (resx > resy) then
return
end
Dimensions.setUpTransforms(
setUpTransforms(
critLineCenterX,
critLineCenterY,
critLineRotation
)
renderConsoleImage(consoleBaseImage, 1)
if doFlash then
for button=0,5 do
if game.GetButton(button) then
renderConsoleImage(buttonGlowImages[button], 0.75)
end
end
-- Knobs also work
-- commented out do to missing/incorrect textures
--[[
for knob=0,1 do
local state = game.GetKnob(knob)
if state ~= lastKnobState[knob] then
renderConsoleImage(knobGlowImages[knob], 1)
lastKnobState[knob] = state
end
end
]]
end
doFlash = not doFlash;
gfx.BeginPath();
gfx.ImageRect(
-CONSOLE_W/2,
-CONSOLE_H/2+350,
CONSOLE_W,
CONSOLE_H,
consoleBaseImage,
1,
0
);
end
return {

View File

@ -1,8 +1,4 @@
local Dimensions = require 'common.dimensions'
local blackGradientImage = gfx.CreateSkinImage('gameplay/crit_line/black_gradient.png', 0)
local baseImage = gfx.CreateSkinImage("gameplay/crit_line/base.png", 0)
local baseImageLandscape = gfx.CreateSkinImage("gameplay/crit_line/base_landscape.png", 0)
local textImage = gfx.CreateSkinImage("gameplay/crit_line/text.png", 0)
@ -18,54 +14,45 @@ local cursorGlowTopImages = {
gfx.CreateSkinImage("gameplay/crit_line/cursor_glow_top_right.png", 0),
}
local cursorGlowWhite = gfx.CreateSkinImage("gameplay/crit_line/cursor_glow_white.png", 0);
local cursorGlowColor = gfx.CreateSkinImage("gameplay/crit_line/cursor_glow_color.png", 0);
local cursorTailColor = gfx.CreateSkinImage("gameplay/crit_line/cursor_tail_color.png", 0)
local cursorTailImages = {
gfx.CreateSkinImage("gameplay/crit_line/cursor_tail_l.png", 0),
gfx.CreateSkinImage("gameplay/crit_line/cursor_tail_r.png", 0),
}
local CRITBAR_W = 1496
local CRITBAR_H = 348
local CRITBAR_W = 1080 * 1.4
local CRITBAR_H = 251 * 1.4
local scale;
local isLandscape = false;
local drawCursors = function (scale, cursors, laserActive)
local cursorW = 598 * 0.165;
local cursorH = 673 * 0.14;
local setUpTransforms = function (x,y,rotation)
local resx, resy = game.GetResolution();
isLandscape = resx > resy;
local tailW = cursorW * 9
local tailH = cursorH * 9
local desw, desh;
if (isLandscape) then
desw = 1920;
desh = 1080;
else
desw = 1080;
desh = 1920;
end
scale = resx / desw
gfx.Translate(x, y)
gfx.Rotate(rotation)
gfx.Scale(scale,scale)
end
local drawCursors = function (centerX, centerY,cursors)
local cursorW = 598*0.2;
local cursorH = 673*0.2;
for i = 0, 1, 1 do
local luaIndex = i + 1
local cursor = cursors[i];
local r, g, b = game.GetLaserColor(i);
gfx.Save();
local cursor = cursors[i];
gfx.BeginPath();
gfx.SkewX(cursor.skew)
local skew = cursor.pos * 0.001;
gfx.SkewX(skew);
local cursorPos = cursor.pos * (1 / scale)
local cursorX = cursorPos - cursorW / 2;
local cursorY = -cursorH / 2;
gfx.SetImageTint(r, g, b);
gfx.ImageRect(
cursorPos - tailW / 2,
- tailH / 2,
tailW,
tailH,
cursorTailColor,
cursor.alpha / 2,
0
)
local glowAlpha = cursor.alpha;
if (i == 1) then glowAlpha = glowAlpha * 0.7; end
local cursorX = (cursor.pos *(1/scale) - cursorW/2);
local cursorY = (-cursorH/2);
gfx.ImageRect(
cursorX,
@ -82,23 +69,11 @@ local drawCursors = function (scale, cursors, laserActive)
cursorY,
cursorW,
cursorH,
cursorGlowWhite,
glowAlpha,
cursorGlowBottomImages[i+1],
cursor.alpha,
0
);
gfx.SetImageTint(r, g, b);
gfx.ImageRect(
cursorX,
cursorY,
cursorW,
cursorH,
cursorGlowColor,
glowAlpha,
0
);
gfx.SetImageTint(255, 255, 255);
gfx.ImageRect(
cursorX,
cursorY,
@ -114,55 +89,40 @@ local drawCursors = function (scale, cursors, laserActive)
cursorY,
cursorW,
cursorH,
cursorGlowWhite,
glowAlpha,
cursorGlowTopImages[i+1],
cursor.alpha,
0
);
gfx.SetImageTint(r, g, b);
gfx.ImageRect(
cursorX,
cursorY,
cursorW,
cursorH,
cursorGlowColor,
glowAlpha,
0
);
gfx.SetImageTint(255, 255, 255);
gfx.Restore();
end
end
local renderBase = function (deltaTime, centerX, centerY, rotation)
_, isLandscape = Dimensions.setUpTransforms(centerX, centerY, rotation)
local renderBase = function (deltaTime, centerX, centerY, rotation, cursors)
setUpTransforms(centerX, centerY, rotation)
gfx.BeginPath()
gfx.FillColor(0, 0, 0, 192)
gfx.Rect(-9999, 0, 9999 * 2, 1080)
gfx.Rect(-1080/2, 0, 1080, 1080)
gfx.Fill()
gfx.BeginPath();
if (isLandscape) then
gfx.BeginPath();
gfx.ImageRect(-9999, -CRITBAR_H/2, 9999 * 2, CRITBAR_H, baseImageLandscape, 1, 0);
gfx.ImageRect(-CRITBAR_W/2, -CRITBAR_H/2, CRITBAR_W, CRITBAR_H, baseImageLandscape, 1, 0);
else
gfx.BeginPath();
gfx.ImageRect(-CRITBAR_W/2, -CRITBAR_H/2, CRITBAR_W, CRITBAR_H, baseImage, 1, 0);
end
drawCursors(centerX, centerY, cursors)
gfx.ResetTransform()
end
local renderOverlay = function (deltaTime, centerX, centerY, rotation, cursors, laserActive)
scale, _ = Dimensions.setUpTransforms(centerX, centerY, rotation)
drawCursors(scale, cursors, laserActive)
gfx.ResetTransform()
local renderOverlay = function (deltaTime)
end
return {
renderBase=renderBase,
renderOverlay=renderOverlay
}
}

View File

@ -1,109 +1,30 @@
local Dimensions = require "common.dimensions"
-- Used for comparing button_hit()'s delta parameter with the
-- gameplay_earlyLateFor/gameplay_msDisplay skin settings values.
-- If the number is <= delta then the EarlyLate/ms should be shown
local compare = {
["ALL"] = 2,
["CRITICAL (or worse)"] = 2,
["NEAR (or worse)"] = 1,
["NONE"] = -1,
["OFF"] = -1
}
local desw = 1080;
local desh = 1920;
local portraitHeightFractions = {
["UPPER+"] = 2.4,
["UPPER"] = 3,
["STANDARD"] = 4.2,
["LOWER"] = 5.3,
}
local transitionExistScale = 0;
local landscapeHeightFractions = {
["UPPER+"] = 1.5,
["UPPER"] = 2.7,
["STANDARD"] = 4.1,
["LOWER"] = 6.7,
}
local earlyLateFor = compare[game.GetSkinSetting("gameplay_earlyLateFor")]
local msFor = compare[game.GetSkinSetting("gameplay_msFor")]
local earlyLatePosition = game.GetSkinSetting("gameplay_earlyLatePosition")
local EarlyLate = {
timer = 0,
color = {},
earlyLateText = "",
millisecText = ""
}
function EarlyLate.render(deltaTime)
if EarlyLate.timer <= 0 then
return
end
EarlyLate.timer = EarlyLate.timer - deltaTime * 100
local screenW, screenH = Dimensions.screen.width, Dimensions.screen.height
local screenCenterX = screenW / 2
local desh, fractionTable
if screenH > screenW then
desh = 1600
fractionTable = portraitHeightFractions
else
desh = 1080
fractionTable = landscapeHeightFractions
end
local scale = screenH / desh
local y = screenH / 8 * fractionTable[earlyLatePosition]
gfx.BeginPath()
gfx.LoadSkinFont("Digital-Serial-ExtraBold.ttf")
gfx.FontSize(20 * scale)
local color = EarlyLate.color
gfx.FillColor(color[1], color[2], color[3])
local tickTransitions = function (deltaTime)
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_BASELINE)
gfx.FastText(EarlyLate.earlyLateText, screenCenterX - 100 * scale, y)
gfx.FastText(EarlyLate.millisecText, screenCenterX + 100 * scale, y)
if transitionExistScale < 1 then
transitionExistScale = transitionExistScale + deltaTime / 2 -- transition should last for that time in seconds
end
end
function EarlyLate.TriggerAnimation(rating, millisec)
local showEarlyLate = rating <= earlyLateFor
local showMillisec = rating <= msFor
local isEarly = millisec < 0
local render = function (deltaTime, comboState, combo, critLineCenterX, critLineCenterY)
tickTransitions(deltaTime)
if millisec == 0 then return end
if not showEarlyLate and not showMillisec then return end
if showEarlyLate then
EarlyLate.earlyLateText = isEarly and "EARLY" or "LATE"
else
EarlyLate.earlyLateText = ""
if (transitionExistScale >= 1) then
return;
end
if showMillisec then
local millisecText = string.format("%dms", millisec)
-- prepend + sign for lates
millisecText = isEarly and millisecText or "+"..millisecText
EarlyLate.millisecText = millisecText
else
EarlyLate.millisecText = ""
end
if isEarly then
EarlyLate.color = {206, 94, 135}
else
EarlyLate.color = {53, 102, 197}
end
EarlyLate.timer = 120
end
return EarlyLate
local trigger = function ()
end
return {
render=render
}

View File

@ -1,245 +0,0 @@
require 'common.globals'
local Dimensions = require 'common.dimensions'
local Animation = require 'api.animation'
local Animations = {
Crit = Animation.new('gameplay/hit_animation_frames/critical_taps', {
centered = true,
}),
Near = Animation.new('gameplay/hit_animation_frames/near_taps', {
centered = true,
}),
HoldCrit = Animation.new('gameplay/hit_animation_frames/hold_critical', {
centered = true,
loop = true,
}),
HoldDome = Animation.new('gameplay/hit_animation_frames/hold_dome', {
centered = true,
loop = true,
loopPoint = 10
}),
HoldEnd = Animation.new('gameplay/hit_animation_frames/hold_end', {
centered = true,
}),
HoldInner = Animation.new('gameplay/hit_animation_frames/hold_inner', {
centered = true,
loop = true,
}),
LaserCrit = Animation.new('gameplay/hit_animation_frames/laser_critical', {
loop = true,
}),
LaserDome = Animation.new('gameplay/hit_animation_frames/laser_dome', {
loop = true,
}),
LaserEndOuter = Animation.new('gameplay/hit_animation_frames/laser_end_outer', {}),
LaserEndLeft = Animation.new('gameplay/hit_animation_frames/laser_end_l_inner', {}),
LaserEndRight = Animation.new('gameplay/hit_animation_frames/laser_end_r_inner', {}),
};
---@class LaserStateTable
---@field Crit AnimationState
---@field Dome AnimationState
---@field EndInner AnimationState
---@field EndOuter AnimationState
---@type LaserStateTable[]
local laserStateTables = {
{
Crit = Animations.LaserCrit:createState(),
Dome = Animations.LaserDome:createState(),
EndInner = Animations.LaserEndLeft:createState(),
EndOuter = Animations.LaserEndOuter:createState()
},
{
Crit = Animations.LaserCrit:createState(),
Dome = Animations.LaserDome:createState(),
EndInner = Animations.LaserEndRight:createState(),
EndOuter = Animations.LaserEndOuter:createState()
}
}
---@class HoldStateTable
---@field Crit AnimationState
---@field Dome AnimationState
---@field End AnimationState
---@field Inner AnimationState
---@type HoldStateTable[]
local holdStateTables = {}
for i = 1, 6 do
holdStateTables[i] = {
Crit = Animations.HoldCrit:createState(),
Dome = Animations.HoldDome:createState(),
End = Animations.HoldEnd:createState(),
Inner = Animations.HoldInner:createState()
}
end
---@type AnimationState[]
local tapStates = {}
local HitFX = { };
local function setUpTransform(critCenterX, critCenterY, critRotation, xScalar)
local critLine = gameplay.critLine
local x = critCenterX + (critLine.line.x2 - critLine.line.x1) * xScalar
local y = critCenterY + (critLine.line.y2 - critLine.line.y1) * xScalar
Dimensions.setUpTransforms(x, y, critRotation)
end
function HitFX.renderLasers(deltaTime, critCenterX, critCenterY, critRotation, cursors)
local hitSize = 406
-- Lasers
for laser = 1, 2 do
-- Update
local isActive = gameplay.laserActive[laser]
local laserState = laserStateTables[laser]
local isAnimationPlaying = laserState.Dome.running
if isActive and not isAnimationPlaying then
laserState.Crit:restart()
laserState.Dome:restart()
end
if not isActive and isAnimationPlaying then
laserState.Crit:stop()
laserState.Dome:stop()
laserState.EndInner:restart()
laserState.EndOuter:restart()
end
-- Render
scale, _ = Dimensions.setUpTransforms(critCenterX, critCenterY, critRotation)
local laserColor = {game.GetLaserColor(laser - 1)}
local x = cursors[laser - 1].pos * (1 / scale)
laserState.Dome:render(deltaTime, {
centered = true,
width = hitSize,
height = hitSize,
color = laserColor,
x = x,
})
laserState.Crit:render(deltaTime, {
centered = true,
width = hitSize,
height = hitSize,
x = x,
})
laserState.EndInner:render(deltaTime, {
centered = true,
width = hitSize,
height = hitSize,
x = x,
})
laserState.EndOuter:render(deltaTime, {
centered = true,
width = hitSize,
height = hitSize,
color = laserColor,
x = x,
})
end
end
function HitFX.renderButtons(deltaTime, critCenterX, critCenterY, critRotation)
--local baseHitSize = 325;
local hitSize = 406
-- BT + FX
for i = 1, 6 do
--[[
local hitSize = baseHitSize;
if (i > 4) then
hitSize = hitSize * 1.5;
end
]]
local laneWidth = (track.GetCurrentLaneXPos(2) - track.GetCurrentLaneXPos(1)) * (i <= 4 and 1 or 2);
local lanePosition = track.GetCurrentLaneXPos(i) + laneWidth / 2
if (i == 5) then
lanePosition = -track.GetCurrentLaneXPos(6) - laneWidth / 2
end
-- Update Holds
local isHeld = gameplay.noteHeld[i]
local holdStates = holdStateTables[i]
local isAnimationPlaying = holdStates.Dome.running
if isHeld and not isAnimationPlaying then
holdStates.Crit:restart()
holdStates.Dome:restart()
holdStates.Inner:restart()
end
if not isHeld and isAnimationPlaying then
holdStates.Crit:stop()
holdStates.Dome:stop()
holdStates.Inner:stop()
holdStates.End:restart()
end
-- Render holds
setUpTransform(critCenterX, critCenterY, critRotation, lanePosition)
holdStates.Inner:render(deltaTime, {
centered = true,
width = hitSize,
height = hitSize
})
holdStates.Dome:render(deltaTime, {
centered = true,
width = hitSize,
height = hitSize
})
holdStates.Crit:render(deltaTime, {
centered = true,
width = hitSize,
height = hitSize
})
holdStates.End:render(deltaTime, {
centered = true,
width = hitSize,
height = hitSize
})
-- Render Taps
local tapState = tapStates[i]
if tapState then
tapState:render(deltaTime, {
centered = true,
width = hitSize,
height = hitSize,
});
end
end
gfx.ResetTransform()
end
function HitFX.TriggerAnimation(name, lane)
tapStates[lane] = Animations[name]:start();
end
return HitFX;

View File

@ -1,4 +1,4 @@
local Numbers = require('components.numbers')
local Numbers = require('common.numbers')
local bgImage = gfx.CreateSkinImage("gameplay/score_panel/bg.png", 0)

View File

@ -1,5 +1,4 @@
local Charting = require('common.charting');
local DiffRectangle = require('components.diff_rectangle');
local desw = 1080;
@ -99,8 +98,7 @@ local render = function (deltaTime, bpm, laneSpeed, jacketPath, diff, level, pro
);
-- Draw diff rectangle
local adjustedDiff = Charting.GetDisplayDifficulty(gameplay.jacketPath, diff)
DiffRectangle.render(deltaTime, 31, y+140, 0.84, adjustedDiff, level);
DiffRectangle.render(deltaTime, 31, y+140, 0.84, diff, level);
gfx.FontSize(30);
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_MIDDLE)

View File

@ -1,4 +1,4 @@
local Common = require('common.util')
local Common = require('common.common')
local Easing = require('common.easing')
local bgImage = gfx.CreateSkinImage("gameplay/track_end/bg.png", 0)

View File

@ -1,4 +1,4 @@
require("common.gameconfig")
local VolforceWindow = require('components.volforceWindow');
local desw = 1080;
@ -13,9 +13,7 @@ local danBadgeImage = gfx.CreateSkinImage("dan.png", 0);
local idolFrameImage = gfx.CreateSkinImage("crew/frame.png", 0);
-- gameplay table does not have a current username field, because why would it lmao
-- workaround: retrieve it directly from Main.cfg file
local username = GameConfig["MultiplayerUsername"] or game.GetSkinSetting('username') or '';
local username = game.GetSkinSetting('username') or '';
local drawBestDiff = function (deltaTime, score, bestReplay, y)
if not bestReplay then return end
@ -140,4 +138,4 @@ end
return {
render=render
}
}

1016
scripts/gameplay_old.lua Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,251 +1,247 @@
function clamp(x, min, max)
if x < min then
x = min
end
if x > max then
x = max
end
return x
end
function smootherstep(edge0, edge1, x)
-- Scale, and clamp x to 0..1 range
x = clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0)
-- Evaluate polynomial
return x * x * x * (x * (x * 6 - 15) + 10)
end
function to_range(val, start, stop)
return start + (stop - start) * val
end
Animation = {
start = 0,
stop = 0,
progress = 0,
duration = 1,
smoothStart = false
}
function Animation:new(o)
o = o or {}
setmetatable(o, self)
self.__index = self
return o
end
function Animation:restart(start, stop, duration)
self.progress = 0
self.start = start
self.stop = stop
self.duration = duration
end
function Animation:tick(deltaTime)
self.progress = math.min(1, self.progress + deltaTime / self.duration)
if self.progress == 1 then return self.stop end
if self.smoothStart then
return to_range(smootherstep(0, 1, self.progress), self.start, self.stop)
else
return to_range(smootherstep(-1, 1, self.progress) * 2 - 1, self.start, self.stop)
end
end
local yScale = Animation:new()
local diagWidth = 600
local diagHeight = 400
local tabStroke = {start=0, stop=1}
local tabStrokeAnimation = {start=Animation:new(), stop=Animation:new()}
local settingsStrokeAnimation = {x=Animation:new(), y=Animation:new()}
local prevTab = -1
local prevSettingStroke = {x=0, y=0}
local settingStroke = {x=0, y=0}
local prevVis = false
function processSkinSettings()
for ti, tab in ipairs(SettingsDiag.tabs) do
for si, setting in ipairs(tab.settings) do
if (tab.name == 'Game') then
if (setting.name == 'Gauge') then
game.SetSkinSetting('_gaugeType', setting.value);
end
if (setting.name == 'Backup Gauge') then
game.SetSkinSetting('_gaugeARS', setting.value and 1 or 0);
end
end
end
end
end
function render(deltaTime, visible)
if visible and not prevVis then
yScale:restart(0, 1, 0.25)
elseif not visible and prevVis then
yScale:restart(1, 0, 0.25)
end
processSkinSettings()
if not visible and yScale:tick(0) < 0.05 then return end
local posX = SettingsDiag.posX or 0.5
local posY = SettingsDiag.posY or 0.5
local message_1 = "Press both FXs to open/close. Use the Start button to press buttons."
local message_2 = "Use FX keys to navigate tabs. Use arrow keys to navigate and modify settings."
gfx.Save()
resX, resY = game.GetResolution()
local scale = resY / 1080
gfx.ResetScissor()
gfx.ResetTransform()
gfx.Translate(math.floor(diagWidth/2 + posX*(resX-diagWidth)), math.floor(diagHeight/2 + posY*(resY-diagHeight)))
gfx.Scale(scale, scale)
gfx.Scale(1.0, smootherstep(0, 1, yScale:tick(deltaTime)))
gfx.BeginPath()
gfx.Rect(-diagWidth/2, -diagHeight/2, diagWidth, diagHeight)
gfx.FillColor(50,50,50)
gfx.Fill()
gfx.FillColor(255,255,255)
gfx.FontSize(20)
local m_xmin, m_ymin, m_xmax, m_ymax = gfx.TextBounds(0, 0, message_1)
gfx.Text(message_1, diagWidth/2 - m_xmax, diagHeight/2 - m_ymax - 20)
m_xmin, m_ymin, m_xmax, m_ymax = gfx.TextBounds(0, 0, message_2)
gfx.Text(message_2, diagWidth/2 - m_xmax, diagHeight/2 - m_ymax)
tabStroke.start = tabStrokeAnimation.start:tick(deltaTime)
tabStroke.stop = tabStrokeAnimation.stop:tick(deltaTime)
settingStroke.x = settingsStrokeAnimation.x:tick(deltaTime)
settingStroke.y = settingsStrokeAnimation.y:tick(deltaTime)
local tabBarHeight = 0
local nextTabX = 5
gfx.TextAlign(gfx.TEXT_ALIGN_TOP + gfx.TEXT_ALIGN_LEFT)
gfx.FontSize(35)
gfx.Save() --draw tab bar
gfx.Translate(-diagWidth / 2, -diagHeight / 2)
for ti, tab in ipairs(SettingsDiag.tabs) do
local xmin,ymin, xmax,ymax = gfx.TextBounds(nextTabX, 5, tab.name)
if ti == SettingsDiag.currentTab and SettingsDiag.currentTab ~= prevTab then
tabStrokeAnimation.start:restart(tabStroke.start, nextTabX, 0.1)
tabStrokeAnimation.stop:restart(tabStroke.stop, xmax, 0.1)
end
tabBarHeight = math.max(tabBarHeight, ymax + 5)
gfx.Text(tab.name, nextTabX, 5)
nextTabX = xmax + 10
end
gfx.BeginPath()
gfx.MoveTo(0, tabBarHeight)
gfx.LineTo(diagWidth, tabBarHeight)
gfx.StrokeWidth(2)
gfx.StrokeColor(0,127,255)
gfx.Stroke()
gfx.BeginPath()
gfx.MoveTo(tabStroke.start, tabBarHeight)
gfx.LineTo(tabStroke.stop, tabBarHeight)
gfx.StrokeColor(255, 127, 0)
gfx.Stroke()
gfx.Restore() --draw tab bar end
gfx.FontSize(30)
gfx.Save() --draw current tab
gfx.Translate(-diagWidth / 2, -diagHeight / 2)
gfx.Translate(5, tabBarHeight + 5)
gfx.BeginPath()
gfx.MoveTo(0, settingStroke.y)
gfx.LineTo(settingStroke.x, settingStroke.y)
gfx.StrokeWidth(2)
gfx.StrokeColor(255, 127, 0)
gfx.Stroke()
local settingHeight = 30
local tab = SettingsDiag.tabs[SettingsDiag.currentTab]
for si, setting in ipairs(tab.settings) do
processSkinSettings(setting.name, setting.value)
local disp = ""
if setting.type == "enum" then
disp = string.format("%s: %s", setting.name, setting.options[setting.value])
elseif setting.type == "int" then
disp = string.format("%s: %d", setting.name, setting.value)
elseif setting.type == "float" then
disp = string.format("%s: %.2f", setting.name, setting.value)
if setting.max == 1 and setting.min == 0 then --draw slider
disp = setting.name .. ": "
local xmin,ymin, xmax,ymax = gfx.TextBounds(0, 0, disp)
local width = diagWidth - 20 - xmax
gfx.BeginPath()
gfx.MoveTo(xmax + 5, 20)
gfx.LineTo(xmax + 5 + width, 20)
gfx.StrokeColor(0,127,255)
gfx.StrokeWidth(2)
gfx.Stroke()
gfx.BeginPath()
gfx.MoveTo(xmax + 5, 20)
gfx.LineTo(xmax + 5 + width * setting.value, 20)
gfx.StrokeColor(255,127,0)
gfx.StrokeWidth(2)
gfx.Stroke()
end
elseif setting.type == "button" then
disp = string.format("%s", setting.name)
local xmin, ymin, xmax,ymax = gfx.TextBounds(0, 0, disp)
gfx.BeginPath()
gfx.Rect(-2, 3, 4+xmax-xmin, 28)
gfx.FillColor(0, 64, 128)
if si == SettingsDiag.currentSetting then
gfx.StrokeColor(255, 127, 0)
else
gfx.StrokeColor(0,127,255)
end
gfx.StrokeWidth(2)
gfx.Fill()
gfx.Stroke()
gfx.FillColor(255,255,255)
else
disp = string.format("%s:", setting.name)
local xmin,ymin, xmax,ymax = gfx.TextBounds(0, 0, disp)
gfx.BeginPath()
gfx.Rect(xmax + 5, 5, 20,20)
gfx.FillColor(255, 127, 0, setting.value and 255 or 0)
gfx.StrokeColor(0,127,255)
gfx.StrokeWidth(2)
gfx.Fill()
gfx.Stroke()
gfx.FillColor(255,255,255)
end
gfx.Text(disp, 0 ,0)
if si == SettingsDiag.currentSetting then
local setting_name = setting.name .. ":"
if setting.type == "button" then
setting_name = setting.name
end
local xmin,ymin, xmax,ymax = gfx.TextBounds(0, 0, setting_name)
ymax = ymax + settingHeight * (si - 1)
if xmax ~= prevSettingStroke.x or ymax ~= prevSettingStroke.y then
settingsStrokeAnimation.x:restart(settingStroke.x, xmax, 0.1)
settingsStrokeAnimation.y:restart(settingStroke.y, ymax, 0.1)
end
prevSettingStroke.x = xmax
prevSettingStroke.y = ymax
end
gfx.Translate(0, settingHeight)
end
gfx.Restore() --draw current tab end
prevTab = SettingsDiag.currentTab
prevVis = visible
gfx.Restore()
function clamp(x, min, max)
if x < min then
x = min
end
if x > max then
x = max
end
return x
end
function smootherstep(edge0, edge1, x)
-- Scale, and clamp x to 0..1 range
x = clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0)
-- Evaluate polynomial
return x * x * x * (x * (x * 6 - 15) + 10)
end
function to_range(val, start, stop)
return start + (stop - start) * val
end
Animation = {
start = 0,
stop = 0,
progress = 0,
duration = 1,
smoothStart = false
}
function Animation:new(o)
o = o or {}
setmetatable(o, self)
self.__index = self
return o
end
function Animation:restart(start, stop, duration)
self.progress = 0
self.start = start
self.stop = stop
self.duration = duration
end
function Animation:tick(deltaTime)
self.progress = math.min(1, self.progress + deltaTime / self.duration)
if self.progress == 1 then return self.stop end
if self.smoothStart then
return to_range(smootherstep(0, 1, self.progress), self.start, self.stop)
else
return to_range(smootherstep(-1, 1, self.progress) * 2 - 1, self.start, self.stop)
end
end
local yScale = Animation:new()
local diagWidth = 600
local diagHeight = 400
local tabStroke = {start=0, stop=1}
local tabStrokeAnimation = {start=Animation:new(), stop=Animation:new()}
local settingsStrokeAnimation = {x=Animation:new(), y=Animation:new()}
local prevTab = -1
local prevSettingStroke = {x=0, y=0}
local settingStroke = {x=0, y=0}
local prevVis = false
function processSkinSettings()
for ti, tab in ipairs(SettingsDiag.tabs) do
for si, setting in ipairs(tab.settings) do
if (tab.name == 'Game') then
if (setting.name == 'Gauge') then
game.SetSkinSetting('_gaugeType', setting.value);
end
if (setting.name == 'Backup Gauge') then
game.SetSkinSetting('_gaugeARS', setting.value and 1 or 0);
end
end
end
end
end
function render(deltaTime, visible)
if visible and not prevVis then
yScale:restart(0, 1, 0.25)
elseif not visible and prevVis then
yScale:restart(1, 0, 0.25)
end
processSkinSettings()
if not visible and yScale:tick(0) < 0.05 then return end
local posX = SettingsDiag.posX or 0.5
local posY = SettingsDiag.posY or 0.5
local message_1 = "Press both FXs to open/close. Use the Start button to press buttons."
local message_2 = "Use FX keys to navigate tabs. Use arrow keys to navigate and modify settings."
resX, resY = game.GetResolution()
local scale = resY / 1080
gfx.ResetTransform()
gfx.Translate(math.floor(diagWidth/2 + posX*(resX-diagWidth)), math.floor(diagHeight/2 + posY*(resY-diagHeight)))
gfx.Scale(scale, scale)
gfx.Scale(1.0, smootherstep(0, 1, yScale:tick(deltaTime)))
gfx.BeginPath()
gfx.Rect(-diagWidth/2, -diagHeight/2, diagWidth, diagHeight)
gfx.FillColor(50,50,50)
gfx.Fill()
gfx.FillColor(255,255,255)
gfx.FontSize(20)
local m_xmin, m_ymin, m_xmax, m_ymax = gfx.TextBounds(0, 0, message_1)
gfx.Text(message_1, diagWidth/2 - m_xmax, diagHeight/2 - m_ymax - 20)
m_xmin, m_ymin, m_xmax, m_ymax = gfx.TextBounds(0, 0, message_2)
gfx.Text(message_2, diagWidth/2 - m_xmax, diagHeight/2 - m_ymax)
tabStroke.start = tabStrokeAnimation.start:tick(deltaTime)
tabStroke.stop = tabStrokeAnimation.stop:tick(deltaTime)
settingStroke.x = settingsStrokeAnimation.x:tick(deltaTime)
settingStroke.y = settingsStrokeAnimation.y:tick(deltaTime)
local tabBarHeight = 0
local nextTabX = 5
gfx.TextAlign(gfx.TEXT_ALIGN_TOP + gfx.TEXT_ALIGN_LEFT)
gfx.FontSize(35)
gfx.Save() --draw tab bar
gfx.Translate(-diagWidth / 2, -diagHeight / 2)
for ti, tab in ipairs(SettingsDiag.tabs) do
local xmin,ymin, xmax,ymax = gfx.TextBounds(nextTabX, 5, tab.name)
if ti == SettingsDiag.currentTab and SettingsDiag.currentTab ~= prevTab then
tabStrokeAnimation.start:restart(tabStroke.start, nextTabX, 0.1)
tabStrokeAnimation.stop:restart(tabStroke.stop, xmax, 0.1)
end
tabBarHeight = math.max(tabBarHeight, ymax + 5)
gfx.Text(tab.name, nextTabX, 5)
nextTabX = xmax + 10
end
gfx.BeginPath()
gfx.MoveTo(0, tabBarHeight)
gfx.LineTo(diagWidth, tabBarHeight)
gfx.StrokeWidth(2)
gfx.StrokeColor(0,127,255)
gfx.Stroke()
gfx.BeginPath()
gfx.MoveTo(tabStroke.start, tabBarHeight)
gfx.LineTo(tabStroke.stop, tabBarHeight)
gfx.StrokeColor(255, 127, 0)
gfx.Stroke()
gfx.Restore() --draw tab bar end
gfx.FontSize(30)
gfx.Save() --draw current tab
gfx.Translate(-diagWidth / 2, -diagHeight / 2)
gfx.Translate(5, tabBarHeight + 5)
gfx.BeginPath()
gfx.MoveTo(0, settingStroke.y)
gfx.LineTo(settingStroke.x, settingStroke.y)
gfx.StrokeWidth(2)
gfx.StrokeColor(255, 127, 0)
gfx.Stroke()
local settingHeight = 30
local tab = SettingsDiag.tabs[SettingsDiag.currentTab]
for si, setting in ipairs(tab.settings) do
processSkinSettings(setting.name, setting.value)
local disp = ""
if setting.type == "enum" then
disp = string.format("%s: %s", setting.name, setting.options[setting.value])
elseif setting.type == "int" then
disp = string.format("%s: %d", setting.name, setting.value)
elseif setting.type == "float" then
disp = string.format("%s: %.2f", setting.name, setting.value)
if setting.max == 1 and setting.min == 0 then --draw slider
disp = setting.name .. ": "
local xmin,ymin, xmax,ymax = gfx.TextBounds(0, 0, disp)
local width = diagWidth - 20 - xmax
gfx.BeginPath()
gfx.MoveTo(xmax + 5, 20)
gfx.LineTo(xmax + 5 + width, 20)
gfx.StrokeColor(0,127,255)
gfx.StrokeWidth(2)
gfx.Stroke()
gfx.BeginPath()
gfx.MoveTo(xmax + 5, 20)
gfx.LineTo(xmax + 5 + width * setting.value, 20)
gfx.StrokeColor(255,127,0)
gfx.StrokeWidth(2)
gfx.Stroke()
end
elseif setting.type == "button" then
disp = string.format("%s", setting.name)
local xmin, ymin, xmax,ymax = gfx.TextBounds(0, 0, disp)
gfx.BeginPath()
gfx.Rect(-2, 3, 4+xmax-xmin, 28)
gfx.FillColor(0, 64, 128)
if si == SettingsDiag.currentSetting then
gfx.StrokeColor(255, 127, 0)
else
gfx.StrokeColor(0,127,255)
end
gfx.StrokeWidth(2)
gfx.Fill()
gfx.Stroke()
gfx.FillColor(255,255,255)
else
disp = string.format("%s:", setting.name)
local xmin,ymin, xmax,ymax = gfx.TextBounds(0, 0, disp)
gfx.BeginPath()
gfx.Rect(xmax + 5, 5, 20,20)
gfx.FillColor(255, 127, 0, setting.value and 255 or 0)
gfx.StrokeColor(0,127,255)
gfx.StrokeWidth(2)
gfx.Fill()
gfx.Stroke()
gfx.FillColor(255,255,255)
end
gfx.Text(disp, 0 ,0)
if si == SettingsDiag.currentSetting then
local setting_name = setting.name .. ":"
if setting.type == "button" then
setting_name = setting.name
end
local xmin,ymin, xmax,ymax = gfx.TextBounds(0, 0, setting_name)
ymax = ymax + settingHeight * (si - 1)
if xmax ~= prevSettingStroke.x or ymax ~= prevSettingStroke.y then
settingsStrokeAnimation.x:restart(settingStroke.x, xmax, 0.1)
settingsStrokeAnimation.y:restart(settingStroke.y, ymax, 0.1)
end
prevSettingStroke.x = xmax
prevSettingStroke.y = ymax
end
gfx.Translate(0, settingHeight)
end
gfx.Restore() --draw current tab end
prevTab = SettingsDiag.currentTab
prevVis = visible
end

View File

@ -1,400 +1,400 @@
--
-- json.lua
--
-- Copyright (c) 2019 rxi
--
-- Permission is hereby granted, free of charge, to any person obtaining a copy of
-- this software and associated documentation files (the "Software"), to deal in
-- the Software without restriction, including without limitation the rights to
-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
-- of the Software, and to permit persons to whom the Software is furnished to do
-- so, subject to the following conditions:
--
-- The above copyright notice and this permission notice shall be included in all
-- copies or substantial portions of the Software.
--
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-- SOFTWARE.
--
local json = { _version = "0.1.1" }
-------------------------------------------------------------------------------
-- Encode
-------------------------------------------------------------------------------
local encode
local escape_char_map = {
[ "\\" ] = "\\\\",
[ "\"" ] = "\\\"",
[ "\b" ] = "\\b",
[ "\f" ] = "\\f",
[ "\n" ] = "\\n",
[ "\r" ] = "\\r",
[ "\t" ] = "\\t",
}
local escape_char_map_inv = { [ "\\/" ] = "/" }
for k, v in pairs(escape_char_map) do
escape_char_map_inv[v] = k
end
local function escape_char(c)
return escape_char_map[c] or string.format("\\u%04x", c:byte())
end
local function encode_nil(val)
return "null"
end
local function encode_table(val, stack)
local res = {}
stack = stack or {}
-- Circular reference?
if stack[val] then error("circular reference") end
stack[val] = true
if rawget(val, 1) ~= nil or next(val) == nil then
-- Treat as array -- check keys are valid and it is not sparse
local n = 0
for k in pairs(val) do
if type(k) ~= "number" then
error("invalid table: mixed or invalid key types")
end
n = n + 1
end
if n ~= #val then
error("invalid table: sparse array")
end
-- Encode
for i, v in ipairs(val) do
table.insert(res, encode(v, stack))
end
stack[val] = nil
return "[" .. table.concat(res, ",") .. "]"
else
-- Treat as an object
for k, v in pairs(val) do
if type(k) ~= "string" then
error("invalid table: mixed or invalid key types")
end
table.insert(res, encode(k, stack) .. ":" .. encode(v, stack))
end
stack[val] = nil
return "{" .. table.concat(res, ",") .. "}"
end
end
local function encode_string(val)
return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"'
end
local function encode_number(val)
-- Check for NaN, -inf and inf
if val ~= val or val <= -math.huge or val >= math.huge then
error("unexpected number value '" .. tostring(val) .. "'")
end
return string.format("%.14g", val)
end
local type_func_map = {
[ "nil" ] = encode_nil,
[ "table" ] = encode_table,
[ "string" ] = encode_string,
[ "number" ] = encode_number,
[ "boolean" ] = tostring,
}
encode = function(val, stack)
local t = type(val)
local f = type_func_map[t]
if f then
return f(val, stack)
end
error("unexpected type '" .. t .. "'")
end
function json.encode(val)
return ( encode(val) )
end
-------------------------------------------------------------------------------
-- Decode
-------------------------------------------------------------------------------
local parse
local function create_set(...)
local res = {}
for i = 1, select("#", ...) do
res[ select(i, ...) ] = true
end
return res
end
local space_chars = create_set(" ", "\t", "\r", "\n")
local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
local literals = create_set("true", "false", "null")
local literal_map = {
[ "true" ] = true,
[ "false" ] = false,
[ "null" ] = nil,
}
local function next_char(str, idx, set, negate)
for i = idx, #str do
if set[str:sub(i, i)] ~= negate then
return i
end
end
return #str + 1
end
local function decode_error(str, idx, msg)
local line_count = 1
local col_count = 1
for i = 1, idx - 1 do
col_count = col_count + 1
if str:sub(i, i) == "\n" then
line_count = line_count + 1
col_count = 1
end
end
error( string.format("%s at line %d col %d", msg, line_count, col_count) )
end
local function codepoint_to_utf8(n)
-- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
local f = math.floor
if n <= 0x7f then
return string.char(n)
elseif n <= 0x7ff then
return string.char(f(n / 64) + 192, n % 64 + 128)
elseif n <= 0xffff then
return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
elseif n <= 0x10ffff then
return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
f(n % 4096 / 64) + 128, n % 64 + 128)
end
error( string.format("invalid unicode codepoint '%x'", n) )
end
local function parse_unicode_escape(s)
local n1 = tonumber( s:sub(3, 6), 16 )
local n2 = tonumber( s:sub(9, 12), 16 )
-- Surrogate pair?
if n2 then
return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
else
return codepoint_to_utf8(n1)
end
end
local function parse_string(str, i)
local has_unicode_escape = false
local has_surrogate_escape = false
local has_escape = false
local last
for j = i + 1, #str do
local x = str:byte(j)
if x < 32 then
decode_error(str, j, "control character in string")
end
if last == 92 then -- "\\" (escape char)
if x == 117 then -- "u" (unicode escape sequence)
local hex = str:sub(j + 1, j + 5)
if not hex:find("%x%x%x%x") then
decode_error(str, j, "invalid unicode escape in string")
end
if hex:find("^[dD][89aAbB]") then
has_surrogate_escape = true
else
has_unicode_escape = true
end
else
local c = string.char(x)
if not escape_chars[c] then
decode_error(str, j, "invalid escape char '" .. c .. "' in string")
end
has_escape = true
end
last = nil
elseif x == 34 then -- '"' (end of string)
local s = str:sub(i + 1, j - 1)
if has_surrogate_escape then
s = s:gsub("\\u[dD][89aAbB]..\\u....", parse_unicode_escape)
end
if has_unicode_escape then
s = s:gsub("\\u....", parse_unicode_escape)
end
if has_escape then
s = s:gsub("\\.", escape_char_map_inv)
end
return s, j + 1
else
last = x
end
end
decode_error(str, i, "expected closing quote for string")
end
local function parse_number(str, i)
local x = next_char(str, i, delim_chars)
local s = str:sub(i, x - 1)
local n = tonumber(s)
if not n then
decode_error(str, i, "invalid number '" .. s .. "'")
end
return n, x
end
local function parse_literal(str, i)
local x = next_char(str, i, delim_chars)
local word = str:sub(i, x - 1)
if not literals[word] then
decode_error(str, i, "invalid literal '" .. word .. "'")
end
return literal_map[word], x
end
local function parse_array(str, i)
local res = {}
local n = 1
i = i + 1
while 1 do
local x
i = next_char(str, i, space_chars, true)
-- Empty / end of array?
if str:sub(i, i) == "]" then
i = i + 1
break
end
-- Read token
x, i = parse(str, i)
res[n] = x
n = n + 1
-- Next token
i = next_char(str, i, space_chars, true)
local chr = str:sub(i, i)
i = i + 1
if chr == "]" then break end
if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
end
return res, i
end
local function parse_object(str, i)
local res = {}
i = i + 1
while 1 do
local key, val
i = next_char(str, i, space_chars, true)
-- Empty / end of object?
if str:sub(i, i) == "}" then
i = i + 1
break
end
-- Read key
if str:sub(i, i) ~= '"' then
decode_error(str, i, "expected string for key")
end
key, i = parse(str, i)
-- Read ':' delimiter
i = next_char(str, i, space_chars, true)
if str:sub(i, i) ~= ":" then
decode_error(str, i, "expected ':' after key")
end
i = next_char(str, i + 1, space_chars, true)
-- Read value
val, i = parse(str, i)
-- Set
res[key] = val
-- Next token
i = next_char(str, i, space_chars, true)
local chr = str:sub(i, i)
i = i + 1
if chr == "}" then break end
if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
end
return res, i
end
local char_func_map = {
[ '"' ] = parse_string,
[ "0" ] = parse_number,
[ "1" ] = parse_number,
[ "2" ] = parse_number,
[ "3" ] = parse_number,
[ "4" ] = parse_number,
[ "5" ] = parse_number,
[ "6" ] = parse_number,
[ "7" ] = parse_number,
[ "8" ] = parse_number,
[ "9" ] = parse_number,
[ "-" ] = parse_number,
[ "t" ] = parse_literal,
[ "f" ] = parse_literal,
[ "n" ] = parse_literal,
[ "[" ] = parse_array,
[ "{" ] = parse_object,
}
parse = function(str, idx)
local chr = str:sub(idx, idx)
local f = char_func_map[chr]
if f then
return f(str, idx)
end
decode_error(str, idx, "unexpected character '" .. chr .. "'")
end
function json.decode(str)
if type(str) ~= "string" then
error("expected argument of type string, got " .. type(str))
end
local res, idx = parse(str, next_char(str, 1, space_chars, true))
idx = next_char(str, idx, space_chars, true)
if idx <= #str then
decode_error(str, idx, "trailing garbage")
end
return res
end
return json
--
-- json.lua
--
-- Copyright (c) 2019 rxi
--
-- Permission is hereby granted, free of charge, to any person obtaining a copy of
-- this software and associated documentation files (the "Software"), to deal in
-- the Software without restriction, including without limitation the rights to
-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
-- of the Software, and to permit persons to whom the Software is furnished to do
-- so, subject to the following conditions:
--
-- The above copyright notice and this permission notice shall be included in all
-- copies or substantial portions of the Software.
--
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-- SOFTWARE.
--
local json = { _version = "0.1.1" }
-------------------------------------------------------------------------------
-- Encode
-------------------------------------------------------------------------------
local encode
local escape_char_map = {
[ "\\" ] = "\\\\",
[ "\"" ] = "\\\"",
[ "\b" ] = "\\b",
[ "\f" ] = "\\f",
[ "\n" ] = "\\n",
[ "\r" ] = "\\r",
[ "\t" ] = "\\t",
}
local escape_char_map_inv = { [ "\\/" ] = "/" }
for k, v in pairs(escape_char_map) do
escape_char_map_inv[v] = k
end
local function escape_char(c)
return escape_char_map[c] or string.format("\\u%04x", c:byte())
end
local function encode_nil(val)
return "null"
end
local function encode_table(val, stack)
local res = {}
stack = stack or {}
-- Circular reference?
if stack[val] then error("circular reference") end
stack[val] = true
if rawget(val, 1) ~= nil or next(val) == nil then
-- Treat as array -- check keys are valid and it is not sparse
local n = 0
for k in pairs(val) do
if type(k) ~= "number" then
error("invalid table: mixed or invalid key types")
end
n = n + 1
end
if n ~= #val then
error("invalid table: sparse array")
end
-- Encode
for i, v in ipairs(val) do
table.insert(res, encode(v, stack))
end
stack[val] = nil
return "[" .. table.concat(res, ",") .. "]"
else
-- Treat as an object
for k, v in pairs(val) do
if type(k) ~= "string" then
error("invalid table: mixed or invalid key types")
end
table.insert(res, encode(k, stack) .. ":" .. encode(v, stack))
end
stack[val] = nil
return "{" .. table.concat(res, ",") .. "}"
end
end
local function encode_string(val)
return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"'
end
local function encode_number(val)
-- Check for NaN, -inf and inf
if val ~= val or val <= -math.huge or val >= math.huge then
error("unexpected number value '" .. tostring(val) .. "'")
end
return string.format("%.14g", val)
end
local type_func_map = {
[ "nil" ] = encode_nil,
[ "table" ] = encode_table,
[ "string" ] = encode_string,
[ "number" ] = encode_number,
[ "boolean" ] = tostring,
}
encode = function(val, stack)
local t = type(val)
local f = type_func_map[t]
if f then
return f(val, stack)
end
error("unexpected type '" .. t .. "'")
end
function json.encode(val)
return ( encode(val) )
end
-------------------------------------------------------------------------------
-- Decode
-------------------------------------------------------------------------------
local parse
local function create_set(...)
local res = {}
for i = 1, select("#", ...) do
res[ select(i, ...) ] = true
end
return res
end
local space_chars = create_set(" ", "\t", "\r", "\n")
local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
local literals = create_set("true", "false", "null")
local literal_map = {
[ "true" ] = true,
[ "false" ] = false,
[ "null" ] = nil,
}
local function next_char(str, idx, set, negate)
for i = idx, #str do
if set[str:sub(i, i)] ~= negate then
return i
end
end
return #str + 1
end
local function decode_error(str, idx, msg)
local line_count = 1
local col_count = 1
for i = 1, idx - 1 do
col_count = col_count + 1
if str:sub(i, i) == "\n" then
line_count = line_count + 1
col_count = 1
end
end
error( string.format("%s at line %d col %d", msg, line_count, col_count) )
end
local function codepoint_to_utf8(n)
-- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
local f = math.floor
if n <= 0x7f then
return string.char(n)
elseif n <= 0x7ff then
return string.char(f(n / 64) + 192, n % 64 + 128)
elseif n <= 0xffff then
return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
elseif n <= 0x10ffff then
return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
f(n % 4096 / 64) + 128, n % 64 + 128)
end
error( string.format("invalid unicode codepoint '%x'", n) )
end
local function parse_unicode_escape(s)
local n1 = tonumber( s:sub(3, 6), 16 )
local n2 = tonumber( s:sub(9, 12), 16 )
-- Surrogate pair?
if n2 then
return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
else
return codepoint_to_utf8(n1)
end
end
local function parse_string(str, i)
local has_unicode_escape = false
local has_surrogate_escape = false
local has_escape = false
local last
for j = i + 1, #str do
local x = str:byte(j)
if x < 32 then
decode_error(str, j, "control character in string")
end
if last == 92 then -- "\\" (escape char)
if x == 117 then -- "u" (unicode escape sequence)
local hex = str:sub(j + 1, j + 5)
if not hex:find("%x%x%x%x") then
decode_error(str, j, "invalid unicode escape in string")
end
if hex:find("^[dD][89aAbB]") then
has_surrogate_escape = true
else
has_unicode_escape = true
end
else
local c = string.char(x)
if not escape_chars[c] then
decode_error(str, j, "invalid escape char '" .. c .. "' in string")
end
has_escape = true
end
last = nil
elseif x == 34 then -- '"' (end of string)
local s = str:sub(i + 1, j - 1)
if has_surrogate_escape then
s = s:gsub("\\u[dD][89aAbB]..\\u....", parse_unicode_escape)
end
if has_unicode_escape then
s = s:gsub("\\u....", parse_unicode_escape)
end
if has_escape then
s = s:gsub("\\.", escape_char_map_inv)
end
return s, j + 1
else
last = x
end
end
decode_error(str, i, "expected closing quote for string")
end
local function parse_number(str, i)
local x = next_char(str, i, delim_chars)
local s = str:sub(i, x - 1)
local n = tonumber(s)
if not n then
decode_error(str, i, "invalid number '" .. s .. "'")
end
return n, x
end
local function parse_literal(str, i)
local x = next_char(str, i, delim_chars)
local word = str:sub(i, x - 1)
if not literals[word] then
decode_error(str, i, "invalid literal '" .. word .. "'")
end
return literal_map[word], x
end
local function parse_array(str, i)
local res = {}
local n = 1
i = i + 1
while 1 do
local x
i = next_char(str, i, space_chars, true)
-- Empty / end of array?
if str:sub(i, i) == "]" then
i = i + 1
break
end
-- Read token
x, i = parse(str, i)
res[n] = x
n = n + 1
-- Next token
i = next_char(str, i, space_chars, true)
local chr = str:sub(i, i)
i = i + 1
if chr == "]" then break end
if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
end
return res, i
end
local function parse_object(str, i)
local res = {}
i = i + 1
while 1 do
local key, val
i = next_char(str, i, space_chars, true)
-- Empty / end of object?
if str:sub(i, i) == "}" then
i = i + 1
break
end
-- Read key
if str:sub(i, i) ~= '"' then
decode_error(str, i, "expected string for key")
end
key, i = parse(str, i)
-- Read ':' delimiter
i = next_char(str, i, space_chars, true)
if str:sub(i, i) ~= ":" then
decode_error(str, i, "expected ':' after key")
end
i = next_char(str, i + 1, space_chars, true)
-- Read value
val, i = parse(str, i)
-- Set
res[key] = val
-- Next token
i = next_char(str, i, space_chars, true)
local chr = str:sub(i, i)
i = i + 1
if chr == "}" then break end
if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
end
return res, i
end
local char_func_map = {
[ '"' ] = parse_string,
[ "0" ] = parse_number,
[ "1" ] = parse_number,
[ "2" ] = parse_number,
[ "3" ] = parse_number,
[ "4" ] = parse_number,
[ "5" ] = parse_number,
[ "6" ] = parse_number,
[ "7" ] = parse_number,
[ "8" ] = parse_number,
[ "9" ] = parse_number,
[ "-" ] = parse_number,
[ "t" ] = parse_literal,
[ "f" ] = parse_literal,
[ "n" ] = parse_literal,
[ "[" ] = parse_array,
[ "{" ] = parse_object,
}
parse = function(str, idx)
local chr = str:sub(idx, idx)
local f = char_func_map[chr]
if f then
return f(str, idx)
end
decode_error(str, idx, "unexpected character '" .. chr .. "'")
end
function json.decode(str)
if type(str) ~= "string" then
error("expected argument of type string, got " .. type(str))
end
local res, idx = parse(str, next_char(str, 1, space_chars, true))
idx = next_char(str, idx, space_chars, true)
if idx <= #str then
decode_error(str, idx, "trailing garbage")
end
return res
end
return json

View File

@ -2,37 +2,37 @@
local lang = {
Challanges = {
--rightside
ch = "Analyze your skill!",
ch1 = "Find out how good you are by taking on challenges!\nIn this mode your ability will pushed to the limits.",
ch = "Idk",
ch1 = "Torture yourself :)",
},
Multiplayer = {
--rightside
mp = "Online matchmaking!",
mp2 = "Play against your friends!\nSupport for up to 8 players.",
mp = "Wait, there's multiplayer???",
mp2 = "Yes, but nobody is ever online",
},
Start = {
--rightside
st = "Basic play!",
st2 = "Play whatever songs you like!\nComplete for high scores on an IR around the world.",
st = "Play shit idk",
st2 = "Play something blah blah blah blah\nWith newlines",
--leftside
st3 = "Start",
sc = "VOL to select",
sc = "Scroll",
desc = "Test description. Blah blah blah",
},
Nautica = {
--rightside
dls = "Download more songs!",
dls2 = "Get new charts from ksm.dev, updated daily!",
dls = "Download more songs",
dls2 = "ksm.dev",
},
Settings = {
--rightside
se = "Open settings!",
se1= "Tweak things to your liking.",
se = "Adjust things",
se1= "Open settings",
},
Exit = {
--rightside
ex = "Close the game!",
ex2 = "Close the game and end this session.\nGoodbye!",
ex = "Leave this cursed game",
ex2 = "C'mon press that button!\nYou know you want to do it",
},
Result = {
--leftside

View File

@ -1,11 +1,11 @@
local call = nil
local EN = require("language.EN")
local DE = require("language.DE")
local SK = require("language.SK")
local HU = require("language.HU")
local test2 = require("language.test2")
local call = EN
if game.GetSkinSetting('words') == "EN" then
call = EN
elseif game.GetSkinSetting('words') == "DE" then
@ -19,4 +19,4 @@ elseif game.GetSkinSetting('words') == "test2" then
end
return call
return call

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +1,11 @@
local Numbers = require('common.numbers')
local Easing = require('common.easing');
local Charting = require('common.charting');
local Background = require('components.background');
local Footer = require('components.footer');
local Numbers = require('components.numbers')
local DiffRectangle = require('components.diff_rectangle');
local lang = require("language.call")
require('common.gameconfig')
local crew = game.GetSkinSetting("single_idol")
local creww = game.GetSkinSetting("single_idol")
local VolforceWindow = require('components.volforceWindow')
@ -48,18 +45,12 @@ local defaultJacketImage = gfx.CreateSkinImage("result/default_jacket.png", 0);
local bestScoreBadgeImage = gfx.CreateSkinImage("result/best.png", 0);
local defaultCardImage = gfx.CreateSkinImage("result/default_appeal_card.png", 0)
local defaultBadgeImage = gfx.CreateSkinImage("result/default_dan.png", 0)
local appealCardImage = gfx.CreateSkinImage("crew/appeal_card.png", 0);
local danBadgeImage = gfx.CreateSkinImage("dan/inf.png", 0);
local badgeLines = gfx.CreateSkinImage("result/badge_lines.png", 0);
local badgeGrade = gfx.CreateSkinImage("result/badge_gradient.png", 0);
local gaugeTypeMirrorImage = gfx.CreateSkinImage("result/gauge_type_badges/mirror.png", 0);
local gaugeTypeRandomImage = gfx.CreateSkinImage("result/gauge_type_badges/random.png", 0);
local gaugeTypeMirrorRandomImage = gfx.CreateSkinImage("result/gauge_type_badges/random_mirror.png", 0);
local gradeImages = {
S = gfx.CreateSkinImage("common/grades/S.png", 0),
AAA_P = gfx.CreateSkinImage("common/grades/AAA+.png", 0),
@ -103,8 +94,7 @@ local difficultyLabelImages = {
gfx.CreateSkinImage("diff/5 infinite.png", 0),
gfx.CreateSkinImage("diff/6 gravity.png", 0),
gfx.CreateSkinImage("diff/7 heavenly.png", 0),
gfx.CreateSkinImage("diff/8 vivid.png", 0),
gfx.CreateSkinImage("diff/9 exceed.png", 0)
gfx.CreateSkinImage("diff/8 vivid.png", 0)
}
local clearBadgeImages = {
@ -139,11 +129,7 @@ local clearBadgeImages = {
}
-- ANIMS
local idolAnimation = gfx.LoadSkinAnimation('crew/anim/'..crew, 1 / 30, 0, true);
if not idolAnimation then
game.Log("Crew folder crew/anim/"..crew.." does not exist.", game.LOGGER_WARNING)
end
local idolAnimation = gfx.LoadSkinAnimation('crew/anim/'..creww, 1 / 30, 0, true);
local transitionEnterScale = 0;
local idolAnimTransitionScale = 0;
@ -168,9 +154,7 @@ local BOTTOM_PANEL_TRANSTION_ENTER_OFFSET = 256;
local highScore;
-- gameplay table does not have a current username field, because why would it lmao
-- workaround: retrieve it directly from Main.cfg file
local username = GameConfig["MultiplayerUsername"] or game.GetSkinSetting('username') or '';
local username = game.GetSkinSetting('username');
local msg = game.GetSkinSetting("MSG");
local earlyLateBarsStats = {
@ -192,13 +176,6 @@ local irText = ''
game.LoadSkinSample("result")
game.LoadSkinSample("shutter")
local function isHard(result)
if result.flags == nil then
return result.gauge_type == 1
end
return result.flags & 1 == 1
end
local handleSfx = function()
if not bgSfxPlayed then
game.PlaySample("result", true)
@ -207,55 +184,6 @@ local handleSfx = function()
end
end
local drawGraph = function(x,y,w,h)
if isHard(result) then
gfx.BeginPath()
gfx.Rect(x,y,w,103)
gfx.FillColor(26,26,26,255)
gfx.Fill()
gfx.FillColor(255,255,255,255)
else
gfx.BeginPath()
gfx.Rect(x,y,w,h-68)
gfx.FillColor(55,27,51,255)
gfx.Fill()
gfx.BeginPath()
gfx.Rect(x,y+30,w,72)
gfx.FillColor(7,24,28,255)
gfx.Fill()
gfx.FillColor(255,255,255,255)
end
gfx.BeginPath()
gfx.MoveTo(x,y + h + 2 - h * result.gaugeSamples[1])
for i = 2, #result.gaugeSamples do
gfx.LineTo(x + i * w / #result.gaugeSamples,y + h + 2 - h * result.gaugeSamples[i])
end
if isHard(result) then
gfx.StrokeWidth(3)
gfx.StrokeColor(232,163,10)
gfx.Stroke()
gfx.Scissor(x, y + h *0.01, w, h*0.98)
gfx.Stroke()
gfx.ResetScissor()
gfx.Scissor(x, y + h * 0.99, w, (h * 0.03) + 4)
gfx.StrokeColor(255,0,0)
gfx.Stroke()
gfx.ResetScissor()
else
gfx.StrokeWidth(3)
gfx.StrokeColor(46,211,241)
gfx.Scissor(x, y + h * 0.3, w, (h * 0.7) + 4)
gfx.Stroke()
gfx.ResetScissor()
gfx.Scissor(x, y, w, h*0.3)
gfx.StrokeColor(215,48,182)
gfx.Stroke()
gfx.ResetScissor()
end
end
function drawTimingBar(y, value, max, type)
gfx.BeginPath();
@ -273,19 +201,17 @@ function drawTimingBar(y, value, max, type)
end
local drawIdol = function(deltaTime)
if idolAnimation then
local idolAnimTickRes = gfx.TickAnimation(idolAnimation, deltaTime);
if idolAnimTickRes == 1 then
gfx.GlobalAlpha(idolAnimTransitionScale);
idolAnimTransitionScale = idolAnimTransitionScale + 1 / 60;
if (GameConfig["AutoScoreScreenshot"] or idolAnimTransitionScale > 1) then
idolAnimTransitionScale = 1;
end
gfx.ImageRect(0, 0, desw, desh, idolAnimation, 1, 0);
gfx.GlobalAlpha(1);
local idolAnimTickRes = gfx.TickAnimation(idolAnimation, deltaTime);
if idolAnimTickRes == 1 then
gfx.GlobalAlpha(idolAnimTransitionScale);
idolAnimTransitionScale = idolAnimTransitionScale + 1 / 60;
if (idolAnimTransitionScale > 1) then
idolAnimTransitionScale = 1;
end
gfx.ImageRect(0, 0, desw, desh, idolAnimation, 1, 0);
gfx.GlobalAlpha(1);
end
end
@ -475,21 +401,6 @@ local drawRightPanelContent = function()
gfx.Restore()
end
-- Draw the gauge type flags if needed (mirror, random)
if(result.mirror or result.random) then
gfx.BeginPath();
local gaugeTypeFlagPosX = gaugePosX + 10;
local gaugeTypeFlagPosY = gaugePosY - 30;
local flagw, flagh = gfx.ImageSize(gaugeTypeMirrorImage)
if(result.mirror and result.random) then
gfx.ImageRect(gaugeTypeFlagPosX, gaugeTypeFlagPosY, flagw, flagh, gaugeTypeMirrorRandomImage, 1, 0)
elseif(result.mirror) then
gfx.ImageRect(gaugeTypeFlagPosX, gaugeTypeFlagPosY, flagw, flagh, gaugeTypeMirrorImage, 1, 0)
elseif(result.random) then
gfx.ImageRect(gaugeTypeFlagPosX, gaugeTypeFlagPosY, flagw, flagh, gaugeTypeRandomImage, 1, 0)
end
end
-- Draw err/early/critical/late/err texts
gfx.Text(earlyLateBarsStats.earlyErrors, rightPanelX + 683,
@ -549,27 +460,25 @@ local drawBottomPanelContent = function(deltatime)
-- Draw appeal card
gfx.BeginPath();
gfx.ImageRect(bottomPanelX + 58, bottomPanelY + 277, 103, 132,
result.isSelf and appealCardImage or defaultCardImage, 1, 0);
appealCardImage, 1, 0);
-- Draw description
gfx.FontSize(22)
gfx.LoadSkinFont('Digital-Serial-Bold.ttf')
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_MIDDLE)
gfx.Text(result.isSelf and msg or ("Player "..result.displayIndex), bottomPanelX + 190, bottomPanelY + 282);
gfx.Text(msg, bottomPanelX + 190, bottomPanelY + 282);
-- Draw username
gfx.FontSize(28)
gfx.Text(result.playerName or username, bottomPanelX + 190, bottomPanelY + 314);
gfx.Text(username, bottomPanelX + 190, bottomPanelY + 314);
-- Draw dan badge
gfx.BeginPath();
gfx.ImageRect(bottomPanelX + 187, bottomPanelY + 362, 107, 29,
result.isSelf and danBadgeImage or defaultBadgeImage, 1, 0);
danBadgeImage, 1, 0);
-- Draw volforce
if result.isSelf then
VolforceWindow.render(0, bottomPanelX + 310, bottomPanelY + 355)
end
VolforceWindow.render(0, bottomPanelX + 310, bottomPanelY + 355)
-- Draw IR text
gfx.FontSize(22)
@ -588,23 +497,8 @@ local drawBottomPanelContent = function(deltatime)
gfx.TextAlign(gfx.TEXT_ALIGN_RIGHT + gfx.TEXT_ALIGN_MIDDLE)
gfx.Text(result.medianHitDelta.." ms", rightX, baseY);
gfx.Text(math.floor(result.meanHitDelta).." ms", rightX, baseY + detailTextMargin);
--Draw Graph
drawGraph(leftX-22, baseY-18, 454, 98);
--draw Recommended Offset
local delta = math.floor(result.medianHitDelta);
local songOffset = 0;
if (songOffset == nil) then songOffset = 0; end
local offset = tonumber(songOffset) + delta;
gfx.FillColor(255,255,255,255);
gfx.TextAlign(gfx.TEXT_ALIGN_RIGHT + gfx.TEXT_ALIGN_TOP)
gfx.Text('RECOMMENDED SONG OFFSET:', leftX + 367, baseY + 89);
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_TOP)
gfx.Text(string.format("%dms", offset), leftX + 370, baseY + 89);
end
local drawJacketPanel = function()
gfx.BeginPath();
local tw, th = gfx.ImageSize(jacketPanelImage);
@ -614,9 +508,10 @@ end
local drawJacketPanelContent = function(deltaTime)
gfx.BeginPath();
gfx.ImageRect(jacketPanelX + 13, jacketPanelY + 28, 265, 265, jacketImage or defaultJacketImage, 1, 0);
local adjustedDiff = Charting.GetDisplayDifficulty(result.jacketPath, result.difficulty)
DiffRectangle.render(deltaTime, jacketPanelX+183, jacketPanelY+2.5, 0.67, adjustedDiff, result.level);
gfx.ImageRect(jacketPanelX + 13, jacketPanelY + 28, 265, 265,
jacketImage or defaultJacketImage, 1, 0);
DiffRectangle.render(deltaTime, jacketPanelX+183, jacketPanelY+2.5, 0.67, result.difficulty, result.level);
-- gfx.BeginPath();
-- gfx.ImageRect(jacketPanelX + 183, jacketPanelY + 2.5, 140 / 1.5, 31 / 1.5,
@ -646,7 +541,7 @@ end
local tickTransitions = function(deltaTime)
if not GameConfig["AutoScoreScreenshot"] and transitionEnterScale < 1 then
if transitionEnterScale < 1 then
transitionEnterScale = transitionEnterScale + deltaTime / 0.66 -- transition should last for that time in seconds
else
transitionEnterScale = 1
@ -663,7 +558,7 @@ local tickTransitions = function(deltaTime)
(1 - Easing.outQuad(transitionEnterScale)))
if not GameConfig["AutoScoreScreenshot"] and badgeLinesAnimScale < 1 then
if badgeLinesAnimScale < 1 then
badgeLinesAnimScale = badgeLinesAnimScale + deltaTime / 0.5 -- transition should last for that time in seconds
else
badgeLinesAnimScale = 0
@ -838,4 +733,4 @@ end
screenshot_captured = function(path)
game.PlaySample("shutter")
end
end

13
scripts/shared/idol.lua Normal file
View File

@ -0,0 +1,13 @@
local idols ={
-- SDVX IV
nearnoah_summer={"crew/anim/nearnoah_summer", 1 / 30, 0, true}
-- SDVX V
-- SDVX III
}
return idols

File diff suppressed because it is too large Load Diff

View File

@ -1,13 +1,16 @@
local Easing = require('common.easing')
local Dim = require("common.dimensions")
require('common')
local Easing = require('common.easing');
local SongSelectHeader = require('components.headers.songSelectHeader')
local Footer = require('components.footer');
local backgroundImage = gfx.CreateSkinImage("bg_pattern.png", gfx.IMAGE_REPEATX | gfx.IMAGE_REPEATY)
local defaultFolderBgImage = gfx.CreateSkinImage('song_select/filter_wheel/bg.png', 0)
local collectionFolderBgImage = gfx.CreateSkinImage('song_select/filter_wheel/col_bg.png', 0)
local subFolderBgImage = gfx.CreateSkinImage('song_select/filter_wheel/sub_bg.png', 0)
local scrollBarBackgroundImage = gfx.CreateSkinImage("song_select/scrollbar/bg.png", 1)
local scrollBarFillImage = gfx.CreateSkinImage("song_select/scrollbar/fill.png", 1)
local scrollbarBgImage = gfx.CreateSkinImage("song_select/scrollbar/bg.png", 1)
local scrollbarFillImage = gfx.CreateSkinImage("song_select/scrollbar/fill.png", 1)
local cursorImages = {
gfx.CreateSkinImage("song_select/cursor.png", 1), -- Effective rate or fallback
@ -16,7 +19,7 @@ local cursorImages = {
gfx.CreateSkinImage("song_select/cursor_blast.png", 1), -- Blastive rate
}
local ITEM_HEIGHT = 172
local ITEM_HEIGHT = 172;
local specialFolders = {
{
@ -89,27 +92,27 @@ local specialFolders = {
}
-- AUDIO
game.LoadSkinSample('song_wheel/cursor_change.wav')
game.LoadSkinSample('filter_wheel/open_close.wav')
game.LoadSkinSample('song_wheel/cursor_change.wav');
game.LoadSkinSample('filter_wheel/open_close.wav');
local resx, resy = game.GetResolution()
local desw, desh = 1080, 1920
local scale = 1
local scale = 1;
local isFilterWheelActive = false
local previousActiveState = false -- for open/close sounds
local isFilterWheelActive = false;
local previousActiveState = false; -- for open/close sounds
local selectionMode = 'folders'
local selectedFolder = 1
local selectedLevel = 1
local selectionMode = 'folders';
local selectedFolder = 1;
local selectedLevel = 1;
local transitionScrollScale = 0
local transitionScrollOffsetY = 0
local scrollingUp = false
local transitionScrollScale = 0;
local transitionScrollOffsetY = 0;
local scrollingUp = false;
local transitionLeaveScale = 1
local transitionLeaveReappearTimer = 0
local TRANSITION_LEAVE_DURATION = 0.1
local transitionLeaveScale = 1;
local transitionLeaveReappearTimer = 0;
local TRANSITION_LEAVE_DURATION = 0.1;
-- Window variables
local resX, resY
@ -135,31 +138,31 @@ function resetLayoutInformation()
end
function getCorrectedIndex(from, offset)
local total = 1
local total = 1;
if selectionMode == 'folders' then
total = #filters.folder
total = #filters.folder;
else
total = #filters.level
total = #filters.level;
end
index = from + offset
index = from + offset;
if index < 1 then
index = total + (from + offset) -- this only happens if the offset is negative
end
if index > total then
indexesUntilEnd = total - from
indexesUntilEnd = total - from;
index = offset - indexesUntilEnd -- this only happens if the offset is positive
end
return index
return index;
end
function getFolderData(folderLabel)
local folderType = 'unknown'
local isSpecial = false
local folderBgImage = defaultFolderBgImage
local folderType = 'unknown';
local isSpecial = false;
local folderBgImage = defaultFolderBgImage;
if not folderLabel then
return {
@ -177,16 +180,16 @@ function getFolderData(folderLabel)
if (string.find(folderLabel, 'Folder: ')) then
folderType = 'folder'
folderType = 'folder';
folderLabel = folderLabel:gsub('Folder: ', '') -- Delete default prefix
elseif (string.find(folderLabel, 'Collection: ')) then
folderType = 'collection'
folderType = 'collection';
folderLabel = folderLabel:gsub('Collection: ', '') -- Delete default prefix
folderBgImage = collectionFolderBgImage
folderBgImage = collectionFolderBgImage;
elseif (string.find(folderLabel, 'Level: ')) then
folderType = 'level'
folderType = 'level';
folderLabel = folderLabel:gsub('Level: ', '') -- Delete default prefix
folderLabel = 'LEVEL ' .. folderLabel
folderLabel = 'LEVEL ' .. folderLabel;
end
local labelMatcherString = string.upper(folderLabel)
@ -194,8 +197,8 @@ function getFolderData(folderLabel)
for i, specialFolder in ipairs(specialFolders) do
for i, specialFolderKey in ipairs(specialFolder.keys) do
if (specialFolderKey == labelMatcherString) then
folderBgImage = specialFolder.folderBg
isSpecial = true
folderBgImage = specialFolder.folderBg;
isSpecial = true;
end
end
end
@ -223,51 +226,51 @@ function drawFolder(label, y)
-- Draw the folder label, but only if the folder is not special
if (not folderData.isSpecial) then
gfx.BeginPath()
gfx.BeginPath();
gfx.FontSize(38)
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_MIDDLE)
gfx.FillColor(255, 255, 255, 255)
gfx.Text(folderData.label, x + 18, y + 72)
gfx.FillColor(255, 255, 255, 255);
gfx.Text(folderData.label, x + 18, y + 72);
end
end
function drawFolderList()
gfx.GlobalAlpha(1-transitionLeaveScale)
local numOfItemsAround = 7
local selectedIndex = 1
local folderList = filters.folder
local numOfItemsAround = 7;
local selectedIndex = 1;
local folderList = filters.folder;
if selectionMode == 'folders' then
selectedIndex = selectedFolder
folderList = filters.folder
folderList = filters.folder;
else
selectedIndex = selectedLevel
folderList = filters.level
folderList = filters.level;
end
local yOffset = transitionScrollOffsetY
local yOffset = transitionScrollOffsetY;
local i = 1
local i = 1;
while (i <= numOfItemsAround) do
local index = getCorrectedIndex(selectedIndex, -i)
drawFolder(folderList[index],
desh / 2 - ITEM_HEIGHT / 2 - ITEM_HEIGHT * i + yOffset)
i = i + 1
i = i + 1;
end
-- Draw the selected song
drawFolder(folderList[selectedIndex], desh / 2 - ITEM_HEIGHT / 2 + yOffset)
i = 1
i = 1;
while (i <= numOfItemsAround) do
local index = getCorrectedIndex(selectedIndex, i)
drawFolder(folderList[index],
desh / 2 - ITEM_HEIGHT / 2 + ITEM_HEIGHT * i + yOffset)
i = i + 1
i = i + 1;
end
gfx.GlobalAlpha(1)
gfx.GlobalAlpha(1);
end
function drawCursor()
@ -276,7 +279,7 @@ function drawCursor()
gfx.BeginPath()
local cursorImageIndex = game.GetSkinSetting('_gaugeType')
local cursorImage = cursorImages[cursorImageIndex or 1]
local cursorImage = cursorImages[cursorImageIndex or 1];
gfx.ImageRect(desw / 2 - 14, desh / 2 - 213 / 2, 555, 213, cursorImage, 1, 0)
end
@ -285,39 +288,27 @@ function drawScrollbar()
if not isFilterWheelActive or transitionLeaveScale ~= 0 then return end
gfx.BeginPath()
local resize = 0.85
local lw, lh = gfx.ImageSize(scrollBarBackgroundImage)
local lw = lw * resize
local lh = lh * resize
local xPos = desw-20
local backgroundYPos = desh/2 - lh/2
gfx.ImageRect(xPos, backgroundYPos, lw, lh, scrollBarBackgroundImage, 1, 0)
local bgW = 13*0.85;
local bgH = 1282*0.85;
local scrollPosX = desw-20
local scrollPosY = desh/2-bgH/2
gfx.ImageRect(scrollPosX, scrollPosY, bgW, bgH, scrollbarBgImage, 1, 0)
local total = game.GetSkinSetting('_songWheelScrollbarTotal')
local index = game.GetSkinSetting('_songWheelScrollbarIndex')
if (index == nil) then return end;
gfx.BeginPath()
local sw, sh = gfx.ImageSize(scrollBarFillImage)
local sw = sw * resize
local sh = sh * resize
local fillXPos = xPos - 6
local fillW = 27*0.85
local fillH = 65*0.85
local fillPosOffsetY = (bgH-fillH)*(
(index-1) /
math.max(1,total-1)
)
-- figure out index and total
local index = 1
local total = 1
if selectionMode == 'folders' then
index = selectedFolder
total = #filters.folder
else
index = selectedLevel
total = #filters.level
end
local minScrollYPos = backgroundYPos
local maxScrollYPos = backgroundYPos + lh - sh
local scrollStep = (maxScrollYPos - minScrollYPos) / (total - 1)
local scrollbarYOffset = (index - 1) * scrollStep
local scrollbarYPos = minScrollYPos + scrollbarYOffset
gfx.ImageRect(fillXPos, scrollbarYPos, sw, sh, scrollBarFillImage, 1, 0)
gfx.ImageRect(scrollPosX-6, scrollPosY+fillPosOffsetY, fillW, fillH, scrollbarFillImage, 1, 0)
end
function tickTransitions(deltaTime)
@ -329,10 +320,10 @@ function tickTransitions(deltaTime)
if scrollingUp then
transitionScrollOffsetY = Easing.inQuad(1 - transitionScrollScale) *
ITEM_HEIGHT
ITEM_HEIGHT;
else
transitionScrollOffsetY = Easing.inQuad(1 - transitionScrollScale) *
-ITEM_HEIGHT
-ITEM_HEIGHT;
end
-- LEAVE TRANSITION
@ -342,7 +333,7 @@ function tickTransitions(deltaTime)
else
transitionLeaveScale = 1
end
transitionLeaveReappearTimer = 1
transitionLeaveReappearTimer = 1;
else
if (transitionLeaveReappearTimer == 1) then
-- This stuff happens right after filterwheel is deactivated
@ -351,85 +342,90 @@ function tickTransitions(deltaTime)
transitionLeaveReappearTimer = transitionLeaveReappearTimer - deltaTime / (TRANSITION_LEAVE_DURATION + 0.05) -- same reasoning as in the songwheel
if (transitionLeaveReappearTimer <= 0) then
transitionLeaveScale = 0
transitionLeaveReappearTimer = 0
transitionLeaveScale = 0;
transitionLeaveReappearTimer = 0;
end
end
end
function drawFilterWheelContent(deltatime)
tickTransitions(deltatime)
tickTransitions(deltatime);
drawFolderList()
end
local drawFilterWheel = function (deltaTime)
local drawFilterWheel = function (x,y,w,h, deltaTime)
gfx.Translate(x,y);
gfx.Scale(w/1080, h/1920);
drawFilterWheelContent(deltaTime)
drawCursor()
drawScrollbar()
if (game.GetSkinSetting('_currentScreen') == 'songwheel') then
SongSelectHeader.draw(deltaTime)
SongSelectHeader.draw(deltaTime);
Footer.draw(deltaTime);
end
if (isFilterWheelActive ~= previousActiveState) then
game.PlaySample('filter_wheel/open_close.wav')
previousActiveState = isFilterWheelActive
game.PlaySample('filter_wheel/open_close.wav');
previousActiveState = isFilterWheelActive;
end
-- Debug text
gfx.BeginPath()
gfx.BeginPath();
gfx.FontSize(18)
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_TOP)
gfx.FillColor(255, 255, 255, 255)
gfx.FillColor(255, 255, 255, 255);
if game.GetSkinSetting('debug_showInformation') then
gfx.Text('S_M: ' .. selectionMode .. ' // S_F: ' .. selectedFolder ..
' // S_L: ' .. selectedLevel .. ' // L_TS: ' ..
transitionLeaveScale .. ' // L_TRT: ' .. transitionLeaveReappearTimer, 8, 1870)
transitionLeaveScale .. ' // L_TRT: ' .. transitionLeaveReappearTimer, 8, 1870);
end
end
render = function(deltaTime, shown)
isFilterWheelActive = shown
isFilterWheelActive = shown;
if not shown then
game.SetSkinSetting('_songWheelOverlayActive', 0)
game.SetSkinSetting('_songWheelOverlayActive', 0);
else
game.SetSkinSetting('_songWheelOverlayActive', 1)
game.SetSkinSetting('_songWheelOverlayActive', 1);
end
game.SetSkinSetting('_songWheelActiveFolderLabel', getFolderData(filters.folder[selectedFolder]).label)
game.SetSkinSetting('_songWheelActiveSubFolderLabel', getFolderData(filters.level[selectedLevel]).label)
game.SetSkinSetting('_songWheelActiveFolderLabel', getFolderData(filters.folder[selectedFolder]).label);
game.SetSkinSetting('_songWheelActiveSubFolderLabel', getFolderData(filters.level[selectedLevel]).label);
Dim.updateResolution()
Dim.transformToScreenSpace()
-- detect resolution change
local resx, resy = game.GetResolution();
if resx ~= resX or resy ~= resY then
resolutionChange(resx, resy)
end
gfx.GlobalAlpha(1)
drawFilterWheel(deltaTime)
drawFilterWheel((resX - fullX) / 2, 0, fullX, fullY, deltaTime);
end
set_selection = function(newIndex, isFolder)
local oldIndex = 1
local total = 1
local oldIndex = 1;
local total = 1;
if isFolder then
oldIndex = selectedFolder
selectedFolder = newIndex
total = #filters.folder
total = #filters.folder;
else
oldIndex = selectedLevel
selectedLevel = newIndex
total = #filters.level
total = #filters.level;
end
transitionScrollScale = 0
transitionScrollScale = 0;
scrollingUp = false
scrollingUp = false;
if ((newIndex > oldIndex and not (newIndex == total and oldIndex == 1)) or
(newIndex == 1 and oldIndex == total)) then scrollingUp = true end
(newIndex == 1 and oldIndex == total)) then scrollingUp = true; end
game.PlaySample('song_wheel/cursor_change.wav')
game.PlaySample('song_wheel/cursor_change.wav');
end
set_mode = function(isFolder)

View File

@ -0,0 +1,36 @@
resx,resy = game.GetResolution()
local wheelY = -resy
local bgFade = 0
local yoff = 0
local lastSelected = 0
render = function(deltaTime, shown)
gfx.ResetTransform()
gfx.BeginPath();
gfx.LoadSkinFont("segoeui.ttf");
gfx.TextAlign(gfx.TEXT_ALIGN_CENTER + gfx.TEXT_ALIGN_MIDDLE);
gfx.FontSize(40);
if shown then
bgFade = math.min(bgFade + deltaTime * 10, 1)
wheelY = math.min(wheelY + deltaTime * resy * 10, 0)
else
wheelY = math.max(wheelY - deltaTime * resy * 10, -resy)
bgFade = math.max(bgFade - deltaTime * 10, 0)
end
gfx.FillColor(0,0,0,math.floor(200 * bgFade))
gfx.Rect(0,0,resx,resy)
gfx.Fill()
gfx.BeginPath()
yoff = 0.8 * yoff + (settings.currentSelection - lastSelected)
lastSelected = settings.currentSelection
if bgFade > 0 then
for i,setting in ipairs(settings) do
if i == settings.currentSelection then
gfx.FillColor(255,255,255)
else
gfx.FillColor(70,70,70)
end
gfx.FastText(string.format("%s: %s", setting.name, setting.value), resx/2, resy/2 + 40 * (i - settings.currentSelection + yoff) + wheelY, 40, gfx.TEXT_ALIGN_CENTER + gfx.TEXT_ALIGN_MIDDLE);
end
end
end

View File

@ -0,0 +1,149 @@
local window = {
isPortrait = false,
resX = 0,
resY = 0,
scale = 1,
w = 0,
h = 0,
set = function(this, doScale)
local resX, resY = game.GetResolution();
if ((this.resX ~= resX) or (this.resY ~= this.resY)) then
this.isPortrait = resY > resX;
this.w = (this.isPortrait and 1080) or 1920;
this.h = this.w * (resY / resX);
this.scale = resX / this.w;
this.resX = resX;
this.resY = resY;
end
if (doScale) then gfx.Scale(this.scale, this.scale); end
end,
};
local wheel = {
cache = { w = 0, h = 0 },
visibleSongs = 11,
margin = 13,
x = 0,
y = 0,
w = 0,
h = {
song = 0,
total = 0,
},
setSizes = function(this)
if ((this.cache.w ~= window.w) or (this.cache.h ~= window.h)) then
local marginTotal = this.margin * (this.visibleSongs - 1);
this.x = window.w / 2;
this.y = 0;
this.w = window.w / 2;
this.h.total = window.h - marginTotal;
this.h.song = this.h.total / this.visibleSongs;
this.cache.w = window.w;
this.cache.h = window.h;
end
end,
};
local displaying = {};
local jacketCache = {};
local currDiff = 1;
local currSong = 1;
local jacketFallback = gfx.CreateSkinImage('song_select/loading.png', 0);
local getJacket = function(diff)
if ((not jacketCache[diff.jacketPath])
or (jacketCache[diff.jacketPath] == jacketFallback)) then
jacketCache[diff.jacketPath] = gfx.LoadImageJob(
diff.jacketPath,
jacketFallback,
500,
500
);
end
return jacketCache[diff.jacketPath];
end
local setDisplaying = function()
local songs = songwheel.songs;
local enoughSongs = #songs >= wheel.visibleSongs;
displaying[5] = songs[currSong] or {};
for i = 1, 4 do
if (enoughSongs) then
displaying[5 - i] = songs[currSong - i] or songs[currSong + #songs - i];
else
displaying[5 - i] = songs[currSong - i] or {};
end
end
for i = 1, 3 do
if (enoughSongs) then
displaying[5 + i] = songs[currSong + i] or songs[currSong - #songs + i];
else
displaying[5 + i] = songs[currSong + i] or {};
end
end
end
local renderWheel = function()
local margin = wheel.margin;
local x = wheel.x;
local y = wheel.y;
local w = wheel.w;
local h = wheel.h.song;
for i, song in ipairs(displaying) do
local isSelected = i == 5;
gfx.BeginPath();
gfx.FillColor(0, 0, 0, (isSelected and 200) or 100);
gfx.Rect(x, y, w, h);
gfx.Fill();
if (song and song.difficulties) then
local jacket = getJacket(song.difficulties[currDiff] or song.difficulties[1]);
if (jacket) then
gfx.BeginPath();
gfx.ImageRect(x, y, h, h, jacket, (isSelected and 1) or 0.5, 0);
end
end
y = y + h + margin;
end
end
render = function(dt)
window:set(true);
wheel:setSizes();
setDisplaying();
renderWheel();
gfx.ForceRender();
end
set_index = function(newSong)
currSong = newSong;
end
set_diff = function(newDiff)
currDiff = newDiff;
end
songs_changed = function(withAll)
if (not withAll) then return; end
end

View File

@ -0,0 +1,675 @@
easing = require("easing")
gfx.LoadSkinFont("rounded-mplus-1c-bold.ttf")
game.LoadSkinSample("cursor_song")
game.LoadSkinSample("cursor_difficulty")
local resx, resy = game.GetResolution()
local levelFont = ImageFont.new("font-level", "0123456789")
local diffFont = ImageFont.new("diff_num", "0123456789")
local bpmFont = ImageFont.new("number", "0123456789.") -- FIXME: font-default
local desw, desh;
local resx, resy;
local portrait;
local scale;
function ResetLayoutInformation()
resx, resy = game.GetResolution()
portrait = resy > resx
desw = portrait and 1080 or 1920
desh = desw * (resy / resx)
scale = resx / desw
end
function render(deltaTime)
ResetLayoutInformation()
end
-- Grades
---------
local noGrade = Image.skin("song_select/grade/nograde.png")
local grades = {
{["min"] = 9900000, ["image"] = Image.skin("song_select/grade/s.png")},
{["min"] = 9800000, ["image"] = Image.skin("song_select/grade/aaap.png")},
{["min"] = 9700000, ["image"] = Image.skin("song_select/grade/aaa.png")},
{["min"] = 9500000, ["image"] = Image.skin("song_select/grade/aap.png")},
{["min"] = 9300000, ["image"] = Image.skin("song_select/grade/aa.png")},
{["min"] = 9000000, ["image"] = Image.skin("song_select/grade/ap.png")},
{["min"] = 8700000, ["image"] = Image.skin("song_select/grade/a.png")},
{["min"] = 7500000, ["image"] = Image.skin("song_select/grade/b.png")},
{["min"] = 6500000, ["image"] = Image.skin("song_select/grade/c.png")},
{["min"] = 0, ["image"] = Image.skin("song_select/grade/d.png")},
}
function lookup_grade_image(difficulty)
local gradeImage = noGrade
if difficulty.scores[1] ~= nil then
local highScore = difficulty.scores[1]
for i, v in ipairs(grades) do
if highScore.score >= v.min then
gradeImage = v.image
break
end
end
end
return { image = gradeImage, flicker = (gradeImage == grades[1].image) }
end
-- Medals
---------
local noMedal = Image.skin("song_select/medal/nomedal.png")
local medals = {
Image.skin("song_select/medal/played.png"),
Image.skin("song_select/medal/clear.png"),
Image.skin("song_select/medal/hard.png"),
Image.skin("song_select/medal/uc.png"),
Image.skin("song_select/medal/puc.png")
}
function lookup_medal_image(difficulty)
local medalImage = noMedal
local flicker = false
if difficulty.scores[1] ~= nil then
if difficulty.topBadge ~= 0 then
medalImage = medals[difficulty.topBadge]
if difficulty.topBadge >= 3 then -- hard
flicker = true
end
end
end
return { image = medalImage, flicker = flicker }
end
-- Lookup difficulty
function lookup_difficulty(diffs, diff)
local diffIndex = nil
for i, v in ipairs(diffs) do
if v.difficulty + 1 == diff then
diffIndex = i
end
end
local difficulty = nil
if diffIndex ~= nil then
difficulty = diffs[diffIndex]
end
return difficulty
end
-- JacketCache class
--------------------
JacketCache = {}
JacketCache.new = function()
local this = {
cache = {},
images = {
loading = Image.skin("song_select/loading.png"),
}
}
setmetatable(this, {__index = JacketCache})
return this
end
JacketCache.get = function(this, path)
local jacket = this.cache[path]
if not jacket or jacket == this.images.loading.image then
jacket = gfx.LoadImageJob(path, this.images.loading.image)
this.cache[path] = jacket
end
return Image.wrap(jacket)
end
-- SongData class
-----------------
SongData = {}
SongData.new = function(jacketCache)
local this = {
selectedIndex = 1,
selectedDifficulty = 0,
memo = Memo.new(),
jacketCache = jacketCache,
images = {
dataBg = Image.skin("song_select/data_bg.png"),
fg = Image.skin("song_select/fg.png"),
cursor = Image.skin("song_select/level_cursor.png"),
none = Image.skin("song_select/level/none.png"),
difficulties = {
Image.skin("song_select/level/novice.png"),
Image.skin("song_select/level/advanced.png"),
Image.skin("song_select/level/exhaust.png"),
Image.skin("song_select/level/maximum.png"),
Image.skin("song_select/level/infinite.png"),
Image.skin("song_select/level/gravity.png"),
Image.skin("song_select/level/heavenly.png"),
Image.skin("song_select/level/vivid.png")
},
}
}
setmetatable(this, {__index = SongData})
return this
end
SongData.render = function(this, deltaTime)
local song = songwheel.songs[this.selectedIndex]
if not song then return end
-- Lookup difficulty
local diff = song.difficulties[this.selectedDifficulty]
if diff == nil then diff = song.difficulties[1] end
-- Draw the background
this.images.dataBg:draw({ x = desw / 2, y = desh / 2, w = 1080 ,h = 1920})
-- Draw the jacket
local jacket = this.jacketCache:get(diff.jacketPath)
jacket:draw({ x = 97, y = 326, w = 346, h = 346, anchor_h = Image.ANCHOR_LEFT, anchor_v = Image.ANCHOR_TOP })
-- Draw the title
local title = this.memo:memoize(string.format("title_%s", song.id), function ()
gfx.LoadSkinFont("NotoSans-Regular.ttf")
return gfx.CreateLabel(song.title, 24, 0)
end)
gfx.FillColor(255, 255, 255, 255)
gfx.DrawLabel(title, 32, desh / 2 - 4, 390)
-- Draw the artist
local artist = this.memo:memoize(string.format("artist_%s", song.id), function ()
gfx.LoadSkinFont("NotoSans-Regular.ttf")
return gfx.CreateLabel(song.artist, 24, 0)
end)
gfx.FillColor(255, 255, 255, 255)
gfx.DrawLabel(artist, 32, desh / 2 + 42, 390)
-- Draw the effector
local effector = this.memo:memoize(string.format("eff_%s_%s", song.id, diff.id), function ()
gfx.LoadSkinFont("NotoSans-Regular.ttf")
return gfx.CreateLabel(diff.effector, 16, 0)
end)
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_BASELINE)
gfx.FillColor(255, 255, 255, 255)
gfx.DrawLabel(effector, 260, desh / 2 + 208, 320)
-- Draw the illustrator
if diff.illustrator then
local illustrator = this.memo:memoize(string.format("ill_%s_%s", song.id, diff.id), function ()
gfx.LoadSkinFont("NotoSans-Regular.ttf")
return gfx.CreateLabel(diff.illustrator, 16, 0)
end)
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_BASELINE)
gfx.FillColor(255, 255, 255, 255)
gfx.DrawLabel(illustrator, 260, desh / 2 + 238, 320)
end
-- Draw the bpm
gfx.LoadSkinFont("Digital-Serial-Bold.ttf")
gfx.FontSize(24)
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_BASELINE)
gfx.FillColor(255, 255, 255, 255)
gfx.Text(song.bpm, 75, desh / 2 - 34)
this:draw_cursor(diff.difficulty)
-- Draw the hi-score
local hiScore = diff.scores[1]
if hiScore then
-- FIXME: large / small font
local scoreText = string.format("%08d", hiScore.score)
levelFont:draw(scoreText, 362, 220, 1, gfx.TEXT_ALIGN_LEFT, gfx.TEXT_ALIGN_MIDDLE)
-- local scoreHiText = string.format("%04d", math.floor(hiScore.score / 1000))
-- levelFont:draw(scoreHiText, 362, 220, 1, gfx.TEXT_ALIGN_LEFT, gfx.TEXT_ALIGN_MIDDLE)
-- local scoreLoText = string.format("%04d", hiScore.score % 1000)
-- bpmFont:draw(scoreLoText, 470, 220, 1, gfx.TEXT_ALIGN_LEFT, gfx.TEXT_ALIGN_MIDDLE)
end
-- Draw the grade and medal
local grade = lookup_grade_image(diff)
grade.image:draw({ x = desw / 2 - 157, y = desh / 2 - 162, scale = 0.85, alpha = grade.flicker and glowState and 0.9 or 1 })
local medal = lookup_medal_image(diff)
medal.image:draw({ x = desw / 2 - 72, y = desh / 2 - 199, scale = 0.86, alpha = medal.flicker and glowState and 0.9 or 1})
for i = 1, 4 do
local d = lookup_difficulty(song.difficulties, i)
this:draw_difficulty(i - 1, d, jacket)
end
end
SongData.draw_title_artist = function(this, label, x, y, maxWidth)
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_BASELINE)
gfx.FillColor(55, 55, 55, 64)
gfx.DrawLabel(label, x + 2, y + 2, maxWidth)
gfx.FillColor(55, 55, 55, 255)
gfx.DrawLabel(label, x, y, maxWidth)
end
SongData.set_index = function(this, newIndex)
this.selectedIndex = newIndex
end
SongData.draw_cursor = function(this, index)
local x = 98
local y = desh / 2 + 133
-- Draw the cursor
this.images.cursor:draw({ x = x + index * 115, y = y, scale = 0.85 })
end
SongData.set_difficulty = function(this, newDiff)
this.selectedDifficulty = newDiff
end
SongData.draw_difficulty = function(this, index, diff, jacket)
local x = 98
local y = desh / 2 + 135
-- Draw the jacket icon
local jacket = this.jacketCache.images.loading
if diff ~= nil then jacket = this.jacketCache:get(diff.jacketPath) end
if diff == nil then
this.images.none:draw({ x = x + index * 115, y = y - 600, scale = 0.78})
else
-- Draw the background
this.images.difficulties[diff.difficulty + 1]:draw({ x = x + index * 115, y = y, scale = 0.78})
-- Draw the level
local levelText = string.format("%02d", diff.level)
diffFont:draw(levelText, x + index * 115, y - 20, 1, gfx.TEXT_ALIGN_CENTER, gfx.TEXT_ALIGN_MIDDLE)
end
end
-- SongTable class
------------------
SongTable = {}
SongTable.new = function(jacketCache)
local this = {
cols = 1,
rows = 11,
selectedIndex = 1,
selectedDifficulty = 0,
rowOffset = 0, -- song index offset of top-left song in page
cursorPos = 0, -- cursor position in page [0..cols * rows)
displayCursorPos = 0,
cursorAnim = 0,
cursorAnimTotal = 0.1,
memo = Memo.new(),
jacketCache = jacketCache,
images = {
matchingBg = Image.skin("song_select/matching_bg.png"),
scoreBg = Image.skin("song_select/score_bg.png"),
force = Image.skin("song_select/force.png"),
cursor = Image.skin("song_select/cursor.png"),
cursorText = Image.skin("song_select/cursor_text.png"),
cursorDiamond = Image.skin("song_select/cursor_diamond.png"),
cursorDiamondWire = Image.skin("song_select/cursor_diamond_wire.png"),
plates = {
Image.skin("song_select/plate/novice.png"),
Image.skin("song_select/plate/advanced.png"),
Image.skin("song_select/plate/exhaust.png"),
Image.skin("song_select/plate/maximum.png"),
Image.skin("song_select/plate/infinite.png"),
Image.skin("song_select/plate/gravity.png"),
Image.skin("song_select/plate/heavenly.png"),
Image.skin("song_select/plate/vivid.png")
}
}
}
setmetatable(this, {__index = SongTable})
return this
end
SongTable.calc_cursor_point = function(this, pos)
local col = pos % this.cols
local row = math.floor((pos) / this.cols)
local x = desw * 0.75 + col * this.images.cursor.w
local y = 0 + row * this.images.cursor.h
return x, y
end
SongTable.set_index = function(this, newIndex)
if newIndex ~= this.selectedIndex then
game.PlaySample("cursor_song")
end
local delta = newIndex - this.selectedIndex
if delta < -1 or delta > 1 then
local newOffset = newIndex - 1
this.rowOffset = math.floor((newIndex - 1) / this.cols) * this.cols
this.cursorPos = (newIndex - 1) - this.rowOffset
this.displayCursorPos = this.cursorPos
else
local newCursorPos = this.cursorPos + delta
if newCursorPos < 0 then
-- scroll up
this.rowOffset = this.rowOffset - this.cols
if this.rowOffset < 0 then
-- this.rowOffset = math.floor(#songwheel.songs / this.cols)
end
newCursorPos = newCursorPos + this.cols
elseif newCursorPos >= this.cols * this.rows then
-- scroll down
this.rowOffset = this.rowOffset + this.cols
newCursorPos = newCursorPos - this.cols
else
-- no scroll, move cursor in page
end
if this.cursorAnim > 0 then
this.displayCursorPos = easing.outQuad(0.5 - this.cursorAnim, this.displayCursorPos, this.cursorPos - this.displayCursorPos, 0.5)
end
this.cursorPos = newCursorPos
this.cursorAnim = this.cursorAnimTotal
end
this.selectedIndex = newIndex
end
SongTable.set_difficulty = function(this, newDiff)
if newDiff ~= this.selectedDifficulty then
game.PlaySample("cursor_difficulty")
end
this.selectedDifficulty = newDiff
end
SongTable.render = function(this, deltaTime)
this:draw_songs()
this:draw_cursor(deltaTime)
end
SongTable.draw_songs = function(this)
for i = 1, this.cols * this.rows do
if this.rowOffset + i <= #songwheel.songs then
this:draw_song(i - 1, this.rowOffset + i)
end
end
end
-- Draw the song plate
SongTable.draw_song = function(this, pos, songIndex)
local song = songwheel.songs[songIndex]
if not song then return end
-- Lookup difficulty
local diff = song.difficulties[this.selectedDifficulty]
if diff == nil then diff = song.difficulties[1] end
local x, y = this:calc_cursor_point(pos)
x = x + 4
y = y + 16
-- Draw the jacket
local jacket = this.jacketCache:get(diff.jacketPath)
jacket:draw({ x = x - 24, y = y - 21, w = 122, h = 122 })
-- Draw the background
gfx.FillColor(255, 255, 255)
this.images.scoreBg:draw({ x = x + 72, y = y + 16 })
if diff.force and diff.force > 0 then
this.images.matchingBg:draw({ x = x + 72, y = y - 62 })
end
this.images.plates[diff.difficulty + 1]:draw({ x = x, y = y })
-- Draw the title
local title = this.memo:memoize(string.format("title_%s", song.id), function ()
gfx.LoadSkinFont("rounded-mplus-1c-bold.ttf")
return gfx.CreateLabel(song.title, 14, 0)
end)
gfx.TextAlign(gfx.TEXT_ALIGN_CENTER + gfx.TEXT_ALIGN_BASELINE)
gfx.DrawLabel(title, x - 22, y + 53, 125)
-- Draw the grade and medal
local grade = lookup_grade_image(diff)
grade.image:draw({ x = x + 78, y = y - 23, alpha = grade.flicker and glowState and 0.9 or 1 })
local medal = lookup_medal_image(diff)
medal.image:draw({ x = x + 78, y = y + 10, alpha = medal.flicker and glowState and 0.9 or 1 })
-- Draw the level
local levelText = string.format("%02d", diff.level)
levelFont:draw(levelText, x + 72, y + 56, 1, gfx.TEXT_ALIGN_CENTER, gfx.TEXT_ALIGN_MIDDLE)
-- Draw the volforce
--if diff.force and diff.force > 0 then
--local forceText = string.format("%d", math.floor(diff.force * 100))
--bpmFont:draw(forceText, x + , y - 60, 1, gfx.TEXT_ALIGN_CENTER, gfx.TEXT_ALIGN_MIDDLE)
--end
--if diff.forceInTotal then
--this.images.force:draw({x = x - 75, y = y - 60, w = 59, h = 59 })
--end
end
-- Draw the song cursor
SongTable.draw_cursor = function(this, deltaTime)
gfx.Save()
local pos = this.displayCursorPos
if this.cursorAnim > 0 then
this.cursorAnim = this.cursorAnim - deltaTime
if this.cursorAnim <= 0 then
this.displayCursorPos = this.cursorPos
pos = this.cursorPos
else
pos = easing.outQuad(this.cursorAnimTotal - this.cursorAnim, this.displayCursorPos, this.cursorPos - this.displayCursorPos, this.cursorAnimTotal)
end
end
local x, y = this:calc_cursor_point(pos)
gfx.FillColor(255, 255, 255)
local t = currentTime % 1
-- scroll text
gfx.Scissor(
x - this.images.cursor.w / 2, y - (this.images.cursor.h - 30) / 2,
this.images.cursor.w, this.images.cursor.h - 30)
local offset = (currentTime * 50) % 290
local alpha = glowState and 0.8 or 1
this.images.cursorText:draw({ x = x + 96, y = y + offset, alpha = alpha })
this.images.cursorText:draw({ x = x + 96, y = y - 290 + offset, alpha = alpha })
this.images.cursorText:draw({ x = x - 96, y = y + offset, alpha = alpha })
this.images.cursorText:draw({ x = x - 96, y = y - 290 + offset, alpha = alpha })
gfx.ResetScissor()
-- diamong wireframe
local h = (this.images.cursorDiamondWire.h * 1.5) * easing.outQuad(t * 2, 0, 1, 1)
this.images.cursorDiamondWire:draw({ x = x, y = y, w = this.images.cursorDiamondWire.w * 1.5, h = h, alpha = 0.5 })
-- ghost cursor
alpha = easing.outSine(t, 1, -1, 1)
h = this.images.cursor.h * easing.outSine(t, 0, 1, 1)
this.images.cursor:draw({ x = x, y = y, h = h, alpha = alpha })
-- concrete cursor
-- local w = this.images.cursor.w * easing.outSine(t, 1, 0.05, 0.5)
this.images.cursor:draw({ x = x, y = y, alpha = glowState and 0.8 or 1 })
-- diamond knot
gfx.GlobalCompositeOperation(gfx.BLEND_OP_LIGHTER)
this.images.cursorDiamond:draw({ x = x + 100, y = y, alpha = 1 })
this.images.cursorDiamond:draw({ x = x - 100, y = y, alpha = 1 })
local s = this.images.cursorDiamond.w / 1.5
this.images.cursorDiamond:draw({ x = x + 90 + easing.outQuad(t, 0, -4, 0.5), y = y, w = s, h = s, alpha = 0.5 })
this.images.cursorDiamond:draw({ x = x - 90 - easing.outQuad(t, 0, -4, 0.5), y = y, w = s, h = s, alpha = 0.5 })
gfx.Restore()
end
-- main
-------
local jacketCache = JacketCache.new()
local songData = SongData.new(jacketCache)
local songTable = SongTable.new(jacketCache)
glowState = false
currentTime = 0
-- Callback
get_page_size = function()
return 12
end
searchIndex = 1
soffset = 0
searchText = gfx.CreateLabel("", 5, 0)
draw_search = function(x,y,w,h)
soffset = soffset + (searchIndex) - (songwheel.searchInputActive and 0 or 1)
if searchIndex ~= (songwheel.searchInputActive and 0 or 1) then
game.PlaySample("woosh")
end
searchIndex = songwheel.searchInputActive and 0 or 1
gfx.BeginPath()
local bgfade = 1 - (searchIndex + soffset)
--if not songwheel.searchInputActive then bgfade = soffset end
gfx.FillColor(0,0,0,math.floor(200 * bgfade))
gfx.Rect(0,0,resx,resy)
gfx.Fill()
gfx.ForceRender()
local xpos = x + (searchIndex + soffset)*w
gfx.UpdateLabel(searchText ,string.format("Search: %s",songwheel.searchText), 30, 0)
gfx.BeginPath()
gfx.RoundedRect(xpos,y,w,h,h/2)
gfx.FillColor(30,30,30)
gfx.StrokeColor(0,128,255)
gfx.StrokeWidth(1)
gfx.Fill()
gfx.Stroke()
gfx.BeginPath();
gfx.LoadSkinFont("segoeui.ttf");
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_MIDDLE);
gfx.DrawLabel(searchText, xpos+10,y+(h/2), w-20)
end
-- Callback
function render(deltaTime)
ResetLayoutInformation()
if ((math.floor(currentTime * 1000) % 100) < 50) then
glowState = false
else
glowState = true
end
local xshift = (resx - desw * scale) / 2
local yshift = (resy - desh * scale) / 2
gfx.Translate(xshift, yshift)
--gfx.Scale(scale, scale)
songData:render(deltaTime)
songTable:render(deltaTime)
--if totalForce then
--local forceText = string.format("%.2f", totalForce)
-- gfx.SetImageTint(255, 254, 2)
--bpmFont:draw(forceText, 140, 353, 1, gfx.TEXT_ALIGN_LEFT, gfx.TEXT_ALIGN_MIDDLE)
--end
-- Draw the search status
if songwheel.searchStatus then
gfx.BeginPath()
gfx.LoadSkinFont("segoeui.ttf")
gfx.FillColor(255, 255, 255)
gfx.FontSize(20)
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_BOTTOM)
gfx.Text(songwheel.searchStatus, 3, desh)
end
soffset = soffset * 0.8
draw_search(120, 5, 600, 40)
end
-- Callback
set_index = function(newIndex)
songData:set_index(newIndex)
songTable:set_index(newIndex)
end
-- Callback
set_diff = function(newDiff)
songData:set_difficulty(newDiff)
songTable:set_difficulty(newDiff)
end
-- force calculation
--------------------
totalForce = nil
local badgeRates = {
0.5, -- Played
1.0, -- Cleared
1.02, -- Hard clear
1.04, -- UC
1.1 -- PUC
}
local gradeRates = {
{["min"] = 9900000, ["rate"] = 1.05}, -- S
{["min"] = 9800000, ["rate"] = 1.02}, -- AAA+
{["min"] = 9700000, ["rate"] = 1}, -- AAA
{["min"] = 9500000, ["rate"] = 0.97}, -- AA+
{["min"] = 9300000, ["rate"] = 0.94}, -- AA
{["min"] = 9000000, ["rate"] = 0.91}, -- A+
{["min"] = 8700000, ["rate"] = 0.88}, -- A
{["min"] = 7500000, ["rate"] = 0.85}, -- B
{["min"] = 6500000, ["rate"] = 0.82}, -- C
{["min"] = 0, ["rate"] = 0.8} -- D
}
calculate_force = function(diff)
if #diff.scores < 1 then
return 0
end
local score = diff.scores[1]
local badgeRate = badgeRates[diff.topBadge]
local gradeRate
for i, v in ipairs(gradeRates) do
if score.score >= v.min then
gradeRate = v.rate
break
end
end
return math.floor((diff.level * 2) * (score.score / 10000000) * gradeRate * badgeRate) / 100
end
-- callback
songs_changed = function(withAll)
if (not withAll) then return end
local diffsById = {}
local diffs = {}
for i = 1, #songwheel.allSongs do
local song = songwheel.allSongs[i]
for j = 1, #song.difficulties do
local diff = song.difficulties[j]
diff.force = calculate_force(diff)
table.insert(diffs, diff)
diffsById[diff.id] = diff
end
end
table.sort(diffs, function (l, r)
return l.force > r.force
end)
totalForce = 0
for i = 1, 50 do
if diffs[i] then
totalForce = totalForce + diffs[i].force
diffs[i].forceInTotal = true
end
end
for i = 1, #songwheel.songs do
local song = songwheel.songs[i]
for j = 1, #song.difficulties do
local diff = song.difficulties[j]
local newDiff = diffsById[diff.id]
song.difficulties[j] = newDiff
end
end
end

View File

@ -0,0 +1,894 @@
--Horizontal alignment
TEXT_ALIGN_LEFT = 1
TEXT_ALIGN_CENTER = 2
TEXT_ALIGN_RIGHT = 4
--Vertical alignment
TEXT_ALIGN_TOP = 8
TEXT_ALIGN_MIDDLE = 16
TEXT_ALIGN_BOTTOM = 32
TEXT_ALIGN_BASELINE = 64
local jacket = nil;
local selectedIndex = 1
local selectedDiff = 1
local songCache = {}
local ioffset = 0
local doffset = 0
local soffset = 0
local diffColors = {{0,0,255}, {0,255,0}, {255,0,0}, {255, 0, 255}}
local timer = 0
local effector = 0
local searchText = gfx.CreateLabel("",5,0)
local searchIndex = 1
local jacketFallback = gfx.CreateSkinImage("song_select/loading.png", 0)
local showGuide = game.GetSkinSetting("show_guide")
local legendTable = {
{["labelSingleLine"] = gfx.CreateLabel("DIFFICULTY SELECT",16, 0), ["labelMultiLine"] = gfx.CreateLabel("DIFFICULTY\nSELECT",16, 0), ["image"] = gfx.CreateSkinImage("legend/knob-left.png", 0)},
{["labelSingleLine"] = gfx.CreateLabel("MUSIC SELECT",16, 0), ["labelMultiLine"] = gfx.CreateLabel("MUSIC\nSELECT",16, 0), ["image"] = gfx.CreateSkinImage("legend/knob-right.png", 0)},
{["labelSingleLine"] = gfx.CreateLabel("FILTER MUSIC",16, 0), ["labelMultiLine"] = gfx.CreateLabel("FILTER\nMUSIC",16, 0), ["image"] = gfx.CreateSkinImage("legend/FX-L.png", 0)},
{["labelSingleLine"] = gfx.CreateLabel("SORT MUSIC",16, 0), ["labelMultiLine"] = gfx.CreateLabel("SORT\nMUSIC",16, 0), ["image"] = gfx.CreateSkinImage("legend/FX-R.png", 0)},
{["labelSingleLine"] = gfx.CreateLabel("MUSIC MODS",16, 0), ["labelMultiLine"] = gfx.CreateLabel("MUSIC\nMODS",16, 0), ["image"] = gfx.CreateSkinImage("legend/FX-LR.png", 0)},
{["labelSingleLine"] = gfx.CreateLabel("PLAY",16, 0), ["labelMultiLine"] = gfx.CreateLabel("PLAY",16, 0), ["image"] = gfx.CreateSkinImage("legend/start.png", 0)}
}
local grades = {
{["max"] = 6999999, ["image"] = gfx.CreateSkinImage("common/grades/D.png", 0)},
{["max"] = 7999999, ["image"] = gfx.CreateSkinImage("common/grades/C.png", 0)},
{["max"] = 8699999, ["image"] = gfx.CreateSkinImage("common/grades/B.png", 0)},
{["max"] = 8999999, ["image"] = gfx.CreateSkinImage("common/grades/A.png", 0)},
{["max"] = 9299999, ["image"] = gfx.CreateSkinImage("common/grades/A+.png", 0)},
{["max"] = 9499999, ["image"] = gfx.CreateSkinImage("common/grades/AA.png", 0)},
{["max"] = 9699999, ["image"] = gfx.CreateSkinImage("common/grades/AA+.png", 0)},
{["max"] = 9799999, ["image"] = gfx.CreateSkinImage("common/grades/AAA.png", 0)},
{["max"] = 9899999, ["image"] = gfx.CreateSkinImage("common/grades/AAA+.png", 0)},
{["max"] = 99999999, ["image"] = gfx.CreateSkinImage("common/grades/S.png", 0)}
}
local badges = {
gfx.CreateSkinImage("badges/played.png", 0),
gfx.CreateSkinImage("badges/clear.png", 0),
gfx.CreateSkinImage("badges/hard-clear.png", 0),
gfx.CreateSkinImage("badges/full-combo.png", 0),
gfx.CreateSkinImage("badges/perfect.png", 0)
}
local foreground = gfx.CreateSkinImage("song_select/fg.png", 0);
local recordCache = {}
gfx.LoadSkinFont("dfmarugoth.ttf");
game.LoadSkinSample("menu_click")
game.LoadSkinSample("click-02")
game.LoadSkinSample("woosh")
local wheelSize = 12
get_page_size = function()
return math.floor(wheelSize/2)
end
-- Responsive UI variables
-- Aspect Ratios
local aspectFloat = 1.850
local aspectRatio = "widescreen"
local landscapeWidescreenRatio = 1.850
local landscapeStandardRatio = 1.500
local portraitWidescreenRatio = 0.5
-- Responsive sizes
local fifthX = 0
local fourthX= 0
local thirdX = 0
local halfX = 0
local fullX = 0
local fifthY = 0
local fourthY= 0
local thirdY = 0
local halfY = 0
local fullY = 0
adjustScreen = function(x,y)
local a = x/y;
if x >= y and a <= landscapeStandardRatio then
aspectRatio = "landscapeStandard"
aspectFloat = 1.1
elseif x >= y and landscapeStandardRatio <= a and a <= landscapeWidescreenRatio then
aspectRatio = "landscapeWidescreen"
aspectFloat = 1.2
elseif x <= y and portraitWidescreenRatio <= a and a < landscapeStandardRatio then
aspectRatio = "PortraitWidescreen"
aspectFloat = 0.5
else
aspectRatio = "landscapeWidescreen"
aspectFloat = 1.0
end
fifthX = x/5
fourthX= x/4
thirdX = x/3
halfX = x/2
fullX = x
fifthY = y/5
fourthY= y/4
thirdY = y/3
halfY = y/2
fullY = y
end
check_or_create_cache = function(song, loadJacket)
if not songCache[song.id] then songCache[song.id] = {} end
if not songCache[song.id]["title"] then
songCache[song.id]["title"] = gfx.CreateLabel(song.title, 14, 0)
end
if not songCache[song.id]["artist"] then
songCache[song.id]["artist"] = gfx.CreateLabel(song.artist, 14, 0)
end
if not songCache[song.id]["bpm"] then
songCache[song.id]["bpm"] = gfx.CreateLabel(string.format("%s",song.bpm), 12, 0)
end
if not songCache[song.id]["effector"] then
songCache[song.id]["effector"] = gfx.CreateLabel(string.format("BPM: %s",song.bpm), 20, 0)
end
if not songCache[song.id]["jacket"] then
songCache[song.id]["jacket"] = { }
end
for i = 1, #song.difficulties do
songCache[song.id]["jacket"][i] = gfx.LoadImageJob(song.difficulties[i].jacketPath, jacketFallback, 200, 200)
end
end
function record_handler_factory(hash)
return (function(res)
if res.statusCode == 42 then
recordCache[hash] = {good=false, reason="Untracked"}
elseif res.statusCode == 20 and res.body ~= nil then
recordCache[hash] = {good=true, record=res.body.record}
elseif res.statusCode == 44 then
recordCache[hash] = {good=true, record=nil}
else
recordCache[hash] = {good=false, reason="Failed"}
end
end)
end
function get_record(hash)
if recordCache[hash] then return recordCache[hash] end
recordCache[hash] = {good=false, reason="Loading..."}
IR.Record(hash, record_handler_factory(hash))
return recordCache[hash]
end
function log_table(table)
str = "{"
for k, v in pairs(table) do
str = str .. k .. ": "
t = type(v)
if t == "table" then
str = str .. log_table(v)
elseif t == "string" then
str = str .. "\"" .. v .. "\""
elseif t == "boolean" then
if v then
str = str .. "true"
else
str = str .. "false"
end
else
str = str .. v
end
str = str .. ", "
end
return str .. "}"
end
draw_scores_ir = function(difficulty, x, y, w, h)
-- draw the top score for this difficulty
local xOffset = 5
local height = h/3 - 10
local ySpacing = h/3
local yOffset = h/3
gfx.FontSize(30);
gfx.TextAlign(gfx.TEXT_ALIGN_BOTTOM + gfx.TEXT_ALIGN_CENTER);
gfx.FastText("HIGH SCORE", x +(w/4), y+(h/2))
gfx.FastText("IR RECORD", x + (3/4 * w), y + (h/2))
gfx.BeginPath()
gfx.Rect(x+xOffset,y+h/2,w/2-(xOffset*2),h/2)
gfx.FillColor(30,30,30,10)
gfx.StrokeColor(0,128,255)
gfx.StrokeWidth(1)
gfx.Fill()
gfx.Stroke()
gfx.BeginPath()
gfx.Rect(x + xOffset + w/2,y+h/2,w/2-(xOffset*2),h/2)
gfx.FillColor(30,30,30,10)
gfx.StrokeColor(0,128,255)
gfx.StrokeWidth(1)
gfx.Fill()
gfx.Stroke()
if difficulty.scores[1] ~= nil then
local highScore = difficulty.scores[1]
scoreLabel = gfx.CreateLabel(string.format("%08d",highScore.score), 40, 0)
for i,v in ipairs(grades) do
if v.max > highScore.score then
gfx.BeginPath()
iw,ih = gfx.ImageSize(v.image)
iarr = ih / iw
oldheight = h/2 - 10
newheight = iarr * (h/2-10)
centreoffset = (oldheight - newheight)/2 + 3 -- +3 is stupid but ehhh
gfx.ImageRect(x+xOffset, y+h/2 + centreoffset, oldheight, newheight, v.image, 1, 0) --this is nasty but it works for me
break
end
end
if difficulty.topBadge ~= 0 then
gfx.BeginPath()
gfx.ImageRect(x+xOffset+w/2-h/2, y+h/2 +5, (h/2-10), h/2-10, badges[difficulty.topBadge], 1, 0)
end
gfx.FillColor(255,255,255)
gfx.FontSize(40);
gfx.TextAlign(gfx.TEXT_ALIGN_MIDDLE + gfx.TEXT_ALIGN_CENTER);
gfx.DrawLabel(scoreLabel, x+(w/4),y+(h/4)*3,w/2)
end
irRecord = get_record(difficulty.hash)
if not irRecord.good then
recordLabel = gfx.CreateLabel(irRecord.reason, 40, 0)
gfx.FillColor(255, 255, 255)
gfx.FontSize(40)
gfx.TextAlign(gfx.TEXT_ALIGN_MIDDLE + gfx.TEXT_ALIGN_CENTER);
gfx.DrawLabel(recordLabel, x+(w * 3/4),y+(h/4)*3,w/2)
elseif irRecord.record == nil then --record not set, but can be tracked
recordLabel = gfx.CreateLabel(string.format("%08d", 0), 40, 0)
gfx.FillColor(170, 170, 170)
gfx.FontSize(40)
gfx.TextAlign(gfx.TEXT_ALIGN_MIDDLE + gfx.TEXT_ALIGN_CENTER);
gfx.DrawLabel(recordLabel, x+(w * 3/4),y+(h/4)*3,w/2)
else
recordScoreLabel = gfx.CreateLabel(string.format("%08d", irRecord.record.score), 26, 0)
recordPlayerLabel = gfx.CreateLabel(irRecord.record.username, 26, 0)
if irRecord.record.lamp ~= 0 then
gfx.BeginPath()
gfx.ImageRect(x+xOffset+w-h/2, y+h/2 +5, (h/2-10), h/2-10, badges[irRecord.record.lamp], 1, 0)
end
for i,v in ipairs(grades) do
if v.max > irRecord.record.score then
gfx.BeginPath()
iw,ih = gfx.ImageSize(v.image)
iarr = ih / iw
oldheight = h/2 - 10
newheight = iarr * (h/2-10)
centreoffset = (oldheight - newheight)/2 + 3 -- +3 is stupid but ehhh
gfx.ImageRect(x+xOffset+w/2, y+h/2 + centreoffset, oldheight, newheight, v.image, 1, 0) --this is nasty but it works for me
break
end
end
gfx.FillColor(255, 255, 255)
gfx.FontSize(40)
gfx.TextAlign(gfx.TEXT_ALIGN_MIDDLE + gfx.TEXT_ALIGN_CENTER);
gfx.DrawLabel(recordPlayerLabel, x+(w * 3/4),y+(h/4)*2.55,w/2)
gfx.DrawLabel(recordScoreLabel, x+(w * 3/4),y+(h/4)*3.45,w/2)
end
end
draw_scores = function(difficulty, x, y, w, h)
if IRData.Active then return draw_scores_ir(difficulty, x, y, w, h) end
-- draw the top score for this difficulty
local xOffset = 5
local height = h/3 - 10
local ySpacing = h/3
local yOffset = h/3
gfx.FontSize(30);
gfx.TextAlign(gfx.TEXT_ALIGN_BOTTOM + gfx.TEXT_ALIGN_CENTER);
gfx.BeginPath()
gfx.FillColor(30,30,30,10)
gfx.StrokeColor(0,128,255)
gfx.StrokeWidth(1)
gfx.Fill()
gfx.Stroke()
if difficulty.scores[1] ~= nil then
local highScore = difficulty.scores[1]
scoreLabel = gfx.CreateLabel(string.format("%08d",highScore.score), 40, 0)
for i,v in ipairs(grades) do
if v.max > highScore.score then
gfx.BeginPath()
iw,ih = gfx.ImageSize(v.image)
iar = iw / ih;
--gfx.ImageRect(x+xOffset,y+h/2 +5, iar * (h/2-10),h/2-10, v.image, 1, 0)
break
end
end
if difficulty.topBadge ~= 0 then
gfx.BeginPath()
--gfx.ImageRect(x+xOffset+w-h/2, y+h/2 +5, (h/2-10), h/2-10, badges[difficulty.topBadge], 1, 0)
end
gfx.FillColor(255,255,255)
gfx.FontSize(40);
gfx.TextAlign(gfx.TEXT_ALIGN_BOTTOM + gfx.TEXT_ALIGN_LEFT);
gfx.DrawLabel(scoreLabel, x/11,y/1.48,w*2)
end
end
draw_song = function(song, x, y, w, h, selected)
local diffIndex = math.min(selectedDiff, #song.difficulties)
local difficulty = song.difficulties[diffIndex]
local clearLampR = 255
local clearLampG = 255
local clearLampB = 255
local clearLampA = 100
if difficulty ~= nil then
if difficulty.scores[1] ~= nil then
if difficulty.topBadge == 1 then -- fail/played
clearLampR = 255
clearLampG = 25
clearLampB = 25
clearLampA = 200
end
if difficulty.topBadge == 2 then -- clear
clearLampR = 25
clearLampG = 255
clearLampB = 25
clearLampA = 200
end
if difficulty.topBadge == 3 then -- hard clear
clearLampR = 255
clearLampG = 25
clearLampB = 255
clearLampA = 200
end
if difficulty.topBadge == 4 then -- full combo
clearLampR = 255
clearLampG = 100
clearLampB = 25
clearLampA = 200
end
if difficulty.topBadge == 5 then -- perfect
clearLampR = 255
clearLampG = 255
clearLampB = 25
clearLampA = 200
end
end
end
check_or_create_cache(song)
gfx.BeginPath()
gfx.Rect(x+1,y+1, w-2, h-2)
gfx.FillColor(220,220,220)
gfx.StrokeColor(0,8,0)
gfx.StrokeWidth(2)
gfx.Fill()
gfx.Stroke()
gfx.FillColor(255,255,255)
if songCache[song.id]["jacket"][diffIndex] then
gfx.BeginPath()
gfx.ImageRect(x+2, y+2, h-4, h-4, songCache[song.id]["jacket"][diffIndex], 1, 0)
end
-- Song title
gfx.BeginPath()
gfx.Rect(x+1, y + h - h/4 - 1, w-2, h/4)
gfx.FillColor(0,0,0,200)
gfx.Fill()
gfx.BeginPath()
gfx.FillColor(255,255,255)
gfx.TextAlign(gfx.TEXT_ALIGN_BOTTOM + gfx.TEXT_ALIGN_LEFT)
gfx.DrawLabel(songCache[song.id]["title"], (x)+h/2 + 4, y + h - 7, -1)
--gfx.DrawLabel(songCache[song.id]["artist"], x+10, y + 50, w-10)
-- Song difficulty
gfx.BeginPath()
gfx.Rect(x - 1, y + h-h/2 - 4, h/2, h/2)
gfx.FillColor(0,0,0,200)
gfx.Fill()
gfx.FillColor(255,255,255)
gfx.LoadSkinFont("commext.ttf")
gfx.FontSize(28)
gfx.TextAlign(gfx.TEXT_ALIGN_CENTER + gfx.TEXT_ALIGN_BOTTOM)
if (song.difficulties[selectedDiff] ~= nil) then
gfx.FastText(song.difficulties[selectedDiff].level, x + h/4, y + h - 10)
else
gfx.FastText(song.difficulties[selectedDiff - 1].level, x + h/4, y + h - 10)
end
-- CLEAN THIS SHIT UP
local diff_long = ""
local diff_short = ""
if (song.difficulties[selectedDiff] ~= nil) then
if (song.difficulties[selectedDiff].difficulty == 0) then
diff_long = "NOVICE"
diff_short = "NOV"
elseif (song.difficulties[selectedDiff].difficulty == 1) then
diff_long = "ADVANCED"
diff_short = "ADV"
elseif (song.difficulties[selectedDiff].difficulty == 2) then
diff_long = "EXHAUST"
diff_short = "EXH"
elseif (song.difficulties[selectedDiff].difficulty == 3) then
diff_long = "INFINITE"
diff_short = "INF"
else
diff_long = "UNKNOWN"
diff_short = "???"
end
else
if (song.difficulties[selectedDiff - 1].difficulty == 0) then
diff_long = "NOVICE"
diff_short = "NOV"
elseif (song.difficulties[selectedDiff - 1].difficulty == 1) then
diff_long = "ADVANCED"
diff_short = "ADV"
elseif (song.difficulties[selectedDiff - 1].difficulty == 2) then
diff_long = "EXHAUST"
diff_short = "EXH"
elseif (song.difficulties[selectedDiff - 1].difficulty == 3) then
diff_long = "INFINITE"
diff_short = "INF"
else
diff_long = "UNKNOWN"
diff_short = "???"
end
end
gfx.FontSize(8)
gfx.LoadSkinFont("dfmarugoth.ttf")
gfx.FastText(diff_long, x + h/4, y + h - 7)
local seldiff = nil
if song.difficulties[selectedDiff] ~= nil then
seldiff = selectedDiff
else
seldiff = selectedDiff - 1
end
if song.difficulties[seldiff].topBadge ~= 0 then
if song.difficulties[seldiff].scores[1] ~= nil then
local highScore = song.difficulties[seldiff].scores[1]
for i,v in ipairs(grades) do
if v.max > highScore.score then
gfx.BeginPath()
iw,ih = gfx.ImageSize(v.image)
iar = iw / ih;
gfx.ImageRect(x + w/1.45, y + h/8 + 2, (h/1.5-14), h/1.5-14, v.image, 1, 0)
break
end
end
end
gfx.BeginPath()
gfx.ImageRect(x + w/2, y + h/8, (h/1.5-10), h/1.5-10, badges[song.difficulties[seldiff].topBadge], 1, 0)
end
end
draw_diff_icon = function(diff, x, y, w, h, selected)
local shrinkX = w/4
local shrinkY = h/4
gfx.BeginPath()
gfx.RoundedRectVarying(x+shrinkX,y+shrinkY,w-shrinkX*2,h-shrinkY*2,0,0,0,0)
gfx.FillColor(15,15,15)
gfx.StrokeColor(table.unpack(diffColors[diff.difficulty + 1]))
gfx.StrokeWidth(2)
gfx.Fill()
gfx.Stroke()
gfx.FillColor(255,255,255)
gfx.TextAlign(gfx.TEXT_ALIGN_MIDDLE + gfx.TEXT_ALIGN_CENTER)
gfx.FontSize(28)
gfx.FastText(tostring(diff.level), x+(w/2),y+(h/2))
end
draw_cursor = function(x,y,rotation,width)
gfx.Save()
gfx.BeginPath();
gfx.Translate(x,y)
gfx.Rotate(rotation)
gfx.StrokeColor(255,128,0)
gfx.StrokeWidth(4)
gfx.Rect(-width/2, -width/2, width, width)
gfx.Stroke()
gfx.Restore()
end
draw_diffs = function(diffs, x, y, w, h)
local diffWidth = w/2.5
local diffHeight = w/2.5
local diffCount = #diffs
local diffSpacingOffset = (diffWidth*0.82)*(selectedDiff-1)
for i = math.max(selectedDiff - 3, 1), math.max(selectedDiff - 1,1) do
local diff = diffs[i]
local xpos = (x + ((w/2 - diffWidth/2) + (-0.8*diffWidth))) - ((diffWidth*0.82)*(i-selectedDiff+1))
if i ~= selectedDiff then
draw_diff_icon(diff, xpos, y, diffWidth, diffHeight, false)
end
end
--after selected
for i = math.min(selectedDiff + 3, diffCount), selectedDiff + 1,-1 do
local diff = diffs[i]
local xpos = (x + ((w/2 - diffWidth/2) + (-0.8*diffWidth))) + ((diffWidth*0.82)*(i-1))
if i ~= selectedDiff then
draw_diff_icon(diff, xpos, y, diffWidth, diffHeight, false)
end
end
local diff = diffs[selectedDiff]
local xpos = x + ((w/2 - diffWidth/2) + (-0.8*diffWidth))
draw_diff_icon(diff, (xpos*0.9)+diffSpacingOffset, y, diffWidth, diffHeight, true)
gfx.BeginPath()
gfx.FillColor(0,128,255)
gfx.Fill()
gfx.BeginPath()
gfx.Fill()
gfx.ResetScissor()
draw_cursor((x + (w/5.7))*(selectedDiff^1.085), y +diffHeight/2, timer * math.pi, diffHeight / 1.5)
end
draw_selected = function(song, x, y, w, h)
check_or_create_cache(song)
-- set up padding and margins
local xPadding = math.floor(w/16)
local yPadding = math.floor(h/32)
local xMargin = math.floor(w/16)
local yMargin = math.floor(h/32)
local width = (w-(xMargin*2))
local height = (h-(yMargin*2))
local xpos = x+xMargin
local ypos = y+yMargin
if aspectRatio == "PortraitWidescreen" then
xPadding = math.floor(w/32)
yPadding = math.floor(h/32)
xMargin = math.floor(w/34)
yMargin = math.floor(h/32)
width = ((w/2)-(xMargin))
height = (h-(yMargin*2))
xpos = x+xMargin/2
ypos = y+yMargin
end
--Border
local diff = song.difficulties[selectedDiff]
gfx.BeginPath()
--gfx.RoundedRectVarying(xpos,ypos,width,height,yPadding,yPadding,yPadding,yPadding)
gfx.FillColor(30,30,30,100)
gfx.StrokeColor(0,128,255)
gfx.StrokeWidth(1)
gfx.Fill()
gfx.Stroke()
-- jacket should take up 1/3 of height, always be square, and be centered
local imageSize = math.floor(height/3)
local imageXPos = ((width/2) - (imageSize/2)) + x+xMargin
if aspectRatio == "PortraitWidescreen" then
--Unless its portrait widesreen..
imageSize = math.floor((height/8)*1.58)
imageXPos = (x+w)/16+(xMargin*0.8)
end
if not songCache[song.id][selectedDiff] or songCache[song.id][selectedDiff] == jacketFallback then
songCache[song.id][selectedDiff] = gfx.LoadImageJob(diff.jacketPath, jacketFallback, 200,200)
end
if songCache[song.id][selectedDiff] then
gfx.BeginPath()
gfx.ImageRect(imageXPos, y+yMargin*4.45+yPadding, imageSize, imageSize, songCache[song.id][selectedDiff], 1, 0)
end
-- difficulty should take up 1/6 of height, full width, and be centered
gfx.LoadSkinFont("commext.ttf")
if aspectRatio == "PortraitWidescreen" then
--difficulty wheel should be right below the jacketImage, and the same width as
--the jacketImage
draw_diffs(song.difficulties,xpos+xPadding/1.5,(ypos*10.3+yPadding+imageSize),imageSize,math.floor((height/3)*1)-yPadding)
else
-- difficulty should take up 1/6 of height, full width, and be centered
draw_diffs(song.difficulties,(w/2)-(imageSize/2),(ypos+yPadding+imageSize),imageSize,math.floor(height/6))
end
-- effector / bpm should take up 1/3 of height, full width
gfx.LoadSkinFont("dfmarugoth.ttf")
if aspectRatio == "PortraitWidescreen" then
gfx.FontSize(40)
gfx.TextAlign(gfx.TEXT_ALIGN_TOP + gfx.TEXT_ALIGN_LEFT)
gfx.DrawLabel(songCache[song.id]["title"], xpos+xPadding/2, y+yMargin*15+yPadding, width)
gfx.FontSize(40)
gfx.DrawLabel(songCache[song.id]["artist"], xpos+xPadding/2, y+yMargin*15.8+yPadding, width)
gfx.FontSize(10)
gfx.DrawLabel(songCache[song.id]["bpm"], xpos+xPadding*2, y+yMargin*14.42+yPadding, width-imageSize)
gfx.FastText(string.format("%s", diff.effector), xpos+xPadding*7.5, y+yMargin*18.87+yPadding)
else
gfx.FontSize(40)
gfx.TextAlign(gfx.TEXT_ALIGN_TOP + gfx.TEXT_ALIGN_LEFT)
gfx.DrawLabel(songCache[song.id]["title"], xpos+10, (height/10)*6, width-20)
gfx.FontSize(30)
gfx.DrawLabel(songCache[song.id]["artist"], xpos+10, (height/10)*6 + 45, width-20)
gfx.FillColor(255,255,255)
gfx.FontSize(20)
gfx.DrawLabel(songCache[song.id]["bpm"], xpos+10, (height/10)*6 + 85)
gfx.FastText(string.format("%s", diff.effector),xpos+10, (height/10)*6 + 115)
end
if aspectRatio == "PortraitWidescreen" then
draw_scores(diff, xpos+xPadding+imageSize+3, (height/3)*2, width-imageSize-20, (height/3)-yPadding)
else
draw_scores(diff, xpos, (height/6)*5, width, (height/6))
end
gfx.ForceRender()
end
draw_songwheel = function(x,y,w,h)
local offsetX = fifthX/2
local width = math.floor((w/5)*4)
if aspectRatio == "landscapeWidescreen" then
wheelSize = 12
offsetX = 80
elseif aspectRatio == "landscapeStandard" then
wheelSize = 10
offsetX = 40
elseif aspectRatio == "PortraitWidescreen" then
wheelSize = 20
offsetX = 20
width = w/2
end
local height = math.floor((h/wheelSize)*1.75)
for i = math.max(selectedIndex - wheelSize/2, 1), math.max(selectedIndex - 1,0) do
local song = songwheel.songs[i]
local xpos = x + width
local offsetY = (selectedIndex - i + ioffset/2) * ( height * 1.05)
local ypos = y+((h/2 - height/2) - offsetY)
draw_song(song, xpos, ypos, width, height)
end
--after selected
for i = math.min(selectedIndex + wheelSize/2, #songwheel.songs), selectedIndex + 1,-1 do
local song = songwheel.songs[i]
local xpos = x + width
local offsetY = (selectedIndex - i + ioffset/2) * ( height * 1.05)
local ypos = y+((h/2 - height/2) - (selectedIndex - i) - offsetY)
local alpha = 255 - (selectedIndex - i + ioffset) * 31
draw_song(song, xpos, ypos, width, height)
end
-- draw selected
local xpos = x + width
local offsetY = (ioffset/2) * ( height - (wheelSize/2*((1)*aspectFloat)))
local ypos = y+((h/2 - height/2) - (ioffset) - offsetY)
draw_song(songwheel.songs[selectedIndex], xpos, ypos, width, height, true)
-- cursor
gfx.BeginPath()
local ypos = y+((h/2 - height/2))
gfx.Rect(xpos, ypos, width, height)
gfx.FillColor(0,0,0,0)
gfx.StrokeColor(255,128,0)
gfx.StrokeWidth(3)
gfx.Fill()
gfx.Stroke()
return songwheel.songs[selectedIndex]
end
draw_legend_pane = function(x,y,w,h,obj)
local xpos = x+5
local ypos = y
local imageSize = h
gfx.BeginPath()
gfx.TextAlign(gfx.TEXT_ALIGN_MIDDLE + gfx.TEXT_ALIGN_LEFT)
gfx.ImageRect(x, y, imageSize, imageSize, obj.image, 1, 0)
xpos = xpos + imageSize + 5
gfx.FontSize(16);
if h < (w-(10+imageSize))/2 then
gfx.DrawLabel(obj.labelSingleLine, xpos, y+(h/2), w-(10+imageSize))
else
gfx.DrawLabel(obj.labelMultiLine, xpos, y+(h/2), w-(10+imageSize))
end
gfx.ForceRender()
end
draw_legend = function(x,y,w,h)
gfx.TextAlign(gfx.TEXT_ALIGN_MIDDLE + gfx.TEXT_ALIGN_LEFT);
gfx.BeginPath()
gfx.FillColor(0,0,0,170)
gfx.Rect(x,y,w,h)
gfx.Fill()
local xpos = 10;
local legendWidth = math.floor((w-20)/#legendTable)
for i,v in ipairs(legendTable) do
local xOffset = draw_legend_pane(xpos+(legendWidth*(i-1)), y+5,legendWidth,h-10,legendTable[i])
end
end
draw_search = function(x,y,w,h)
soffset = soffset + (searchIndex) - (songwheel.searchInputActive and 0 or 1)
if searchIndex ~= (songwheel.searchInputActive and 0 or 1) then
game.PlaySample("woosh")
end
searchIndex = songwheel.searchInputActive and 0 or 1
gfx.BeginPath()
local bgfade = 1 - (searchIndex + soffset)
--if not songwheel.searchInputActive then bgfade = soffset end
gfx.FillColor(0,0,0,math.floor(200 * bgfade))
gfx.Rect(0,0,resx,resy)
gfx.Fill()
gfx.ForceRender()
local xpos = x + (searchIndex + soffset)*w
gfx.UpdateLabel(searchText ,string.format("Search: %s",songwheel.searchText), 30, 0)
gfx.BeginPath()
gfx.RoundedRect(xpos,y,w,h,h/2)
gfx.FillColor(30,30,30)
gfx.StrokeColor(0,128,255)
gfx.StrokeWidth(1)
gfx.Fill()
gfx.Stroke()
gfx.BeginPath();
gfx.LoadSkinFont("NotoSans-Regular.ttf");
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_MIDDLE);
gfx.DrawLabel(searchText, xpos+10,y+(h/2), w-20)
end
render = function(deltaTime)
timer = (timer + deltaTime)
timer = timer % 2
resx,resy = game.GetResolution();
adjustScreen(resx,resy);
gfx.BeginPath();
gfx.LoadSkinFont("dfmarugoth.ttf");
gfx.FontSize(40);
gfx.FillColor(255,255,255);
if songwheel.songs[1] ~= nil then
--draw songwheel and get selected song
if aspectRatio == "PortraitWidescreen" then
local song = draw_songwheel(0,0,fullX,fullY)
--render selected song information
draw_selected(song, 0,0,fullX,resy)
else
local song = draw_songwheel(fifthX*2,0,fifthX*3,fullY)
--render selected song information
draw_selected(song, 0,0,fifthX*2,(fifthY/2)*9)
end
end
--Draw Legend Information
-- if showGuide then
-- if aspectRatio == "PortraitWidescreen" then
-- draw_legend(0,(fifthY/3)*14, fullX, (fifthY/3)*1)
-- else
-- draw_legend(0,(fifthY/2)*9, fullX, (fifthY/2))
-- end
-- end
gfx.BeginPath();
gfx.TextAlign(TEXT_ALIGN_CENTER + TEXT_ALIGN_MIDDLE);
gfx.ImageRect(0, 0, resx, resy, foreground, 1, 0);
--draw text search
if aspectRatio == "PortraitWidescreen" then
draw_search(fifthX*2,5,fifthX*3,fifthY/5)
else
draw_search(fifthX*2,5,fifthX*3,fifthY/3)
end
ioffset = ioffset * 0.9
doffset = doffset * 0.9
soffset = soffset * 0.8
if songwheel.searchStatus then
gfx.BeginPath()
gfx.FillColor(255,255,255)
gfx.FontSize(20);
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_TOP)
gfx.Text(songwheel.searchStatus, 3, 3)
end
if totalForce then
gfx.BeginPath()
gfx.FillColor(255,255,255)
gfx.FontSize(20);
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_BOTTOM)
local forceText = string.format("Force: %.2f", totalForce)
gfx.Text(forceText, 0, fullY)
end
gfx.LoadSkinFont("NotoSans-Regular.ttf");
gfx.ResetTransform()
gfx.ForceRender()
end
set_index = function(newIndex)
if newIndex ~= selectedIndex then
game.PlaySample("menu_click")
end
ioffset = ioffset + selectedIndex - newIndex
selectedIndex = newIndex
end;
set_diff = function(newDiff)
if newDiff ~= selectedDiff then
game.PlaySample("click-02")
end
doffset = doffset + selectedDiff - newDiff
selectedDiff = newDiff
end;
-- force calculation
--------------------
totalForce = nil
local badgeRates = {
0.5, -- Played
1.0, -- Cleared
1.02, -- Hard clear
1.04, -- UC
1.1 -- PUC
}
local gradeRates = {
{["min"] = 9900000, ["rate"] = 1.05}, -- S
{["min"] = 9800000, ["rate"] = 1.02}, -- AAA+
{["min"] = 9700000, ["rate"] = 1}, -- AAA
{["min"] = 9500000, ["rate"] = 0.97}, -- AA+
{["min"] = 9300000, ["rate"] = 0.94}, -- AA
{["min"] = 9000000, ["rate"] = 0.91}, -- A+
{["min"] = 8700000, ["rate"] = 0.88}, -- A
{["min"] = 7500000, ["rate"] = 0.85}, -- B
{["min"] = 6500000, ["rate"] = 0.82}, -- C
{["min"] = 0, ["rate"] = 0.8} -- D
}
calculate_force = function(diff)
if #diff.scores < 1 then
return 0
end
local score = diff.scores[1]
local badgeRate = badgeRates[diff.topBadge]
local gradeRate
for i, v in ipairs(gradeRates) do
if score.score >= v.min then
gradeRate = v.rate
break
end
end
return math.floor((diff.level * 2) * (score.score / 10000000) * gradeRate * badgeRate) / 100
end
songs_changed = function(withAll)
if not withAll then return end
recordCache = {}
local diffs = {}
for i = 1, #songwheel.allSongs do
local song = songwheel.allSongs[i]
for j = 1, #song.difficulties do
local diff = song.difficulties[j]
diff.force = calculate_force(diff)
table.insert(diffs, diff)
end
end
table.sort(diffs, function (l, r)
return l.force > r.force
end)
totalForce = 0
for i = 1, 50 do
if diffs[i] then
totalForce = totalForce + diffs[i].force
end
end
end

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,894 @@
--Horizontal alignment
TEXT_ALIGN_LEFT = 1
TEXT_ALIGN_CENTER = 2
TEXT_ALIGN_RIGHT = 4
--Vertical alignment
TEXT_ALIGN_TOP = 8
TEXT_ALIGN_MIDDLE = 16
TEXT_ALIGN_BOTTOM = 32
TEXT_ALIGN_BASELINE = 64
local jacket = nil;
local selectedIndex = 1
local selectedDiff = 1
local songCache = {}
local ioffset = 0
local doffset = 0
local soffset = 0
local diffColors = {{0,0,255}, {0,255,0}, {255,0,0}, {255, 0, 255}}
local timer = 0
local effector = 0
local searchText = gfx.CreateLabel("",5,0)
local searchIndex = 1
local jacketFallback = gfx.CreateSkinImage("song_select/loading.png", 0)
local showGuide = game.GetSkinSetting("show_guide")
local legendTable = {
{["labelSingleLine"] = gfx.CreateLabel("DIFFICULTY SELECT",16, 0), ["labelMultiLine"] = gfx.CreateLabel("DIFFICULTY\nSELECT",16, 0), ["image"] = gfx.CreateSkinImage("legend/knob-left.png", 0)},
{["labelSingleLine"] = gfx.CreateLabel("MUSIC SELECT",16, 0), ["labelMultiLine"] = gfx.CreateLabel("MUSIC\nSELECT",16, 0), ["image"] = gfx.CreateSkinImage("legend/knob-right.png", 0)},
{["labelSingleLine"] = gfx.CreateLabel("FILTER MUSIC",16, 0), ["labelMultiLine"] = gfx.CreateLabel("FILTER\nMUSIC",16, 0), ["image"] = gfx.CreateSkinImage("legend/FX-L.png", 0)},
{["labelSingleLine"] = gfx.CreateLabel("SORT MUSIC",16, 0), ["labelMultiLine"] = gfx.CreateLabel("SORT\nMUSIC",16, 0), ["image"] = gfx.CreateSkinImage("legend/FX-R.png", 0)},
{["labelSingleLine"] = gfx.CreateLabel("MUSIC MODS",16, 0), ["labelMultiLine"] = gfx.CreateLabel("MUSIC\nMODS",16, 0), ["image"] = gfx.CreateSkinImage("legend/FX-LR.png", 0)},
{["labelSingleLine"] = gfx.CreateLabel("PLAY",16, 0), ["labelMultiLine"] = gfx.CreateLabel("PLAY",16, 0), ["image"] = gfx.CreateSkinImage("legend/start.png", 0)}
}
local grades = {
{["max"] = 6999999, ["image"] = gfx.CreateSkinImage("common/grades/D.png", 0)},
{["max"] = 7999999, ["image"] = gfx.CreateSkinImage("common/grades/C.png", 0)},
{["max"] = 8699999, ["image"] = gfx.CreateSkinImage("common/grades/B.png", 0)},
{["max"] = 8999999, ["image"] = gfx.CreateSkinImage("common/grades/A.png", 0)},
{["max"] = 9299999, ["image"] = gfx.CreateSkinImage("common/grades/A+.png", 0)},
{["max"] = 9499999, ["image"] = gfx.CreateSkinImage("common/grades/AA.png", 0)},
{["max"] = 9699999, ["image"] = gfx.CreateSkinImage("common/grades/AA+.png", 0)},
{["max"] = 9799999, ["image"] = gfx.CreateSkinImage("common/grades/AAA.png", 0)},
{["max"] = 9899999, ["image"] = gfx.CreateSkinImage("common/grades/AAA+.png", 0)},
{["max"] = 99999999, ["image"] = gfx.CreateSkinImage("common/grades/S.png", 0)}
}
local badges = {
gfx.CreateSkinImage("badges/played.png", 0),
gfx.CreateSkinImage("badges/clear.png", 0),
gfx.CreateSkinImage("badges/hard-clear.png", 0),
gfx.CreateSkinImage("badges/full-combo.png", 0),
gfx.CreateSkinImage("badges/perfect.png", 0)
}
local foreground = gfx.CreateSkinImage("song_select/fg.png", 0);
local recordCache = {}
gfx.LoadSkinFont("dfmarugoth.ttf");
game.LoadSkinSample("menu_click")
game.LoadSkinSample("click-02")
game.LoadSkinSample("woosh")
local wheelSize = 12
get_page_size = function()
return math.floor(wheelSize/2)
end
-- Responsive UI variables
-- Aspect Ratios
local aspectFloat = 1.850
local aspectRatio = "widescreen"
local landscapeWidescreenRatio = 1.850
local landscapeStandardRatio = 1.500
local portraitWidescreenRatio = 0.5
-- Responsive sizes
local fifthX = 0
local fourthX= 0
local thirdX = 0
local halfX = 0
local fullX = 0
local fifthY = 0
local fourthY= 0
local thirdY = 0
local halfY = 0
local fullY = 0
adjustScreen = function(x,y)
local a = x/y;
if x >= y and a <= landscapeStandardRatio then
aspectRatio = "landscapeStandard"
aspectFloat = 1.1
elseif x >= y and landscapeStandardRatio <= a and a <= landscapeWidescreenRatio then
aspectRatio = "landscapeWidescreen"
aspectFloat = 1.2
elseif x <= y and portraitWidescreenRatio <= a and a < landscapeStandardRatio then
aspectRatio = "PortraitWidescreen"
aspectFloat = 0.5
else
aspectRatio = "landscapeWidescreen"
aspectFloat = 1.0
end
fifthX = x/5
fourthX= x/4
thirdX = x/3
halfX = x/2
fullX = x
fifthY = y/5
fourthY= y/4
thirdY = y/3
halfY = y/2
fullY = y
end
check_or_create_cache = function(song, loadJacket)
if not songCache[song.id] then songCache[song.id] = {} end
if not songCache[song.id]["title"] then
songCache[song.id]["title"] = gfx.CreateLabel(song.title, 14, 0)
end
if not songCache[song.id]["artist"] then
songCache[song.id]["artist"] = gfx.CreateLabel(song.artist, 14, 0)
end
if not songCache[song.id]["bpm"] then
songCache[song.id]["bpm"] = gfx.CreateLabel(string.format("%s",song.bpm), 12, 0)
end
if not songCache[song.id]["effector"] then
songCache[song.id]["effector"] = gfx.CreateLabel(string.format("BPM: %s",song.bpm), 20, 0)
end
if not songCache[song.id]["jacket"] then
songCache[song.id]["jacket"] = { }
end
for i = 1, #song.difficulties do
songCache[song.id]["jacket"][i] = gfx.LoadImageJob(song.difficulties[i].jacketPath, jacketFallback, 200, 200)
end
end
function record_handler_factory(hash)
return (function(res)
if res.statusCode == 42 then
recordCache[hash] = {good=false, reason="Untracked"}
elseif res.statusCode == 20 and res.body ~= nil then
recordCache[hash] = {good=true, record=res.body.record}
elseif res.statusCode == 44 then
recordCache[hash] = {good=true, record=nil}
else
recordCache[hash] = {good=false, reason="Failed"}
end
end)
end
function get_record(hash)
if recordCache[hash] then return recordCache[hash] end
recordCache[hash] = {good=false, reason="Loading..."}
IR.Record(hash, record_handler_factory(hash))
return recordCache[hash]
end
function log_table(table)
str = "{"
for k, v in pairs(table) do
str = str .. k .. ": "
t = type(v)
if t == "table" then
str = str .. log_table(v)
elseif t == "string" then
str = str .. "\"" .. v .. "\""
elseif t == "boolean" then
if v then
str = str .. "true"
else
str = str .. "false"
end
else
str = str .. v
end
str = str .. ", "
end
return str .. "}"
end
draw_scores_ir = function(difficulty, x, y, w, h)
-- draw the top score for this difficulty
local xOffset = 5
local height = h/3 - 10
local ySpacing = h/3
local yOffset = h/3
gfx.FontSize(30);
gfx.TextAlign(gfx.TEXT_ALIGN_BOTTOM + gfx.TEXT_ALIGN_CENTER);
gfx.FastText("HIGH SCORE", x +(w/4), y+(h/2))
gfx.FastText("IR RECORD", x + (3/4 * w), y + (h/2))
gfx.BeginPath()
gfx.Rect(x+xOffset,y+h/2,w/2-(xOffset*2),h/2)
gfx.FillColor(30,30,30,10)
gfx.StrokeColor(0,128,255)
gfx.StrokeWidth(1)
gfx.Fill()
gfx.Stroke()
gfx.BeginPath()
gfx.Rect(x + xOffset + w/2,y+h/2,w/2-(xOffset*2),h/2)
gfx.FillColor(30,30,30,10)
gfx.StrokeColor(0,128,255)
gfx.StrokeWidth(1)
gfx.Fill()
gfx.Stroke()
if difficulty.scores[1] ~= nil then
local highScore = difficulty.scores[1]
scoreLabel = gfx.CreateLabel(string.format("%08d",highScore.score), 40, 0)
for i,v in ipairs(grades) do
if v.max > highScore.score then
gfx.BeginPath()
iw,ih = gfx.ImageSize(v.image)
iarr = ih / iw
oldheight = h/2 - 10
newheight = iarr * (h/2-10)
centreoffset = (oldheight - newheight)/2 + 3 -- +3 is stupid but ehhh
gfx.ImageRect(x+xOffset, y+h/2 + centreoffset, oldheight, newheight, v.image, 1, 0) --this is nasty but it works for me
break
end
end
if difficulty.topBadge ~= 0 then
gfx.BeginPath()
gfx.ImageRect(x+xOffset+w/2-h/2, y+h/2 +5, (h/2-10), h/2-10, badges[difficulty.topBadge], 1, 0)
end
gfx.FillColor(255,255,255)
gfx.FontSize(40);
gfx.TextAlign(gfx.TEXT_ALIGN_MIDDLE + gfx.TEXT_ALIGN_CENTER);
gfx.DrawLabel(scoreLabel, x+(w/4),y+(h/4)*3,w/2)
end
irRecord = get_record(difficulty.hash)
if not irRecord.good then
recordLabel = gfx.CreateLabel(irRecord.reason, 40, 0)
gfx.FillColor(255, 255, 255)
gfx.FontSize(40)
gfx.TextAlign(gfx.TEXT_ALIGN_MIDDLE + gfx.TEXT_ALIGN_CENTER);
gfx.DrawLabel(recordLabel, x+(w * 3/4),y+(h/4)*3,w/2)
elseif irRecord.record == nil then --record not set, but can be tracked
recordLabel = gfx.CreateLabel(string.format("%08d", 0), 40, 0)
gfx.FillColor(170, 170, 170)
gfx.FontSize(40)
gfx.TextAlign(gfx.TEXT_ALIGN_MIDDLE + gfx.TEXT_ALIGN_CENTER);
gfx.DrawLabel(recordLabel, x+(w * 3/4),y+(h/4)*3,w/2)
else
recordScoreLabel = gfx.CreateLabel(string.format("%08d", irRecord.record.score), 26, 0)
recordPlayerLabel = gfx.CreateLabel(irRecord.record.username, 26, 0)
if irRecord.record.lamp ~= 0 then
gfx.BeginPath()
gfx.ImageRect(x+xOffset+w-h/2, y+h/2 +5, (h/2-10), h/2-10, badges[irRecord.record.lamp], 1, 0)
end
for i,v in ipairs(grades) do
if v.max > irRecord.record.score then
gfx.BeginPath()
iw,ih = gfx.ImageSize(v.image)
iarr = ih / iw
oldheight = h/2 - 10
newheight = iarr * (h/2-10)
centreoffset = (oldheight - newheight)/2 + 3 -- +3 is stupid but ehhh
gfx.ImageRect(x+xOffset+w/2, y+h/2 + centreoffset, oldheight, newheight, v.image, 1, 0) --this is nasty but it works for me
break
end
end
gfx.FillColor(255, 255, 255)
gfx.FontSize(40)
gfx.TextAlign(gfx.TEXT_ALIGN_MIDDLE + gfx.TEXT_ALIGN_CENTER);
gfx.DrawLabel(recordPlayerLabel, x+(w * 3/4),y+(h/4)*2.55,w/2)
gfx.DrawLabel(recordScoreLabel, x+(w * 3/4),y+(h/4)*3.45,w/2)
end
end
draw_scores = function(difficulty, x, y, w, h)
if IRData.Active then return draw_scores_ir(difficulty, x, y, w, h) end
-- draw the top score for this difficulty
local xOffset = 5
local height = h/3 - 10
local ySpacing = h/3
local yOffset = h/3
gfx.FontSize(30);
gfx.TextAlign(gfx.TEXT_ALIGN_BOTTOM + gfx.TEXT_ALIGN_CENTER);
gfx.BeginPath()
gfx.FillColor(30,30,30,10)
gfx.StrokeColor(0,128,255)
gfx.StrokeWidth(1)
gfx.Fill()
gfx.Stroke()
if difficulty.scores[1] ~= nil then
local highScore = difficulty.scores[1]
scoreLabel = gfx.CreateLabel(string.format("%08d",highScore.score), 40, 0)
for i,v in ipairs(grades) do
if v.max > highScore.score then
gfx.BeginPath()
iw,ih = gfx.ImageSize(v.image)
iar = iw / ih;
--gfx.ImageRect(x+xOffset,y+h/2 +5, iar * (h/2-10),h/2-10, v.image, 1, 0)
break
end
end
if difficulty.topBadge ~= 0 then
gfx.BeginPath()
--gfx.ImageRect(x+xOffset+w-h/2, y+h/2 +5, (h/2-10), h/2-10, badges[difficulty.topBadge], 1, 0)
end
gfx.FillColor(255,255,255)
gfx.FontSize(40);
gfx.TextAlign(gfx.TEXT_ALIGN_BOTTOM + gfx.TEXT_ALIGN_LEFT);
gfx.DrawLabel(scoreLabel, x/11,y/1.48,w*2)
end
end
draw_song = function(song, x, y, w, h, selected)
local diffIndex = math.min(selectedDiff, #song.difficulties)
local difficulty = song.difficulties[diffIndex]
local clearLampR = 255
local clearLampG = 255
local clearLampB = 255
local clearLampA = 100
if difficulty ~= nil then
if difficulty.scores[1] ~= nil then
if difficulty.topBadge == 1 then -- fail/played
clearLampR = 255
clearLampG = 25
clearLampB = 25
clearLampA = 200
end
if difficulty.topBadge == 2 then -- clear
clearLampR = 25
clearLampG = 255
clearLampB = 25
clearLampA = 200
end
if difficulty.topBadge == 3 then -- hard clear
clearLampR = 255
clearLampG = 25
clearLampB = 255
clearLampA = 200
end
if difficulty.topBadge == 4 then -- full combo
clearLampR = 255
clearLampG = 100
clearLampB = 25
clearLampA = 200
end
if difficulty.topBadge == 5 then -- perfect
clearLampR = 255
clearLampG = 255
clearLampB = 25
clearLampA = 200
end
end
end
check_or_create_cache(song)
gfx.BeginPath()
gfx.Rect(x+1,y+1, w-2, h-2)
gfx.FillColor(220,220,220)
gfx.StrokeColor(0,8,0)
gfx.StrokeWidth(2)
gfx.Fill()
gfx.Stroke()
gfx.FillColor(255,255,255)
if songCache[song.id]["jacket"][diffIndex] then
gfx.BeginPath()
gfx.ImageRect(x+2, y+2, h-4, h-4, songCache[song.id]["jacket"][diffIndex], 1, 0)
end
-- Song title
gfx.BeginPath()
gfx.Rect(x+1, y + h - h/4 - 1, w-2, h/4)
gfx.FillColor(0,0,0,200)
gfx.Fill()
gfx.BeginPath()
gfx.FillColor(255,255,255)
gfx.TextAlign(gfx.TEXT_ALIGN_BOTTOM + gfx.TEXT_ALIGN_LEFT)
gfx.DrawLabel(songCache[song.id]["title"], (x)+h/2 + 4, y + h - 7, -1)
--gfx.DrawLabel(songCache[song.id]["artist"], x+10, y + 50, w-10)
-- Song difficulty
gfx.BeginPath()
gfx.Rect(x - 1, y + h-h/2 - 4, h/2, h/2)
gfx.FillColor(0,0,0,200)
gfx.Fill()
gfx.FillColor(255,255,255)
gfx.LoadSkinFont("commext.ttf")
gfx.FontSize(28)
gfx.TextAlign(gfx.TEXT_ALIGN_CENTER + gfx.TEXT_ALIGN_BOTTOM)
if (song.difficulties[selectedDiff] ~= nil) then
gfx.FastText(song.difficulties[selectedDiff].level, x + h/4, y + h - 10)
else
gfx.FastText(song.difficulties[selectedDiff - 1].level, x + h/4, y + h - 10)
end
-- CLEAN THIS SHIT UP
local diff_long = ""
local diff_short = ""
if (song.difficulties[selectedDiff] ~= nil) then
if (song.difficulties[selectedDiff].difficulty == 0) then
diff_long = "NOVICE"
diff_short = "NOV"
elseif (song.difficulties[selectedDiff].difficulty == 1) then
diff_long = "ADVANCED"
diff_short = "ADV"
elseif (song.difficulties[selectedDiff].difficulty == 2) then
diff_long = "EXHAUST"
diff_short = "EXH"
elseif (song.difficulties[selectedDiff].difficulty == 3) then
diff_long = "INFINITE"
diff_short = "INF"
else
diff_long = "UNKNOWN"
diff_short = "???"
end
else
if (song.difficulties[selectedDiff - 1].difficulty == 0) then
diff_long = "NOVICE"
diff_short = "NOV"
elseif (song.difficulties[selectedDiff - 1].difficulty == 1) then
diff_long = "ADVANCED"
diff_short = "ADV"
elseif (song.difficulties[selectedDiff - 1].difficulty == 2) then
diff_long = "EXHAUST"
diff_short = "EXH"
elseif (song.difficulties[selectedDiff - 1].difficulty == 3) then
diff_long = "INFINITE"
diff_short = "INF"
else
diff_long = "UNKNOWN"
diff_short = "???"
end
end
gfx.FontSize(8)
gfx.LoadSkinFont("dfmarugoth.ttf")
gfx.FastText(diff_long, x + h/4, y + h - 7)
local seldiff = nil
if song.difficulties[selectedDiff] ~= nil then
seldiff = selectedDiff
else
seldiff = selectedDiff - 1
end
if song.difficulties[seldiff].topBadge ~= 0 then
if song.difficulties[seldiff].scores[1] ~= nil then
local highScore = song.difficulties[seldiff].scores[1]
for i,v in ipairs(grades) do
if v.max > highScore.score then
gfx.BeginPath()
iw,ih = gfx.ImageSize(v.image)
iar = iw / ih;
gfx.ImageRect(x + w/1.45, y + h/8 + 2, (h/1.5-14), h/1.5-14, v.image, 1, 0)
break
end
end
end
gfx.BeginPath()
gfx.ImageRect(x + w/2, y + h/8, (h/1.5-10), h/1.5-10, badges[song.difficulties[seldiff].topBadge], 1, 0)
end
end
draw_diff_icon = function(diff, x, y, w, h, selected)
local shrinkX = w/4
local shrinkY = h/4
gfx.BeginPath()
gfx.RoundedRectVarying(x+shrinkX,y+shrinkY,w-shrinkX*2,h-shrinkY*2,0,0,0,0)
gfx.FillColor(15,15,15)
gfx.StrokeColor(table.unpack(diffColors[diff.difficulty + 1]))
gfx.StrokeWidth(2)
gfx.Fill()
gfx.Stroke()
gfx.FillColor(255,255,255)
gfx.TextAlign(gfx.TEXT_ALIGN_MIDDLE + gfx.TEXT_ALIGN_CENTER)
gfx.FontSize(28)
gfx.FastText(tostring(diff.level), x+(w/2),y+(h/2))
end
draw_cursor = function(x,y,rotation,width)
gfx.Save()
gfx.BeginPath();
gfx.Translate(x,y)
gfx.Rotate(rotation)
gfx.StrokeColor(255,128,0)
gfx.StrokeWidth(4)
gfx.Rect(-width/2, -width/2, width, width)
gfx.Stroke()
gfx.Restore()
end
draw_diffs = function(diffs, x, y, w, h)
local diffWidth = w/2.5
local diffHeight = w/2.5
local diffCount = #diffs
local diffSpacingOffset = (diffWidth*0.82)*(selectedDiff-1)
for i = math.max(selectedDiff - 3, 1), math.max(selectedDiff - 1,1) do
local diff = diffs[i]
local xpos = (x + ((w/2 - diffWidth/2) + (-0.8*diffWidth))) - ((diffWidth*0.82)*(i-selectedDiff+1))
if i ~= selectedDiff then
draw_diff_icon(diff, xpos, y, diffWidth, diffHeight, false)
end
end
--after selected
for i = math.min(selectedDiff + 3, diffCount), selectedDiff + 1,-1 do
local diff = diffs[i]
local xpos = (x + ((w/2 - diffWidth/2) + (-0.8*diffWidth))) + ((diffWidth*0.82)*(i-1))
if i ~= selectedDiff then
draw_diff_icon(diff, xpos, y, diffWidth, diffHeight, false)
end
end
local diff = diffs[selectedDiff]
local xpos = x + ((w/2 - diffWidth/2) + (-0.8*diffWidth))
draw_diff_icon(diff, (xpos*0.9)+diffSpacingOffset, y, diffWidth, diffHeight, true)
gfx.BeginPath()
gfx.FillColor(0,128,255)
gfx.Fill()
gfx.BeginPath()
gfx.Fill()
gfx.ResetScissor()
draw_cursor((x + (w/5.7))*(selectedDiff^1.085), y +diffHeight/2, timer * math.pi, diffHeight / 1.5)
end
draw_selected = function(song, x, y, w, h)
check_or_create_cache(song)
-- set up padding and margins
local xPadding = math.floor(w/16)
local yPadding = math.floor(h/32)
local xMargin = math.floor(w/16)
local yMargin = math.floor(h/32)
local width = (w-(xMargin*2))
local height = (h-(yMargin*2))
local xpos = x+xMargin
local ypos = y+yMargin
if aspectRatio == "PortraitWidescreen" then
xPadding = math.floor(w/32)
yPadding = math.floor(h/32)
xMargin = math.floor(w/34)
yMargin = math.floor(h/32)
width = ((w/2)-(xMargin))
height = (h-(yMargin*2))
xpos = x+xMargin/2
ypos = y+yMargin
end
--Border
local diff = song.difficulties[selectedDiff]
gfx.BeginPath()
--gfx.RoundedRectVarying(xpos,ypos,width,height,yPadding,yPadding,yPadding,yPadding)
gfx.FillColor(30,30,30,100)
gfx.StrokeColor(0,128,255)
gfx.StrokeWidth(1)
gfx.Fill()
gfx.Stroke()
-- jacket should take up 1/3 of height, always be square, and be centered
local imageSize = math.floor(height/3)
local imageXPos = ((width/2) - (imageSize/2)) + x+xMargin
if aspectRatio == "PortraitWidescreen" then
--Unless its portrait widesreen..
imageSize = math.floor((height/8)*1.58)
imageXPos = (x+w)/16+(xMargin*0.8)
end
if not songCache[song.id][selectedDiff] or songCache[song.id][selectedDiff] == jacketFallback then
songCache[song.id][selectedDiff] = gfx.LoadImageJob(diff.jacketPath, jacketFallback, 200,200)
end
if songCache[song.id][selectedDiff] then
gfx.BeginPath()
gfx.ImageRect(imageXPos, y+yMargin*4.45+yPadding, imageSize, imageSize, songCache[song.id][selectedDiff], 1, 0)
end
-- difficulty should take up 1/6 of height, full width, and be centered
gfx.LoadSkinFont("commext.ttf")
if aspectRatio == "PortraitWidescreen" then
--difficulty wheel should be right below the jacketImage, and the same width as
--the jacketImage
draw_diffs(song.difficulties,xpos+xPadding/1.5,(ypos*10.3+yPadding+imageSize),imageSize,math.floor((height/3)*1)-yPadding)
else
-- difficulty should take up 1/6 of height, full width, and be centered
draw_diffs(song.difficulties,(w/2)-(imageSize/2),(ypos+yPadding+imageSize),imageSize,math.floor(height/6))
end
-- effector / bpm should take up 1/3 of height, full width
gfx.LoadSkinFont("dfmarugoth.ttf")
if aspectRatio == "PortraitWidescreen" then
gfx.FontSize(40)
gfx.TextAlign(gfx.TEXT_ALIGN_TOP + gfx.TEXT_ALIGN_LEFT)
gfx.DrawLabel(songCache[song.id]["title"], xpos+xPadding/2, y+yMargin*15+yPadding, width)
gfx.FontSize(40)
gfx.DrawLabel(songCache[song.id]["artist"], xpos+xPadding/2, y+yMargin*15.8+yPadding, width)
gfx.FontSize(10)
gfx.DrawLabel(songCache[song.id]["bpm"], xpos+xPadding*2, y+yMargin*14.42+yPadding, width-imageSize)
gfx.FastText(string.format("%s", diff.effector), xpos+xPadding*7.5, y+yMargin*18.87+yPadding)
else
gfx.FontSize(40)
gfx.TextAlign(gfx.TEXT_ALIGN_TOP + gfx.TEXT_ALIGN_LEFT)
gfx.DrawLabel(songCache[song.id]["title"], xpos+10, (height/10)*6, width-20)
gfx.FontSize(30)
gfx.DrawLabel(songCache[song.id]["artist"], xpos+10, (height/10)*6 + 45, width-20)
gfx.FillColor(255,255,255)
gfx.FontSize(20)
gfx.DrawLabel(songCache[song.id]["bpm"], xpos+10, (height/10)*6 + 85)
gfx.FastText(string.format("%s", diff.effector),xpos+10, (height/10)*6 + 115)
end
if aspectRatio == "PortraitWidescreen" then
draw_scores(diff, xpos+xPadding+imageSize+3, (height/3)*2, width-imageSize-20, (height/3)-yPadding)
else
draw_scores(diff, xpos, (height/6)*5, width, (height/6))
end
gfx.ForceRender()
end
draw_songwheel = function(x,y,w,h)
local offsetX = fifthX/2
local width = math.floor((w/5)*4)
if aspectRatio == "landscapeWidescreen" then
wheelSize = 12
offsetX = 80
elseif aspectRatio == "landscapeStandard" then
wheelSize = 10
offsetX = 40
elseif aspectRatio == "PortraitWidescreen" then
wheelSize = 20
offsetX = 20
width = w/2
end
local height = math.floor((h/wheelSize)*1.75)
for i = math.max(selectedIndex - wheelSize/2, 1), math.max(selectedIndex - 1,0) do
local song = songwheel.songs[i]
local xpos = x + width
local offsetY = (selectedIndex - i + ioffset/2) * ( height * 1.05)
local ypos = y+((h/2 - height/2) - offsetY)
draw_song(song, xpos, ypos, width, height)
end
--after selected
for i = math.min(selectedIndex + wheelSize/2, #songwheel.songs), selectedIndex + 1,-1 do
local song = songwheel.songs[i]
local xpos = x + width
local offsetY = (selectedIndex - i + ioffset/2) * ( height * 1.05)
local ypos = y+((h/2 - height/2) - (selectedIndex - i) - offsetY)
local alpha = 255 - (selectedIndex - i + ioffset) * 31
draw_song(song, xpos, ypos, width, height)
end
-- draw selected
local xpos = x + width
local offsetY = (ioffset/2) * ( height - (wheelSize/2*((1)*aspectFloat)))
local ypos = y+((h/2 - height/2) - (ioffset) - offsetY)
draw_song(songwheel.songs[selectedIndex], xpos, ypos, width, height, true)
-- cursor
gfx.BeginPath()
local ypos = y+((h/2 - height/2))
gfx.Rect(xpos, ypos, width, height)
gfx.FillColor(0,0,0,0)
gfx.StrokeColor(255,128,0)
gfx.StrokeWidth(3)
gfx.Fill()
gfx.Stroke()
return songwheel.songs[selectedIndex]
end
draw_legend_pane = function(x,y,w,h,obj)
local xpos = x+5
local ypos = y
local imageSize = h
gfx.BeginPath()
gfx.TextAlign(gfx.TEXT_ALIGN_MIDDLE + gfx.TEXT_ALIGN_LEFT)
gfx.ImageRect(x, y, imageSize, imageSize, obj.image, 1, 0)
xpos = xpos + imageSize + 5
gfx.FontSize(16);
if h < (w-(10+imageSize))/2 then
gfx.DrawLabel(obj.labelSingleLine, xpos, y+(h/2), w-(10+imageSize))
else
gfx.DrawLabel(obj.labelMultiLine, xpos, y+(h/2), w-(10+imageSize))
end
gfx.ForceRender()
end
draw_legend = function(x,y,w,h)
gfx.TextAlign(gfx.TEXT_ALIGN_MIDDLE + gfx.TEXT_ALIGN_LEFT);
gfx.BeginPath()
gfx.FillColor(0,0,0,170)
gfx.Rect(x,y,w,h)
gfx.Fill()
local xpos = 10;
local legendWidth = math.floor((w-20)/#legendTable)
for i,v in ipairs(legendTable) do
local xOffset = draw_legend_pane(xpos+(legendWidth*(i-1)), y+5,legendWidth,h-10,legendTable[i])
end
end
draw_search = function(x,y,w,h)
soffset = soffset + (searchIndex) - (songwheel.searchInputActive and 0 or 1)
if searchIndex ~= (songwheel.searchInputActive and 0 or 1) then
game.PlaySample("woosh")
end
searchIndex = songwheel.searchInputActive and 0 or 1
gfx.BeginPath()
local bgfade = 1 - (searchIndex + soffset)
--if not songwheel.searchInputActive then bgfade = soffset end
gfx.FillColor(0,0,0,math.floor(200 * bgfade))
gfx.Rect(0,0,resx,resy)
gfx.Fill()
gfx.ForceRender()
local xpos = x + (searchIndex + soffset)*w
gfx.UpdateLabel(searchText ,string.format("Search: %s",songwheel.searchText), 30, 0)
gfx.BeginPath()
gfx.RoundedRect(xpos,y,w,h,h/2)
gfx.FillColor(30,30,30)
gfx.StrokeColor(0,128,255)
gfx.StrokeWidth(1)
gfx.Fill()
gfx.Stroke()
gfx.BeginPath();
gfx.LoadSkinFont("NotoSans-Regular.ttf");
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_MIDDLE);
gfx.DrawLabel(searchText, xpos+10,y+(h/2), w-20)
end
render = function(deltaTime)
timer = (timer + deltaTime)
timer = timer % 2
resx,resy = game.GetResolution();
adjustScreen(resx,resy);
gfx.BeginPath();
gfx.LoadSkinFont("dfmarugoth.ttf");
gfx.FontSize(40);
gfx.FillColor(255,255,255);
if songwheel.songs[1] ~= nil then
--draw songwheel and get selected song
if aspectRatio == "PortraitWidescreen" then
local song = draw_songwheel(0,0,fullX,fullY)
--render selected song information
draw_selected(song, 0,0,fullX,resy)
else
local song = draw_songwheel(fifthX*2,0,fifthX*3,fullY)
--render selected song information
draw_selected(song, 0,0,fifthX*2,(fifthY/2)*9)
end
end
--Draw Legend Information
-- if showGuide then
-- if aspectRatio == "PortraitWidescreen" then
-- draw_legend(0,(fifthY/3)*14, fullX, (fifthY/3)*1)
-- else
-- draw_legend(0,(fifthY/2)*9, fullX, (fifthY/2))
-- end
-- end
gfx.BeginPath();
gfx.TextAlign(TEXT_ALIGN_CENTER + TEXT_ALIGN_MIDDLE);
gfx.ImageRect(0, 0, resx, resy, foreground, 1, 0);
--draw text search
if aspectRatio == "PortraitWidescreen" then
draw_search(fifthX*2,5,fifthX*3,fifthY/5)
else
draw_search(fifthX*2,5,fifthX*3,fifthY/3)
end
ioffset = ioffset * 0.9
doffset = doffset * 0.9
soffset = soffset * 0.8
if songwheel.searchStatus then
gfx.BeginPath()
gfx.FillColor(255,255,255)
gfx.FontSize(20);
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_TOP)
gfx.Text(songwheel.searchStatus, 3, 3)
end
if totalForce then
gfx.BeginPath()
gfx.FillColor(255,255,255)
gfx.FontSize(20);
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_BOTTOM)
local forceText = string.format("Force: %.2f", totalForce)
gfx.Text(forceText, 0, fullY)
end
gfx.LoadSkinFont("NotoSans-Regular.ttf");
gfx.ResetTransform()
gfx.ForceRender()
end
set_index = function(newIndex)
if newIndex ~= selectedIndex then
game.PlaySample("menu_click")
end
ioffset = ioffset + selectedIndex - newIndex
selectedIndex = newIndex
end;
set_diff = function(newDiff)
if newDiff ~= selectedDiff then
game.PlaySample("click-02")
end
doffset = doffset + selectedDiff - newDiff
selectedDiff = newDiff
end;
-- force calculation
--------------------
totalForce = nil
local badgeRates = {
0.5, -- Played
1.0, -- Cleared
1.02, -- Hard clear
1.04, -- UC
1.1 -- PUC
}
local gradeRates = {
{["min"] = 9900000, ["rate"] = 1.05}, -- S
{["min"] = 9800000, ["rate"] = 1.02}, -- AAA+
{["min"] = 9700000, ["rate"] = 1}, -- AAA
{["min"] = 9500000, ["rate"] = 0.97}, -- AA+
{["min"] = 9300000, ["rate"] = 0.94}, -- AA
{["min"] = 9000000, ["rate"] = 0.91}, -- A+
{["min"] = 8700000, ["rate"] = 0.88}, -- A
{["min"] = 7500000, ["rate"] = 0.85}, -- B
{["min"] = 6500000, ["rate"] = 0.82}, -- C
{["min"] = 0, ["rate"] = 0.8} -- D
}
calculate_force = function(diff)
if #diff.scores < 1 then
return 0
end
local score = diff.scores[1]
local badgeRate = badgeRates[diff.topBadge]
local gradeRate
for i, v in ipairs(gradeRates) do
if score.score >= v.min then
gradeRate = v.rate
break
end
end
return math.floor((diff.level * 2) * (score.score / 10000000) * gradeRate * badgeRate) / 100
end
songs_changed = function(withAll)
if not withAll then return end
recordCache = {}
local diffs = {}
for i = 1, #songwheel.allSongs do
local song = songwheel.allSongs[i]
for j = 1, #song.difficulties do
local diff = song.difficulties[j]
diff.force = calculate_force(diff)
table.insert(diffs, diff)
end
end
table.sort(diffs, function (l, r)
return l.force > r.force
end)
totalForce = 0
for i = 1, 50 do
if diffs[i] then
totalForce = totalForce + diffs[i].force
end
end
end

View File

@ -0,0 +1,897 @@
--Horizontal alignment
TEXT_ALIGN_LEFT = 1
TEXT_ALIGN_CENTER = 2
TEXT_ALIGN_RIGHT = 4
--Vertical alignment
TEXT_ALIGN_TOP = 8
TEXT_ALIGN_MIDDLE = 16
TEXT_ALIGN_BOTTOM = 32
TEXT_ALIGN_BASELINE = 64
local jacket = nil;
local selectedIndex = 1
local selectedDiff = 1
local songCache = {}
local ioffset = 0
local doffset = 0
local soffset = 0
local diffColors = {{0,0,255}, {0,255,0}, {255,0,0}, {255, 0, 255}}
local timer = 0
local effector = 0
local searchText = gfx.CreateLabel("",5,0)
local searchIndex = 1
local jacketFallback = gfx.CreateSkinImage("song_select/loading.png", 0)
local showGuide = game.GetSkinSetting("show_guide")
local legendTable = {
{["labelSingleLine"] = gfx.CreateLabel("DIFFICULTY SELECT",16, 0), ["labelMultiLine"] = gfx.CreateLabel("DIFFICULTY\nSELECT",16, 0), ["image"] = gfx.CreateSkinImage("legend/knob-left.png", 0)},
{["labelSingleLine"] = gfx.CreateLabel("MUSIC SELECT",16, 0), ["labelMultiLine"] = gfx.CreateLabel("MUSIC\nSELECT",16, 0), ["image"] = gfx.CreateSkinImage("legend/knob-right.png", 0)},
{["labelSingleLine"] = gfx.CreateLabel("FILTER MUSIC",16, 0), ["labelMultiLine"] = gfx.CreateLabel("FILTER\nMUSIC",16, 0), ["image"] = gfx.CreateSkinImage("legend/FX-L.png", 0)},
{["labelSingleLine"] = gfx.CreateLabel("SORT MUSIC",16, 0), ["labelMultiLine"] = gfx.CreateLabel("SORT\nMUSIC",16, 0), ["image"] = gfx.CreateSkinImage("legend/FX-R.png", 0)},
{["labelSingleLine"] = gfx.CreateLabel("MUSIC MODS",16, 0), ["labelMultiLine"] = gfx.CreateLabel("MUSIC\nMODS",16, 0), ["image"] = gfx.CreateSkinImage("legend/FX-LR.png", 0)},
{["labelSingleLine"] = gfx.CreateLabel("PLAY",16, 0), ["labelMultiLine"] = gfx.CreateLabel("PLAY",16, 0), ["image"] = gfx.CreateSkinImage("legend/start.png", 0)}
}
local grades = {
{["max"] = 6999999, ["image"] = gfx.CreateSkinImage("common/grades/D.png", 0)},
{["max"] = 7999999, ["image"] = gfx.CreateSkinImage("common/grades/C.png", 0)},
{["max"] = 8699999, ["image"] = gfx.CreateSkinImage("common/grades/B.png", 0)},
{["max"] = 8999999, ["image"] = gfx.CreateSkinImage("common/grades/A.png", 0)},
{["max"] = 9299999, ["image"] = gfx.CreateSkinImage("common/grades/A+.png", 0)},
{["max"] = 9499999, ["image"] = gfx.CreateSkinImage("common/grades/AA.png", 0)},
{["max"] = 9699999, ["image"] = gfx.CreateSkinImage("common/grades/AA+.png", 0)},
{["max"] = 9799999, ["image"] = gfx.CreateSkinImage("common/grades/AAA.png", 0)},
{["max"] = 9899999, ["image"] = gfx.CreateSkinImage("common/grades/AAA+.png", 0)},
{["max"] = 99999999, ["image"] = gfx.CreateSkinImage("common/grades/S.png", 0)}
}
local badges = {
gfx.CreateSkinImage("badges/played.png", 0),
gfx.CreateSkinImage("badges/clear.png", 0),
gfx.CreateSkinImage("badges/hard-clear.png", 0),
gfx.CreateSkinImage("badges/full-combo.png", 0),
gfx.CreateSkinImage("badges/perfect.png", 0)
}
local recordCache = {}
gfx.LoadSkinFont("dfmarugoth.ttf");
game.LoadSkinSample("menu_click")
game.LoadSkinSample("click-02")
game.LoadSkinSample("woosh")
local wheelSize = 12
get_page_size = function()
return math.floor(wheelSize/2)
end
-- Responsive UI variables
-- Aspect Ratios
local aspectFloat = 1.850
local aspectRatio = "widescreen"
local landscapeWidescreenRatio = 1.850
local landscapeStandardRatio = 1.500
local portraitWidescreenRatio = 0.5
-- Responsive sizes
local fifthX = 0
local fourthX= 0
local thirdX = 0
local halfX = 0
local fullX = 0
local fifthY = 0
local fourthY= 0
local thirdY = 0
local halfY = 0
local fullY = 0
adjustScreen = function(x,y)
local a = x/y;
if x >= y and a <= landscapeStandardRatio then
aspectRatio = "landscapeStandard"
aspectFloat = 1.1
elseif x >= y and landscapeStandardRatio <= a and a <= landscapeWidescreenRatio then
aspectRatio = "landscapeWidescreen"
aspectFloat = 1.2
elseif x <= y and portraitWidescreenRatio <= a and a < landscapeStandardRatio then
aspectRatio = "PortraitWidescreen"
aspectFloat = 0.5
else
aspectRatio = "landscapeWidescreen"
aspectFloat = 1.0
end
fifthX = x/5
fourthX= x/4
thirdX = x/3
halfX = x/2
fullX = x
fifthY = y/5
fourthY= y/4
thirdY = y/3
halfY = y/2
fullY = y
end
check_or_create_cache = function(song, loadJacket)
if not songCache[song.id] then songCache[song.id] = {} end
if not songCache[song.id]["title"] then
songCache[song.id]["title"] = gfx.CreateLabel(song.title, 14, 0)
end
if not songCache[song.id]["artist"] then
songCache[song.id]["artist"] = gfx.CreateLabel(song.artist, 25, 0)
end
if not songCache[song.id]["bpm"] then
songCache[song.id]["bpm"] = gfx.CreateLabel(string.format("BPM: %s",song.bpm), 20, 0)
end
if not songCache[song.id]["effector"] then
songCache[song.id]["effector"] = gfx.CreateLabel(string.format("BPM: %s",song.bpm), 20, 0)
end
if not songCache[song.id]["jacket"] and loadJacket then
songCache[song.id]["jacket"] = gfx.CreateImage(song.difficulties[1].jacketPath, 0)
end
end
function record_handler_factory(hash)
return (function(res)
if res.statusCode == 42 then
recordCache[hash] = {good=false, reason="Untracked"}
elseif res.statusCode == 20 and res.body ~= nil then
recordCache[hash] = {good=true, record=res.body.record}
elseif res.statusCode == 44 then
recordCache[hash] = {good=true, record=nil}
else
recordCache[hash] = {good=false, reason="Failed"}
end
end)
end
function get_record(hash)
if recordCache[hash] then return recordCache[hash] end
recordCache[hash] = {good=false, reason="Loading..."}
IR.Record(hash, record_handler_factory(hash))
return recordCache[hash]
end
function log_table(table)
str = "{"
for k, v in pairs(table) do
str = str .. k .. ": "
t = type(v)
if t == "table" then
str = str .. log_table(v)
elseif t == "string" then
str = str .. "\"" .. v .. "\""
elseif t == "boolean" then
if v then
str = str .. "true"
else
str = str .. "false"
end
else
str = str .. v
end
str = str .. ", "
end
return str .. "}"
end
draw_scores_ir = function(difficulty, x, y, w, h)
-- draw the top score for this difficulty
local xOffset = 5
local height = h/3 - 10
local ySpacing = h/3
local yOffset = h/3
gfx.FontSize(30);
gfx.TextAlign(gfx.TEXT_ALIGN_BOTTOM + gfx.TEXT_ALIGN_CENTER);
gfx.FastText("HIGH SCORE", x +(w/4), y+(h/2))
gfx.FastText("IR RECORD", x + (3/4 * w), y + (h/2))
gfx.BeginPath()
gfx.Rect(x+xOffset,y+h/2,w/2-(xOffset*2),h/2)
gfx.FillColor(30,30,30,10)
gfx.StrokeColor(0,128,255)
gfx.StrokeWidth(1)
gfx.Fill()
gfx.Stroke()
gfx.BeginPath()
gfx.Rect(x + xOffset + w/2,y+h/2,w/2-(xOffset*2),h/2)
gfx.FillColor(30,30,30,10)
gfx.StrokeColor(0,128,255)
gfx.StrokeWidth(1)
gfx.Fill()
gfx.Stroke()
if difficulty.scores[1] ~= nil then
local highScore = difficulty.scores[1]
scoreLabel = gfx.CreateLabel(string.format("%08d",highScore.score), 40, 0)
for i,v in ipairs(grades) do
if v.max > highScore.score then
gfx.BeginPath()
iw,ih = gfx.ImageSize(v.image)
iarr = ih / iw
oldheight = h/2 - 10
newheight = iarr * (h/2-10)
centreoffset = (oldheight - newheight)/2 + 3 -- +3 is stupid but ehhh
gfx.ImageRect(x+xOffset, y+h/2 + centreoffset, oldheight, newheight, v.image, 1, 0) --this is nasty but it works for me
break
end
end
if difficulty.topBadge ~= 0 then
gfx.BeginPath()
gfx.ImageRect(x+xOffset+w/2-h/2, y+h/2 +5, (h/2-10), h/2-10, badges[difficulty.topBadge], 1, 0)
end
gfx.FillColor(255,255,255)
gfx.FontSize(40);
gfx.TextAlign(gfx.TEXT_ALIGN_MIDDLE + gfx.TEXT_ALIGN_CENTER);
gfx.DrawLabel(scoreLabel, x+(w/4),y+(h/4)*3,w/2)
end
irRecord = get_record(difficulty.hash)
if not irRecord.good then
recordLabel = gfx.CreateLabel(irRecord.reason, 40, 0)
gfx.FillColor(255, 255, 255)
gfx.FontSize(40)
gfx.TextAlign(gfx.TEXT_ALIGN_MIDDLE + gfx.TEXT_ALIGN_CENTER);
gfx.DrawLabel(recordLabel, x+(w * 3/4),y+(h/4)*3,w/2)
elseif irRecord.record == nil then --record not set, but can be tracked
recordLabel = gfx.CreateLabel(string.format("%08d", 0), 40, 0)
gfx.FillColor(170, 170, 170)
gfx.FontSize(40)
gfx.TextAlign(gfx.TEXT_ALIGN_MIDDLE + gfx.TEXT_ALIGN_CENTER);
gfx.DrawLabel(recordLabel, x+(w * 3/4),y+(h/4)*3,w/2)
else
recordScoreLabel = gfx.CreateLabel(string.format("%08d", irRecord.record.score), 26, 0)
recordPlayerLabel = gfx.CreateLabel(irRecord.record.username, 26, 0)
if irRecord.record.lamp ~= 0 then
gfx.BeginPath()
gfx.ImageRect(x+xOffset+w-h/2, y+h/2 +5, (h/2-10), h/2-10, badges[irRecord.record.lamp], 1, 0)
end
for i,v in ipairs(grades) do
if v.max > irRecord.record.score then
gfx.BeginPath()
iw,ih = gfx.ImageSize(v.image)
iarr = ih / iw
oldheight = h/2 - 10
newheight = iarr * (h/2-10)
centreoffset = (oldheight - newheight)/2 + 3 -- +3 is stupid but ehhh
gfx.ImageRect(x+xOffset+w/2, y+h/2 + centreoffset, oldheight, newheight, v.image, 1, 0) --this is nasty but it works for me
break
end
end
gfx.FillColor(255, 255, 255)
gfx.FontSize(40)
gfx.TextAlign(gfx.TEXT_ALIGN_MIDDLE + gfx.TEXT_ALIGN_CENTER);
gfx.DrawLabel(recordPlayerLabel, x+(w * 3/4),y+(h/4)*2.55,w/2)
gfx.DrawLabel(recordScoreLabel, x+(w * 3/4),y+(h/4)*3.45,w/2)
end
end
draw_scores = function(difficulty, x, y, w, h)
if IRData.Active then return draw_scores_ir(difficulty, x, y, w, h) end
-- draw the top score for this difficulty
local xOffset = 5
local height = h/3 - 10
local ySpacing = h/3
local yOffset = h/3
gfx.FontSize(30);
gfx.TextAlign(gfx.TEXT_ALIGN_BOTTOM + gfx.TEXT_ALIGN_CENTER);
gfx.FastText("HIGH SCORE", x +(w/2), y+(h/2))
gfx.BeginPath()
gfx.Rect(x+xOffset,y+h/2,w-(xOffset*2),h/2)
gfx.FillColor(30,30,30,10)
gfx.StrokeColor(0,128,255)
gfx.StrokeWidth(1)
gfx.Fill()
gfx.Stroke()
if difficulty.scores[1] ~= nil then
local highScore = difficulty.scores[1]
scoreLabel = gfx.CreateLabel(string.format("%08d",highScore.score), 40, 0)
for i,v in ipairs(grades) do
if v.max > highScore.score then
gfx.BeginPath()
iw,ih = gfx.ImageSize(v.image)
iar = iw / ih;
gfx.ImageRect(x+xOffset,y+h/2 +5, iar * (h/2-10),h/2-10, v.image, 1, 0)
break
end
end
if difficulty.topBadge ~= 0 then
gfx.BeginPath()
gfx.ImageRect(x+xOffset+w-h/2, y+h/2 +5, (h/2-10), h/2-10, badges[difficulty.topBadge], 1, 0)
end
gfx.FillColor(255,255,255)
gfx.FontSize(40);
gfx.TextAlign(gfx.TEXT_ALIGN_MIDDLE + gfx.TEXT_ALIGN_CENTER);
gfx.DrawLabel(scoreLabel, x+(w/2),y+(h/4)*3,w)
end
end
draw_song = function(song, x, y, w, h, selected)
local difficulty = song.difficulties[selectedDiff]
local clearLampR = 255
local clearLampG = 255
local clearLampB = 255
local clearLampA = 100
if difficulty ~= nil then
if difficulty.scores[1] ~= nil then
if difficulty.topBadge == 1 then -- fail/played
clearLampR = 255
clearLampG = 25
clearLampB = 25
clearLampA = 200
end
if difficulty.topBadge == 2 then -- clear
clearLampR = 25
clearLampG = 255
clearLampB = 25
clearLampA = 200
end
if difficulty.topBadge == 3 then -- hard clear
clearLampR = 255
clearLampG = 25
clearLampB = 255
clearLampA = 200
end
if difficulty.topBadge == 4 then -- full combo
clearLampR = 255
clearLampG = 100
clearLampB = 25
clearLampA = 200
end
if difficulty.topBadge == 5 then -- perfect
clearLampR = 255
clearLampG = 255
clearLampB = 25
clearLampA = 200
end
end
end
check_or_create_cache(song)
gfx.BeginPath()
gfx.Rect(x+1,y+1, w-2, h-2)
gfx.FillColor(220,220,220)
gfx.StrokeColor(0,8,0)
gfx.StrokeWidth(2)
gfx.Fill()
gfx.Stroke()
gfx.FillColor(255,255,255)
if not songCache[song.id][1] or songCache[song.id][1] == jacketFallback then
songCache[song.id][1] = gfx.LoadImageJob(song.difficulties[1].jacketPath, jacketFallback, 200,200)
end
if songCache[song.id][1] then
gfx.BeginPath()
gfx.ImageRect(x+2, y+2, h-4, h-4, songCache[song.id][1], 1, 0)
end
-- Song title
gfx.BeginPath()
gfx.Rect(x+1, y + h - h/4 - 1, w-2, h/4)
gfx.FillColor(0,0,0,200)
gfx.Fill()
gfx.BeginPath()
gfx.FillColor(255,255,255)
gfx.TextAlign(gfx.TEXT_ALIGN_BOTTOM + gfx.TEXT_ALIGN_LEFT)
gfx.DrawLabel(songCache[song.id]["title"], (x)+h/2 + 4, y + h - 7, -1)
--gfx.DrawLabel(songCache[song.id]["artist"], x+10, y + 50, w-10)
-- Song difficulty
gfx.BeginPath()
gfx.Rect(x - 1, y + h-h/2 - 4, h/2, h/2)
gfx.FillColor(0,0,0,200)
gfx.Fill()
gfx.FillColor(255,255,255)
gfx.LoadSkinFont("commext.ttf")
gfx.FontSize(28)
gfx.TextAlign(gfx.TEXT_ALIGN_CENTER + gfx.TEXT_ALIGN_BOTTOM)
if (song.difficulties[selectedDiff] ~= nil) then
gfx.FastText(song.difficulties[selectedDiff].level, x + h/4, y + h - 10)
else
gfx.FastText(song.difficulties[selectedDiff - 1].level, x + h/4, y + h - 10)
end
-- CLEAN THIS SHIT UP
local diff_long = ""
local diff_short = ""
if (song.difficulties[selectedDiff] ~= nil) then
if (song.difficulties[selectedDiff].difficulty == 0) then
diff_long = "NOVICE"
diff_short = "NOV"
elseif (song.difficulties[selectedDiff].difficulty == 1) then
diff_long = "ADVANCED"
diff_short = "ADV"
elseif (song.difficulties[selectedDiff].difficulty == 2) then
diff_long = "EXHAUST"
diff_short = "EXH"
elseif (song.difficulties[selectedDiff].difficulty == 3) then
diff_long = "INFINITE"
diff_short = "INF"
else
diff_long = "UNKNOWN"
diff_short = "???"
end
else
if (song.difficulties[selectedDiff - 1].difficulty == 0) then
diff_long = "NOVICE"
diff_short = "NOV"
elseif (song.difficulties[selectedDiff - 1].difficulty == 1) then
diff_long = "ADVANCED"
diff_short = "ADV"
elseif (song.difficulties[selectedDiff - 1].difficulty == 2) then
diff_long = "EXHAUST"
diff_short = "EXH"
elseif (song.difficulties[selectedDiff - 1].difficulty == 3) then
diff_long = "INFINITE"
diff_short = "INF"
else
diff_long = "UNKNOWN"
diff_short = "???"
end
end
gfx.FontSize(8)
gfx.LoadSkinFont("dfmarugoth.ttf")
gfx.FastText(diff_long, x + h/4, y + h - 7)
local seldiff = nil
if song.difficulties[selectedDiff] ~= nil then
seldiff = selectedDiff
else
seldiff = selectedDiff - 1
end
if song.difficulties[seldiff].topBadge ~= 0 then
if song.difficulties[seldiff].scores[1] ~= nil then
local highScore = song.difficulties[seldiff].scores[1]
for i,v in ipairs(grades) do
if v.max > highScore.score then
gfx.BeginPath()
iw,ih = gfx.ImageSize(v.image)
iar = iw / ih;
gfx.ImageRect(x + w/1.45, y + h/8 + 2, (h/1.5-14), h/1.5-14, v.image, 1, 0)
break
end
end
end
gfx.BeginPath()
gfx.ImageRect(x + w/2, y + h/8, (h/1.5-10), h/1.5-10, badges[song.difficulties[seldiff].topBadge], 1, 0)
end
end
draw_diff_icon = function(diff, x, y, w, h, selected)
local shrinkX = w/4
local shrinkY = h/4
if selected then
gfx.FontSize(h/2)
shrinkX = w/6
shrinkY = h/6
else
gfx.FontSize(math.floor(h / 3))
end
gfx.BeginPath()
gfx.RoundedRectVarying(x+shrinkX,y+shrinkY,w-shrinkX*2,h-shrinkY*2,0,0,0,0)
gfx.FillColor(15,15,15)
gfx.StrokeColor(table.unpack(diffColors[diff.difficulty + 1]))
gfx.StrokeWidth(2)
gfx.Fill()
gfx.Stroke()
gfx.FillColor(255,255,255)
gfx.TextAlign(gfx.TEXT_ALIGN_MIDDLE + gfx.TEXT_ALIGN_CENTER)
gfx.FastText(tostring(diff.level), x+(w/2),y+(h/2))
end
draw_cursor = function(x,y,rotation,width)
gfx.Save()
gfx.BeginPath();
gfx.Translate(x,y)
gfx.Rotate(rotation)
gfx.StrokeColor(255,128,0)
gfx.StrokeWidth(4)
gfx.Rect(-width/2, -width/2, width, width)
gfx.Stroke()
gfx.Restore()
end
draw_diffs = function(diffs, x, y, w, h)
local diffWidth = w/2.5
local diffHeight = w/2.5
local diffCount = #diffs
gfx.Scissor(x,y,w,h)
for i = math.max(selectedDiff - 2, 1), math.max(selectedDiff - 1,1) do
local diff = diffs[i]
local xpos = x + ((w/2 - diffWidth/2) + (selectedDiff - i + doffset)*(-0.8*diffWidth))
if i ~= selectedDiff then
draw_diff_icon(diff, xpos, y, diffWidth, diffHeight, false)
end
end
--after selected
for i = math.min(selectedDiff + 2, diffCount), selectedDiff + 1,-1 do
local diff = diffs[i]
local xpos = x + ((w/2 - diffWidth/2) + (selectedDiff - i + doffset)*(-0.8*diffWidth))
if i ~= selectedDiff then
draw_diff_icon(diff, xpos, y, diffWidth, diffHeight, false)
end
end
local diff = diffs[selectedDiff]
local xpos = x + ((w/2 - diffWidth/2) + (doffset)*(-0.8*diffWidth))
draw_diff_icon(diff, xpos, y, diffWidth, diffHeight, true)
gfx.BeginPath()
gfx.FillColor(0,128,255)
gfx.Rect(x,y+10,2,diffHeight-h/6)
gfx.Fill()
gfx.BeginPath()
gfx.Rect(x+w-2,y+10,2,diffHeight-h/6)
gfx.Fill()
gfx.ResetScissor()
draw_cursor(x + w/2, y +diffHeight/2, timer * math.pi, diffHeight / 1.5)
end
draw_selected = function(song, x, y, w, h)
check_or_create_cache(song)
-- set up padding and margins
local xPadding = math.floor(w/16)
local yPadding = math.floor(h/32)
local xMargin = math.floor(w/16)
local yMargin = math.floor(h/32)
local width = (w-(xMargin*2))
local height = (h-(yMargin*2))
local xpos = x+xMargin
local ypos = y+yMargin
if aspectRatio == "PortraitWidescreen" then
xPadding = math.floor(w/32)
yPadding = math.floor(h/32)
xMargin = math.floor(w/34)
yMargin = math.floor(h/32)
width = ((w/2)-(xMargin))
height = (h-(yMargin*2))
xpos = x+xMargin/2
ypos = y+yMargin
end
--Border
local diff = song.difficulties[selectedDiff]
gfx.BeginPath()
gfx.RoundedRectVarying(xpos,ypos,width,height,yPadding,yPadding,yPadding,yPadding)
gfx.FillColor(30,30,30,100)
gfx.StrokeColor(0,128,255)
gfx.StrokeWidth(1)
gfx.Fill()
gfx.Stroke()
-- jacket should take up 1/3 of height, always be square, and be centered
local imageSize = math.floor(height/3)
local imageXPos = ((width/2) - (imageSize/2)) + x+xMargin
if aspectRatio == "PortraitWidescreen" then
--Unless its portrait widesreen..
imageSize = math.floor((height/8)*2)
imageXPos = x+xMargin
end
if not songCache[song.id][selectedDiff] or songCache[song.id][selectedDiff] == jacketFallback then
songCache[song.id][selectedDiff] = gfx.LoadImageJob(diff.jacketPath, jacketFallback, 200,200)
end
if songCache[song.id][selectedDiff] then
gfx.BeginPath()
gfx.ImageRect(imageXPos, y+yMargin+yPadding, imageSize, imageSize, songCache[song.id][selectedDiff], 1, 0)
end
-- difficulty should take up 1/6 of height, full width, and be centered
gfx.LoadSkinFont("commext.ttf")
if aspectRatio == "PortraitWidescreen" then
--difficulty wheel should be right below the jacketImage, and the same width as
--the jacketImage
draw_diffs(song.difficulties,xpos+xPadding,(ypos+yPadding+imageSize),imageSize,math.floor((height/3)*1)-yPadding)
else
-- difficulty should take up 1/6 of height, full width, and be centered
draw_diffs(song.difficulties,(w/2)-(imageSize/2),(ypos+yPadding+imageSize),imageSize,math.floor(height/6))
end
-- effector / bpm should take up 1/3 of height, full width
gfx.LoadSkinFont("dfmarugoth.ttf")
if aspectRatio == "PortraitWidescreen" then
gfx.FontSize(40)
gfx.TextAlign(gfx.TEXT_ALIGN_TOP + gfx.TEXT_ALIGN_LEFT)
gfx.DrawLabel(songCache[song.id]["title"], xpos+xPadding+imageSize, y+yMargin+yPadding, width-imageSize-20)
gfx.FontSize(30)
gfx.DrawLabel(songCache[song.id]["artist"], xpos+xPadding+imageSize+3, y+yMargin+yPadding + 45, width-imageSize-20)
gfx.FontSize(20)
gfx.DrawLabel(songCache[song.id]["bpm"], xpos+xPadding+imageSize+3, y+yMargin+yPadding + 85, width-imageSize-20)
gfx.FastText(string.format("Effector: %s", diff.effector), xpos+xPadding+imageSize+3, y+yMargin+yPadding + 115)
else
gfx.FontSize(40)
gfx.TextAlign(gfx.TEXT_ALIGN_TOP + gfx.TEXT_ALIGN_LEFT)
gfx.DrawLabel(songCache[song.id]["title"], xpos+10, (height/10)*6, width-20)
gfx.FontSize(30)
gfx.DrawLabel(songCache[song.id]["artist"], xpos+10, (height/10)*6 + 45, width-20)
gfx.FillColor(255,255,255)
gfx.FontSize(20)
gfx.DrawLabel(songCache[song.id]["bpm"], xpos+10, (height/10)*6 + 85)
gfx.FastText(string.format("Effector: %s", diff.effector),xpos+10, (height/10)*6 + 115)
end
if aspectRatio == "PortraitWidescreen" then
draw_scores(diff, xpos+xPadding+imageSize+3, (height/3)*2, width-imageSize-20, (height/3)-yPadding)
else
draw_scores(diff, xpos, (height/6)*5, width, (height/6))
end
gfx.ForceRender()
end
draw_songwheel = function(x,y,w,h)
local offsetX = fifthX/2
local width = math.floor((w/5)*4)
if aspectRatio == "landscapeWidescreen" then
wheelSize = 12
offsetX = 80
elseif aspectRatio == "landscapeStandard" then
wheelSize = 10
offsetX = 40
elseif aspectRatio == "PortraitWidescreen" then
wheelSize = 20
offsetX = 20
width = w/2
end
local height = math.floor((h/wheelSize)*1.75)
for i = math.max(selectedIndex - wheelSize/2, 1), math.max(selectedIndex - 1,0) do
local song = songwheel.songs[i]
local xpos = x + width
local offsetY = (selectedIndex - i + ioffset/2) * ( height * 1.05)
local ypos = y+((h/2 - height/2) - offsetY)
draw_song(song, xpos, ypos, width, height)
end
--after selected
for i = math.min(selectedIndex + wheelSize/2, #songwheel.songs), selectedIndex + 1,-1 do
local song = songwheel.songs[i]
local xpos = x + width
local offsetY = (selectedIndex - i + ioffset/2) * ( height * 1.05)
local ypos = y+((h/2 - height/2) - (selectedIndex - i) - offsetY)
local alpha = 255 - (selectedIndex - i + ioffset) * 31
draw_song(song, xpos, ypos, width, height)
end
-- draw selected
local xpos = x + width
local offsetY = (ioffset/2) * ( height - (wheelSize/2*((1)*aspectFloat)))
local ypos = y+((h/2 - height/2) - (ioffset) - offsetY)
draw_song(songwheel.songs[selectedIndex], xpos, ypos, width, height, true)
-- cursor
gfx.BeginPath()
local ypos = y+((h/2 - height/2))
gfx.Rect(xpos, ypos, width, height)
gfx.FillColor(0,0,0,0)
gfx.StrokeColor(255,128,0)
gfx.StrokeWidth(3)
gfx.Fill()
gfx.Stroke()
return songwheel.songs[selectedIndex]
end
draw_legend_pane = function(x,y,w,h,obj)
local xpos = x+5
local ypos = y
local imageSize = h
gfx.BeginPath()
gfx.TextAlign(gfx.TEXT_ALIGN_MIDDLE + gfx.TEXT_ALIGN_LEFT)
gfx.ImageRect(x, y, imageSize, imageSize, obj.image, 1, 0)
xpos = xpos + imageSize + 5
gfx.FontSize(16);
if h < (w-(10+imageSize))/2 then
gfx.DrawLabel(obj.labelSingleLine, xpos, y+(h/2), w-(10+imageSize))
else
gfx.DrawLabel(obj.labelMultiLine, xpos, y+(h/2), w-(10+imageSize))
end
gfx.ForceRender()
end
draw_legend = function(x,y,w,h)
gfx.TextAlign(gfx.TEXT_ALIGN_MIDDLE + gfx.TEXT_ALIGN_LEFT);
gfx.BeginPath()
gfx.FillColor(0,0,0,170)
gfx.Rect(x,y,w,h)
gfx.Fill()
local xpos = 10;
local legendWidth = math.floor((w-20)/#legendTable)
for i,v in ipairs(legendTable) do
local xOffset = draw_legend_pane(xpos+(legendWidth*(i-1)), y+5,legendWidth,h-10,legendTable[i])
end
end
draw_search = function(x,y,w,h)
soffset = soffset + (searchIndex) - (songwheel.searchInputActive and 0 or 1)
if searchIndex ~= (songwheel.searchInputActive and 0 or 1) then
game.PlaySample("woosh")
end
searchIndex = songwheel.searchInputActive and 0 or 1
gfx.BeginPath()
local bgfade = 1 - (searchIndex + soffset)
--if not songwheel.searchInputActive then bgfade = soffset end
gfx.FillColor(0,0,0,math.floor(200 * bgfade))
gfx.Rect(0,0,resx,resy)
gfx.Fill()
gfx.ForceRender()
local xpos = x + (searchIndex + soffset)*w
gfx.UpdateLabel(searchText ,string.format("Search: %s",songwheel.searchText), 30, 0)
gfx.BeginPath()
gfx.RoundedRect(xpos,y,w,h,h/2)
gfx.FillColor(30,30,30)
gfx.StrokeColor(0,128,255)
gfx.StrokeWidth(1)
gfx.Fill()
gfx.Stroke()
gfx.BeginPath();
gfx.LoadSkinFont("NotoSans-Regular.ttf");
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_MIDDLE);
gfx.DrawLabel(searchText, xpos+10,y+(h/2), w-20)
end
render = function(deltaTime)
timer = (timer + deltaTime)
timer = timer % 2
resx,resy = game.GetResolution();
adjustScreen(resx,resy);
gfx.BeginPath();
gfx.LoadSkinFont("dfmarugoth.ttf");
gfx.FontSize(40);
gfx.FillColor(255,255,255);
if songwheel.songs[1] ~= nil then
--draw songwheel and get selected song
if aspectRatio == "PortraitWidescreen" then
local song = draw_songwheel(0,0,fullX,fullY)
--render selected song information
draw_selected(song, 0,0,fullX,resy)
else
local song = draw_songwheel(fifthX*2,0,fifthX*3,fullY)
--render selected song information
draw_selected(song, 0,0,fifthX*2,(fifthY/2)*9)
end
end
--Draw Legend Information
-- if showGuide then
-- if aspectRatio == "PortraitWidescreen" then
-- draw_legend(0,(fifthY/3)*14, fullX, (fifthY/3)*1)
-- else
-- draw_legend(0,(fifthY/2)*9, fullX, (fifthY/2))
-- end
-- end
--draw text search
if aspectRatio == "PortraitWidescreen" then
draw_search(fifthX*2,5,fifthX*3,fifthY/5)
else
draw_search(fifthX*2,5,fifthX*3,fifthY/3)
end
ioffset = ioffset * 0.9
doffset = doffset * 0.9
soffset = soffset * 0.8
if songwheel.searchStatus then
gfx.BeginPath()
gfx.FillColor(255,255,255)
gfx.FontSize(20);
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_TOP)
gfx.Text(songwheel.searchStatus, 3, 3)
end
if totalForce then
gfx.BeginPath()
gfx.FillColor(255,255,255)
gfx.FontSize(20);
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_BOTTOM)
local forceText = string.format("Force: %.2f", totalForce)
gfx.Text(forceText, 0, fullY)
end
gfx.LoadSkinFont("NotoSans-Regular.ttf");
gfx.ResetTransform()
gfx.ForceRender()
end
set_index = function(newIndex)
if newIndex ~= selectedIndex then
game.PlaySample("menu_click")
end
ioffset = ioffset + selectedIndex - newIndex
selectedIndex = newIndex
end;
set_diff = function(newDiff)
if newDiff ~= selectedDiff then
game.PlaySample("click-02")
end
doffset = doffset + selectedDiff - newDiff
selectedDiff = newDiff
end;
-- force calculation
--------------------
totalForce = nil
local badgeRates = {
0.5, -- Played
1.0, -- Cleared
1.02, -- Hard clear
1.04, -- UC
1.1 -- PUC
}
local gradeRates = {
{["min"] = 9900000, ["rate"] = 1.05}, -- S
{["min"] = 9800000, ["rate"] = 1.02}, -- AAA+
{["min"] = 9700000, ["rate"] = 1}, -- AAA
{["min"] = 9500000, ["rate"] = 0.97}, -- AA+
{["min"] = 9300000, ["rate"] = 0.94}, -- AA
{["min"] = 9000000, ["rate"] = 0.91}, -- A+
{["min"] = 8700000, ["rate"] = 0.88}, -- A
{["min"] = 7500000, ["rate"] = 0.85}, -- B
{["min"] = 6500000, ["rate"] = 0.82}, -- C
{["min"] = 0, ["rate"] = 0.8} -- D
}
calculate_force = function(diff)
if #diff.scores < 1 then
return 0
end
local score = diff.scores[1]
local badgeRate = badgeRates[diff.topBadge]
local gradeRate
for i, v in ipairs(gradeRates) do
if score.score >= v.min then
gradeRate = v.rate
break
end
end
return math.floor((diff.level * 2) * (score.score / 10000000) * gradeRate * badgeRate) / 100
end
songs_changed = function(withAll)
if not withAll then return end
recordCache = {}
local diffs = {}
for i = 1, #songwheel.allSongs do
local song = songwheel.allSongs[i]
for j = 1, #song.difficulties do
local diff = song.difficulties[j]
diff.force = calculate_force(diff)
table.insert(diffs, diff)
end
end
table.sort(diffs, function (l, r)
return l.force > r.force
end)
totalForce = 0
for i = 1, 50 do
if diffs[i] then
totalForce = totalForce + diffs[i].force
end
end
end

View File

@ -0,0 +1,945 @@
-- game.Log("Something went wrong!", game.LOGGER_ERROR)
--Horizontal alignment
TEXT_ALIGN_LEFT = 1
TEXT_ALIGN_CENTER = 2
TEXT_ALIGN_RIGHT = 4
--Vertical alignment
TEXT_ALIGN_TOP = 8
TEXT_ALIGN_MIDDLE = 16
TEXT_ALIGN_BOTTOM = 32
TEXT_ALIGN_BASELINE = 64
local jacket = nil;
local selectedIndex = 1
local selectedDiff = 1
local songCache = {}
local ioffset = 0
local doffset = 0
local soffset = 0
local diffColors = {{0,0,255}, {0,255,0}, {255,0,0}, {255, 0, 255}}
local timer = 0
local effector = 0
local searchText = gfx.CreateLabel("",5,0)
local searchIndex = 1
local jacketFallback = gfx.CreateSkinImage("song_select/loading.png", 0)
local showGuide = game.GetSkinSetting("show_guide")
local legendTable = {
{["labelSingleLine"] = gfx.CreateLabel("DIFFICULTY SELECT",16, 0), ["labelMultiLine"] = gfx.CreateLabel("DIFFICULTY\nSELECT",16, 0), ["image"] = gfx.CreateSkinImage("legend/knob-left.png", 0)},
{["labelSingleLine"] = gfx.CreateLabel("MUSIC SELECT",16, 0), ["labelMultiLine"] = gfx.CreateLabel("MUSIC\nSELECT",16, 0), ["image"] = gfx.CreateSkinImage("legend/knob-right.png", 0)},
{["labelSingleLine"] = gfx.CreateLabel("FILTER MUSIC",16, 0), ["labelMultiLine"] = gfx.CreateLabel("FILTER\nMUSIC",16, 0), ["image"] = gfx.CreateSkinImage("legend/FX-L.png", 0)},
{["labelSingleLine"] = gfx.CreateLabel("SORT MUSIC",16, 0), ["labelMultiLine"] = gfx.CreateLabel("SORT\nMUSIC",16, 0), ["image"] = gfx.CreateSkinImage("legend/FX-R.png", 0)},
{["labelSingleLine"] = gfx.CreateLabel("MUSIC MODS",16, 0), ["labelMultiLine"] = gfx.CreateLabel("MUSIC\nMODS",16, 0), ["image"] = gfx.CreateSkinImage("legend/FX-LR.png", 0)},
{["labelSingleLine"] = gfx.CreateLabel("PLAY",16, 0), ["labelMultiLine"] = gfx.CreateLabel("PLAY",16, 0), ["image"] = gfx.CreateSkinImage("legend/start.png", 0)}
}
local grades = {
{["max"] = 6999999, ["image"] = gfx.CreateSkinImage("common/grades/D.png", 0)},
{["max"] = 7999999, ["image"] = gfx.CreateSkinImage("common/grades/C.png", 0)},
{["max"] = 8699999, ["image"] = gfx.CreateSkinImage("common/grades/B.png", 0)},
{["max"] = 8999999, ["image"] = gfx.CreateSkinImage("common/grades/A.png", 0)},
{["max"] = 9299999, ["image"] = gfx.CreateSkinImage("common/grades/A+.png", 0)},
{["max"] = 9499999, ["image"] = gfx.CreateSkinImage("common/grades/AA.png", 0)},
{["max"] = 9699999, ["image"] = gfx.CreateSkinImage("common/grades/AA+.png", 0)},
{["max"] = 9799999, ["image"] = gfx.CreateSkinImage("common/grades/AAA.png", 0)},
{["max"] = 9899999, ["image"] = gfx.CreateSkinImage("common/grades/AAA+.png", 0)},
{["max"] = 99999999, ["image"] = gfx.CreateSkinImage("common/grades/S.png", 0)}
}
local badges = {
gfx.CreateSkinImage("badges/played.png", 0),
gfx.CreateSkinImage("badges/clear.png", 0),
gfx.CreateSkinImage("badges/hard-clear.png", 0),
gfx.CreateSkinImage("badges/full-combo.png", 0),
gfx.CreateSkinImage("badges/perfect.png", 0)
}
local difficultyNumbers = {
[0] = gfx.CreateSkinImage("diff_num/0.png", 0),
[1] = gfx.CreateSkinImage("diff_num/1.png", 0),
[2] = gfx.CreateSkinImage("diff_num/2.png", 0),
[3] = gfx.CreateSkinImage("diff_num/3.png", 0),
[4] = gfx.CreateSkinImage("diff_num/4.png", 0),
[5] = gfx.CreateSkinImage("diff_num/5.png", 0),
[6] = gfx.CreateSkinImage("diff_num/6.png", 0),
[7] = gfx.CreateSkinImage("diff_num/7.png", 0),
[8] = gfx.CreateSkinImage("diff_num/8.png", 0),
[9] = gfx.CreateSkinImage("diff_num/9.png", 0),
};
local difficultyNameOverlays = {
[0] = gfx.CreateSkinImage("song_select/level/novice.png", 0),
[1] = gfx.CreateSkinImage("song_select/level/advanced.png", 0),
[2] = gfx.CreateSkinImage("song_select/level/exhaust.png", 0),
[3] = gfx.CreateSkinImage("song_select/level/maximum.png", 0),
[4] = gfx.CreateSkinImage("song_select/level/maximum.png", 0),
[5] = gfx.CreateSkinImage("song_select/level/maximum.png", 0),
[6] = gfx.CreateSkinImage("song_select/level/maximum.png", 0),
[7] = gfx.CreateSkinImage("song_select/level/maximum.png", 0),
}
local difficultyLevelCursor = gfx.CreateSkinImage("song_select/level_cursor.png", 0);
local foreground = gfx.CreateSkinImage("song_select/fg.png", 0);
local datapanel = gfx.CreateSkinImage("song_select/data_bg.png", 0);
local recordCache = {}
gfx.LoadSkinFont("dfmarugoth.ttf");
game.LoadSkinSample("menu_click")
game.LoadSkinSample("click-02")
game.LoadSkinSample("woosh")
local wheelSize = 12
get_page_size = function()
return math.floor(wheelSize/2)
end
-- Responsive UI variables
-- Aspect Ratios
local aspectFloat = 1.850
local aspectRatio = "widescreen"
local landscapeWidescreenRatio = 1.850
local landscapeStandardRatio = 1.500
local portraitWidescreenRatio = 0.5
-- Responsive sizes
local fifthX = 0
local fourthX= 0
local thirdX = 0
local halfX = 0
local fullX = 0
local fifthY = 0
local fourthY= 0
local thirdY = 0
local halfY = 0
local fullY = 0
adjustScreen = function(x,y)
local a = x/y;
if x >= y and a <= landscapeStandardRatio then
aspectRatio = "landscapeStandard"
aspectFloat = 1.1
elseif x >= y and landscapeStandardRatio <= a and a <= landscapeWidescreenRatio then
aspectRatio = "landscapeWidescreen"
aspectFloat = 1.2
elseif x <= y and portraitWidescreenRatio <= a and a < landscapeStandardRatio then
aspectRatio = "PortraitWidescreen"
aspectFloat = 0.5
else
aspectRatio = "landscapeWidescreen"
aspectFloat = 1.0
end
fifthX = x/5
fourthX= x/4
thirdX = x/3
halfX = x/2
fullX = x
fifthY = y/5
fourthY= y/4
thirdY = y/3
halfY = y/2
fullY = y
end
check_or_create_cache = function(song, loadJacket)
if not songCache[song.id] then songCache[song.id] = {} end
if not songCache[song.id]["title"] then
songCache[song.id]["title"] = gfx.CreateLabel(song.title, 14, 0)
end
if not songCache[song.id]["artist"] then
songCache[song.id]["artist"] = gfx.CreateLabel(song.artist, 14, 0)
end
if not songCache[song.id]["bpm"] then
songCache[song.id]["bpm"] = gfx.CreateLabel(string.format("%s",song.bpm), 12, 0)
end
if not songCache[song.id]["effector"] then
songCache[song.id]["effector"] = gfx.CreateLabel(string.format("BPM: %s",song.bpm), 20, 0)
end
if not songCache[song.id]["jacket"] then
songCache[song.id]["jacket"] = { }
end
for i = 1, #song.difficulties do
songCache[song.id]["jacket"][i] = gfx.LoadImageJob(song.difficulties[i].jacketPath, jacketFallback, 400, 400)
end
end
function record_handler_factory(hash)
return (function(res)
if res.statusCode == 42 then
recordCache[hash] = {good=false, reason="Untracked"}
elseif res.statusCode == 20 and res.body ~= nil then
recordCache[hash] = {good=true, record=res.body.record}
elseif res.statusCode == 44 then
recordCache[hash] = {good=true, record=nil}
else
recordCache[hash] = {good=false, reason="Failed"}
end
end)
end
function get_record(hash)
if recordCache[hash] then return recordCache[hash] end
recordCache[hash] = {good=false, reason="Loading..."}
IR.Record(hash, record_handler_factory(hash))
return recordCache[hash]
end
function log_table(table)
str = "{"
for k, v in pairs(table) do
str = str .. k .. ": "
t = type(v)
if t == "table" then
str = str .. log_table(v)
elseif t == "string" then
str = str .. "\"" .. v .. "\""
elseif t == "boolean" then
if v then
str = str .. "true"
else
str = str .. "false"
end
else
str = str .. v
end
str = str .. ", "
end
return str .. "}"
end
draw_scores_ir = function(difficulty, x, y, w, h)
-- draw the top score for this difficulty
local xOffset = 5
local height = h/3 - 10
local ySpacing = h/3
local yOffset = h/3
gfx.FontSize(30);
gfx.TextAlign(gfx.TEXT_ALIGN_BOTTOM + gfx.TEXT_ALIGN_CENTER);
gfx.FastText("HIGH SCORE", x +(w/4), y+(h/2))
gfx.FastText("IR RECORD", x + (3/4 * w), y + (h/2))
gfx.BeginPath()
gfx.Rect(x+xOffset,y+h/2,w/2-(xOffset*2),h/2)
gfx.FillColor(30,30,30,10)
gfx.StrokeColor(0,128,255)
gfx.StrokeWidth(1)
gfx.Fill()
gfx.Stroke()
gfx.BeginPath()
gfx.Rect(x + xOffset + w/2,y+h/2,w/2-(xOffset*2),h/2)
gfx.FillColor(30,30,30,10)
gfx.StrokeColor(0,128,255)
gfx.StrokeWidth(1)
gfx.Fill()
gfx.Stroke()
if difficulty.scores[1] ~= nil then
local highScore = difficulty.scores[1]
scoreLabel = gfx.CreateLabel(string.format("%08d",highScore.score), 40, 0)
for i,v in ipairs(grades) do
if v.max > highScore.score then
gfx.BeginPath()
iw,ih = gfx.ImageSize(v.image)
iarr = ih / iw
oldheight = h/2 - 10
newheight = iarr * (h/2-10)
centreoffset = (oldheight - newheight)/2 + 3 -- +3 is stupid but ehhh
gfx.ImageRect(x+xOffset, y+h/2 + centreoffset, oldheight, newheight, v.image, 1, 0) --this is nasty but it works for me
break
end
end
if difficulty.topBadge ~= 0 then
gfx.BeginPath()
gfx.ImageRect(x+xOffset+w/2-h/2, y+h/2 +5, (h/2-10), h/2-10, badges[difficulty.topBadge], 1, 0)
end
gfx.FillColor(255,255,255)
gfx.FontSize(40);
gfx.TextAlign(gfx.TEXT_ALIGN_MIDDLE + gfx.TEXT_ALIGN_CENTER);
gfx.DrawLabel(scoreLabel, x+(w/4),y+(h/4)*3,w/2)
end
irRecord = get_record(difficulty.hash)
if not irRecord.good then
recordLabel = gfx.CreateLabel(irRecord.reason, 40, 0)
gfx.FillColor(255, 255, 255)
gfx.FontSize(40)
gfx.TextAlign(gfx.TEXT_ALIGN_MIDDLE + gfx.TEXT_ALIGN_CENTER);
gfx.DrawLabel(recordLabel, x+(w * 3/4),y+(h/4)*3,w/2)
elseif irRecord.record == nil then --record not set, but can be tracked
recordLabel = gfx.CreateLabel(string.format("%08d", 0), 40, 0)
gfx.FillColor(170, 170, 170)
gfx.FontSize(40)
gfx.TextAlign(gfx.TEXT_ALIGN_MIDDLE + gfx.TEXT_ALIGN_CENTER);
gfx.DrawLabel(recordLabel, x+(w * 3/4),y+(h/4)*3,w/2)
else
recordScoreLabel = gfx.CreateLabel(string.format("%08d", irRecord.record.score), 26, 0)
recordPlayerLabel = gfx.CreateLabel(irRecord.record.username, 26, 0)
if irRecord.record.lamp ~= 0 then
gfx.BeginPath()
gfx.ImageRect(x+xOffset+w-h/2, y+h/2 +5, (h/2-10), h/2-10, badges[irRecord.record.lamp], 1, 0)
end
for i,v in ipairs(grades) do
if v.max > irRecord.record.score then
gfx.BeginPath()
iw,ih = gfx.ImageSize(v.image)
iarr = ih / iw
oldheight = h/2 - 10
newheight = iarr * (h/2-10)
centreoffset = (oldheight - newheight)/2 + 3 -- +3 is stupid but ehhh
gfx.ImageRect(x+xOffset+w/2, y+h/2 + centreoffset, oldheight, newheight, v.image, 1, 0) --this is nasty but it works for me
break
end
end
gfx.FillColor(255, 255, 255)
gfx.FontSize(40)
gfx.TextAlign(gfx.TEXT_ALIGN_MIDDLE + gfx.TEXT_ALIGN_CENTER);
gfx.DrawLabel(recordPlayerLabel, x+(w * 3/4),y+(h/4)*2.55,w/2)
gfx.DrawLabel(recordScoreLabel, x+(w * 3/4),y+(h/4)*3.45,w/2)
end
end
draw_scores = function(difficulty, x, y, w, h)
if IRData.Active then return draw_scores_ir(difficulty, x, y, w, h) end
-- draw the top score for this difficulty
local xOffset = 5
local height = h/3 - 10
local ySpacing = h/3
local yOffset = h/3
gfx.FontSize(30);
gfx.TextAlign(gfx.TEXT_ALIGN_BOTTOM + gfx.TEXT_ALIGN_CENTER);
gfx.BeginPath()
gfx.FillColor(30,30,30,10)
gfx.StrokeColor(0,128,255)
gfx.StrokeWidth(1)
gfx.Fill()
gfx.Stroke()
if difficulty.scores[1] ~= nil then
local highScore = difficulty.scores[1]
scoreLabel = gfx.CreateLabel(string.format("%08d",highScore.score), 40, 0)
for i,v in ipairs(grades) do
if v.max > highScore.score then
gfx.BeginPath()
iw,ih = gfx.ImageSize(v.image)
iar = iw / ih;
--gfx.ImageRect(x+xOffset,y+h/2 +5, iar * (h/2-10),h/2-10, v.image, 1, 0)
break
end
end
if difficulty.topBadge ~= 0 then
gfx.BeginPath()
--gfx.ImageRect(x+xOffset+w-h/2, y+h/2 +5, (h/2-10), h/2-10, badges[difficulty.topBadge], 1, 0)
end
gfx.FillColor(255,255,255)
gfx.FontSize(40);
gfx.TextAlign(gfx.TEXT_ALIGN_BOTTOM + gfx.TEXT_ALIGN_LEFT);
gfx.DrawLabel(scoreLabel, x/11,y/1.48,w*2)
end
end
function deep_to_string(t)
local tType = type(t);
if (tType ~= "table") then
return tostring(t);
end
local result = "{";
for k, v in next, t do
local kType = type(k);
local vType = type(v);
local keyString = deep_to_string(k);
local valueString = deep_to_string(v);
if (#result > 1) then
result = result .. ";";
end
result = result .. keyString .. "=" .. valueString;
end
return result .. "}";
end
draw_song = function(song, x, y, w, h, selected)
-- game.Log("draw_song", game.LOGGER_ERROR);
local diffIndex = math.min(selectedDiff, #song.difficulties)
local difficulty = song.difficulties[diffIndex]
local clearLampR = 255
local clearLampG = 255
local clearLampB = 255
local clearLampA = 100
if difficulty ~= nil then
-- game.Log(deep_to_string(difficulty), game.LOGGER_ERROR);
if difficulty.scores[1] ~= nil then
if difficulty.topBadge == 1 then -- fail/played
clearLampR = 255
clearLampG = 25
clearLampB = 25
clearLampA = 200
end
if difficulty.topBadge == 2 then -- clear
clearLampR = 25
clearLampG = 255
clearLampB = 25
clearLampA = 200
end
if difficulty.topBadge == 3 then -- hard clear
clearLampR = 255
clearLampG = 25
clearLampB = 255
clearLampA = 200
end
if difficulty.topBadge == 4 then -- full combo
clearLampR = 255
clearLampG = 100
clearLampB = 25
clearLampA = 200
end
if difficulty.topBadge == 5 then -- perfect
clearLampR = 255
clearLampG = 255
clearLampB = 25
clearLampA = 200
end
end
end
-- game.Log(" past difficulty check", game.LOGGER_ERROR);
check_or_create_cache(song)
gfx.BeginPath()
gfx.Rect(x+1,y+1, w-2, h-2)
gfx.FillColor(220,220,220)
gfx.StrokeColor(0,8,0)
gfx.StrokeWidth(2)
gfx.Fill()
gfx.Stroke()
gfx.FillColor(255,255,255)
if songCache[song.id]["jacket"][diffIndex] then
gfx.BeginPath()
gfx.ImageRect(x+2, y+2, h-4, h-4, songCache[song.id]["jacket"][diffIndex], 1, 0)
end
-- Song title
gfx.BeginPath()
gfx.Rect(x+1, y + h - h/4 - 1, w-2, h/4)
gfx.FillColor(0,0,0,200)
gfx.Fill()
gfx.BeginPath()
gfx.FillColor(255,255,255)
gfx.TextAlign(gfx.TEXT_ALIGN_BOTTOM + gfx.TEXT_ALIGN_LEFT)
gfx.DrawLabel(songCache[song.id]["title"], (x)+h/2 + 4, y + h - 7, -1)
--gfx.DrawLabel(songCache[song.id]["artist"], x+10, y + 50, w-10)
-- Song difficulty
gfx.BeginPath()
gfx.Rect(x - 1, y + h-h/2 - 4, h/2, h/2)
gfx.FillColor(0,0,0,200)
gfx.Fill()
gfx.FillColor(255,255,255)
gfx.LoadSkinFont("commext.ttf")
gfx.FontSize(28)
gfx.TextAlign(gfx.TEXT_ALIGN_CENTER + gfx.TEXT_ALIGN_BOTTOM)
if (song.difficulties[selectedDiff] ~= nil) then
gfx.FastText(song.difficulties[selectedDiff].level, x + h/4, y + h - 10)
else
--gfx.FastText(song.difficulties[selectedDiff - 1].level, x + h/4, y + h - 10)
end
-- CLEAN THIS SHIT UP
local diff_long = ""
local diff_short = ""
if (song.difficulties[selectedDiff] ~= nil) then
if (song.difficulties[selectedDiff].difficulty == 0) then
diff_long = "NOVICE"
diff_short = "NOV"
elseif (song.difficulties[selectedDiff].difficulty == 1) then
diff_long = "ADVANCED"
diff_short = "ADV"
elseif (song.difficulties[selectedDiff].difficulty == 2) then
diff_long = "EXHAUST"
diff_short = "EXH"
elseif (song.difficulties[selectedDiff].difficulty == 3) then
diff_long = "MAXIMUM"
diff_short = "MXM"
else
diff_long = "UNKNOWN"
diff_short = "???"
end
end
gfx.FontSize(8)
gfx.LoadSkinFont("dfmarugoth.ttf")
gfx.FastText(diff_long, x + h/4, y + h - 7)
if (false) then
local seldiff = nil
if song.difficulties[selectedDiff] ~= nil then
seldiff = selectedDiff
else
seldiff = selectedDiff
end
if song.difficulties[seldiff].topBadge ~= 0 then
if song.difficulties[seldiff].scores[1] ~= nil then
local highScore = song.difficulties[seldiff].scores[1]
for i,v in ipairs(grades) do
if v.max > highScore.score then
gfx.BeginPath()
iw,ih = gfx.ImageSize(v.image)
iar = iw / ih;
gfx.ImageRect(x + w/1.45, y + h/8 + 2, (h/1.5-14), h/1.5-14, v.image, 1, 0)
break
end
end
end
gfx.BeginPath()
gfx.ImageRect(x + w/2, y + h/8, (h/1.5-10), h/1.5-10, badges[song.difficulties[seldiff].topBadge], 1, 0)
end
end
end
draw_diff_icon = function(diff, x, y, w, h, selected)
local difficultyIndex = diff.difficulty;
local image = difficultyNameOverlays[difficultyIndex];
local imgx, imgy = gfx.ImageSize(image);
local aspect = imgx / imgy;
h = h * 98 / 112;
local wa = h * aspect;
gfx.BeginPath();
gfx.ImageRect(x - wa / 2, y - h / 2, wa, h, image, 1, 0);
local level = diff.level;
local firstDigit = difficultyNumbers[math.max(0, math.floor(level / 10))];
local secondDigit = difficultyNumbers[level % 10];
h = h * 0.475;
imgx, imgy = gfx.ImageSize(firstDigit);
aspect = imgx / imgy;
wa = h * aspect;
gfx.BeginPath();
gfx.ImageRect(x - wa, y - h / 2, wa, h, firstDigit, 1, 0);
gfx.BeginPath();
gfx.ImageRect(x, y - h / 2, wa, h, secondDigit, 1, 0);
end
draw_cursor = function(x, y, h)
local imgx, imgy = gfx.ImageSize(difficultyLevelCursor);
local aspect = imgx / imgy;
local w = h * aspect;
gfx.BeginPath();
gfx.ImageRect(x - w / 2, y - h / 2, w, h, difficultyLevelCursor, 1, 0);
end
draw_diffs = function(diffs, x, y, w, h)
local diffWidth = w / 5
local diffHeight = diffWidth
for i = 1, #diffs do
local diff = diffs[i]
local xPos = x + w * (i - 0.5) / 4;
local yPos = y + h / 2;
if (i == selectedDiff) then
draw_cursor(xPos, yPos, diffHeight);
end
draw_diff_icon(diff, xPos, yPos, diffWidth, diffHeight, i == selectedDiff);
end
end
draw_selected = function(song, x, y, w, h)
check_or_create_cache(song)
-- set up padding and margins
local xPadding = math.floor(w/16)
local yPadding = math.floor(h/32)
local xMargin = math.floor(w/16)
local yMargin = math.floor(h/32)
local width = (w-(xMargin*2))
local height = (h-(yMargin*2))
local xpos = x+xMargin
local ypos = y+yMargin
if aspectRatio == "PortraitWidescreen" then
xPadding = math.floor(w/32)
yPadding = math.floor(h/32)
xMargin = math.floor(w/34)
yMargin = math.floor(h/32)
width = ((w/2)-(xMargin))
height = (h-(yMargin*2))
xpos = x+xMargin/2
ypos = y+yMargin
end
--Border
local diff = song.difficulties[selectedDiff]
gfx.BeginPath()
--gfx.RoundedRectVarying(xpos,ypos,width,height,yPadding,yPadding,yPadding,yPadding)
gfx.FillColor(30,30,30,100)
gfx.StrokeColor(0,128,255)
gfx.StrokeWidth(1)
gfx.Fill()
gfx.Stroke()
-- jacket should take up 1/3 of height, always be square, and be centered
local imageSize = math.floor(height/3)
local imageXPos = ((width/2) - (imageSize/2)) + x+xMargin
if aspectRatio == "PortraitWidescreen" then
--Unless its portrait widesreen..
imageSize = math.floor((height/8)*1.58)
imageXPos = (x+w)/16+(xMargin*0.8)
end
if not songCache[song.id][selectedDiff] or songCache[song.id][selectedDiff] == jacketFallback then
songCache[song.id][selectedDiff] = gfx.LoadImageJob(diff.jacketPath, jacketFallback, 200,200)
end
if songCache[song.id][selectedDiff] then
gfx.BeginPath()
gfx.ImageRect(imageXPos, y+yMargin*4.45+yPadding, imageSize, imageSize, songCache[song.id][selectedDiff], 1, 0)
end
-- difficulty should take up 1/6 of height, full width, and be centered
gfx.LoadSkinFont("commext.ttf")
if aspectRatio == "PortraitWidescreen" then
--difficulty wheel should be right below the jacketImage, and the same width as
--the jacketImage
local diffPanelWidth = w * 0.4275;
local diffPanelHeight = diffPanelWidth / 4;
draw_diffs(song.difficulties, (w / 2 - diffPanelWidth) / 2, y + 0.5687583444592 * h - diffPanelHeight / 2, diffPanelWidth, diffPanelHeight)
else
-- difficulty should take up 1/6 of height, full width, and be centered
draw_diffs(song.difficulties,(w/2)-(imageSize/2),(ypos+yPadding+imageSize),imageSize,math.floor(height/6))
end
-- effector / bpm should take up 1/3 of height, full width
gfx.LoadSkinFont("dfmarugoth.ttf")
if aspectRatio == "PortraitWidescreen" then
gfx.FontSize(40)
gfx.TextAlign(gfx.TEXT_ALIGN_TOP + gfx.TEXT_ALIGN_LEFT)
gfx.DrawLabel(songCache[song.id]["title"], xpos+xPadding/2, y+yMargin*15+yPadding, width)
gfx.FontSize(40)
gfx.DrawLabel(songCache[song.id]["artist"], xpos+xPadding/2, y+yMargin*15.8+yPadding, width)
gfx.FontSize(10)
gfx.DrawLabel(songCache[song.id]["bpm"], xpos+xPadding*2, y+yMargin*14.42+yPadding, width-imageSize)
gfx.FastText(string.format("%s", diff.effector), xpos+xPadding*7.5, y+yMargin*18.87+yPadding)
else
gfx.FontSize(40)
gfx.TextAlign(gfx.TEXT_ALIGN_TOP + gfx.TEXT_ALIGN_LEFT)
gfx.DrawLabel(songCache[song.id]["title"], xpos+10, (height/10)*6, width-20)
gfx.FontSize(30)
gfx.DrawLabel(songCache[song.id]["artist"], xpos+10, (height/10)*6 + 45, width-20)
gfx.FillColor(255,255,255)
gfx.FontSize(20)
gfx.DrawLabel(songCache[song.id]["bpm"], xpos+10, (height/10)*6 + 85)
gfx.FastText(string.format("%s", diff.effector),xpos+10, (height/10)*6 + 115)
end
if aspectRatio == "PortraitWidescreen" then
draw_scores(diff, xpos+xPadding+imageSize+3, (height/3)*2, width-imageSize-20, (height/3)-yPadding)
else
draw_scores(diff, xpos, (height/6)*5, width, (height/6))
end
gfx.ForceRender()
end
draw_songwheel = function(x,y,w,h)
local offsetX = fifthX/2
local width = math.floor((w/5)*4)
if aspectRatio == "landscapeWidescreen" then
wheelSize = 12
offsetX = 80
elseif aspectRatio == "landscapeStandard" then
wheelSize = 10
offsetX = 40
elseif aspectRatio == "PortraitWidescreen" then
wheelSize = 20
offsetX = 20
width = w/2
end
local height = math.floor((h/wheelSize)*1.75)
for i = math.max(selectedIndex - wheelSize/2, 1), math.max(selectedIndex - 1,0) do
local song = songwheel.songs[i]
local xpos = x + width
local offsetY = (selectedIndex - i + ioffset/2) * ( height * 1.0)
local ypos = y+((h/2 - height/2) - offsetY)
draw_song(song, xpos, ypos, width, height)
end
--after selected
for i = math.min(selectedIndex + wheelSize/2, #songwheel.songs), selectedIndex + 1,-1 do
local song = songwheel.songs[i]
local xpos = x + width
local offsetY = (selectedIndex - i + ioffset/2) * ( height * 1.0)
local ypos = y+((h/2 - height/2) - (selectedIndex - i) - offsetY)
local alpha = 255 - (selectedIndex - i + ioffset) * 31
draw_song(song, xpos, ypos, width, height)
end
-- draw selected
local xpos = x + width
local offsetY = (ioffset/2) * ( height - (wheelSize/2*((1)*aspectFloat)))
local ypos = y+((h/2 - height/2) - (ioffset) - offsetY)
draw_song(songwheel.songs[selectedIndex], xpos, ypos, width, height, true)
-- cursor
gfx.BeginPath()
local ypos = y+((h/2 - height/2))
gfx.Rect(xpos, ypos, width, height)
gfx.FillColor(0,0,0,0)
gfx.StrokeColor(255,128,0)
gfx.StrokeWidth(3)
gfx.Fill()
gfx.Stroke()
return songwheel.songs[selectedIndex]
end
draw_legend_pane = function(x,y,w,h,obj)
local xpos = x+5
local ypos = y
local imageSize = h
gfx.BeginPath()
gfx.TextAlign(gfx.TEXT_ALIGN_MIDDLE + gfx.TEXT_ALIGN_LEFT)
gfx.ImageRect(x, y, imageSize, imageSize, obj.image, 1, 0)
xpos = xpos + imageSize + 5
gfx.FontSize(16);
if h < (w-(10+imageSize))/2 then
gfx.DrawLabel(obj.labelSingleLine, xpos, y+(h/2), w-(10+imageSize))
else
gfx.DrawLabel(obj.labelMultiLine, xpos, y+(h/2), w-(10+imageSize))
end
gfx.ForceRender()
end
draw_legend = function(x,y,w,h)
gfx.TextAlign(gfx.TEXT_ALIGN_MIDDLE + gfx.TEXT_ALIGN_LEFT);
gfx.BeginPath()
gfx.FillColor(0,0,0,170)
gfx.Rect(x,y,w,h)
gfx.Fill()
local xpos = 10;
local legendWidth = math.floor((w-20)/#legendTable)
for i,v in ipairs(legendTable) do
local xOffset = draw_legend_pane(xpos+(legendWidth*(i-1)), y+5,legendWidth,h-10,legendTable[i])
end
end
draw_search = function(x,y,w,h)
soffset = soffset + (searchIndex) - (songwheel.searchInputActive and 0 or 1)
if searchIndex ~= (songwheel.searchInputActive and 0 or 1) then
game.PlaySample("woosh")
end
searchIndex = songwheel.searchInputActive and 0 or 1
gfx.BeginPath()
local bgfade = 1 - (searchIndex + soffset)
--if not songwheel.searchInputActive then bgfade = soffset end
gfx.FillColor(0,0,0,math.floor(200 * bgfade))
gfx.Rect(0,0,resx,resy)
gfx.Fill()
gfx.ForceRender()
local xpos = x + (searchIndex + soffset)*w
gfx.UpdateLabel(searchText ,string.format("Search: %s",songwheel.searchText), 30, 0)
gfx.BeginPath()
gfx.RoundedRect(xpos,y,w,h,h/2)
gfx.FillColor(30,30,30)
gfx.StrokeColor(0,128,255)
gfx.StrokeWidth(1)
gfx.Fill()
gfx.Stroke()
gfx.BeginPath();
gfx.LoadSkinFont("NotoSans-Regular.ttf");
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_MIDDLE);
gfx.DrawLabel(searchText, xpos+10,y+(h/2), w-20)
end
render = function(deltaTime)
gfx.ResetTransform()
timer = (timer + deltaTime)
timer = timer % 2
resx,resy = game.GetResolution();
-- game.Log("res :: " .. resx .. "," .. resy, game.LOGGER_ERROR);
adjustScreen(resx,resy);
gfx.BeginPath();
gfx.LoadSkinFont("dfmarugoth.ttf");
gfx.FontSize(40);
gfx.FillColor(255,255,255);
gfx.ImageRect(0, 0, resx, resy, datapanel, 1, 0);
if songwheel.songs[1] ~= nil then
--draw songwheel and get selected song
if aspectRatio == "PortraitWidescreen" then
local song = draw_songwheel(0,0,fullX,fullY)
--render selected song information
draw_selected(song, 0,0,fullX,resy)
else
local song = draw_songwheel(0,0,fullX,fullY)
--render selected song information
draw_selected(song, 0,0,fullX,resy)
end
end
--Draw Legend Information
-- if showGuide then
-- if aspectRatio == "PortraitWidescreen" then
-- draw_legend(0,(fifthY/3)*14, fullX, (fifthY/3)*1)
-- else
-- draw_legend(0,(fifthY/2)*9, fullX, (fifthY/2))
-- end
-- end
gfx.BeginPath();
gfx.TextAlign(TEXT_ALIGN_CENTER + TEXT_ALIGN_MIDDLE);
gfx.ImageRect(0, 0, resx, resy, foreground, 1, 0);
--draw text search
if aspectRatio == "PortraitWidescreen" then
draw_search(fifthX*2,5,fifthX*3,fifthY/5)
else
draw_search(fifthX*2,5,fifthX*3,fifthY/3)
end
ioffset = ioffset * 0.9
doffset = doffset * 0.9
soffset = soffset * 0.8
if songwheel.searchStatus then
gfx.BeginPath()
gfx.FillColor(255,255,255)
gfx.FontSize(20);
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_TOP)
gfx.Text(songwheel.searchStatus, 3, 3)
end
if totalForce then
gfx.BeginPath()
gfx.FillColor(255,255,255)
gfx.FontSize(20);
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_BOTTOM)
local forceText = string.format("Force: %.2f", totalForce)
gfx.Text(forceText, 0, fullY)
end
gfx.LoadSkinFont("NotoSans-Regular.ttf");
gfx.ResetTransform()
gfx.ForceRender()
end
set_index = function(newIndex)
if newIndex ~= selectedIndex then
game.PlaySample("menu_click")
end
ioffset = ioffset + selectedIndex - newIndex
selectedIndex = newIndex
end;
set_diff = function(newDiff)
if newDiff ~= selectedDiff then
game.PlaySample("click-02")
end
doffset = doffset + selectedDiff - newDiff
selectedDiff = newDiff
end;
-- force calculation
--------------------
totalForce = nil
local badgeRates = {
0.5, -- Played
1.0, -- Cleared
1.02, -- Hard clear
1.04, -- UC
1.1 -- PUC
}
local gradeRates = {
{["min"] = 9900000, ["rate"] = 1.05}, -- S
{["min"] = 9800000, ["rate"] = 1.02}, -- AAA+
{["min"] = 9700000, ["rate"] = 1}, -- AAA
{["min"] = 9500000, ["rate"] = 0.97}, -- AA+
{["min"] = 9300000, ["rate"] = 0.94}, -- AA
{["min"] = 9000000, ["rate"] = 0.91}, -- A+
{["min"] = 8700000, ["rate"] = 0.88}, -- A
{["min"] = 7500000, ["rate"] = 0.85}, -- B
{["min"] = 6500000, ["rate"] = 0.82}, -- C
{["min"] = 0, ["rate"] = 0.8} -- D
}
calculate_force = function(diff)
if #diff.scores < 1 then
return 0
end
local score = diff.scores[1]
local badgeRate = badgeRates[diff.topBadge]
local gradeRate
for i, v in ipairs(gradeRates) do
if score.score >= v.min then
gradeRate = v.rate
break
end
end
return math.floor((diff.level * 2) * (score.score / 10000000) * gradeRate * badgeRate) / 100
end
songs_changed = function(withAll)
if not withAll then return end
recordCache = {}
local diffs = {}
for i = 1, #songwheel.allSongs do
local song = songwheel.allSongs[i]
for j = 1, #song.difficulties do
local diff = song.difficulties[j]
diff.force = calculate_force(diff)
table.insert(diffs, diff)
end
end
table.sort(diffs, function (l, r)
return l.force > r.force
end)
totalForce = 0
for i = 1, 50 do
if diffs[i] then
totalForce = totalForce + diffs[i].force
end
end
end

View File

@ -1,242 +1,243 @@
local Easing = require('common.easing');
local resx, resy = game.GetResolution()
local desw, desh = 1080, 1920
-- AUDIO
game.LoadSkinSample('sort_wheel/enter.wav');
game.LoadSkinSample('sort_wheel/leave.wav');
-- IMAGES
local panelBgImage = gfx.CreateSkinImage('song_select/sort_wheel/bg.png', 0)
local activeItemBgImage = gfx.CreateSkinImage(
'song_select/sort_wheel/active_bg.png', 0)
local titleTextImage =
gfx.CreateSkinImage('song_select/sort_wheel/title.png', 0)
local selection = 1;
local renderedButtonLabels = {}
local FONT_SIZE = 32;
local MARGIN = 16;
local SUB_FONT_SIZE = 26;
local SUB_MARGIN = 8;
local SORT_ORDER_LABEL_TEXTS = {
{
label = 'Title',
asc = '# to A to Z to かな to 漢字',
dsc = '漢字 to かな to Z to A to #'
}, {label = 'Score', asc = 'Worst to best', dsc = 'Best to worst'},
{label = 'Date', asc = 'Oldest to newest', dsc = 'Newest to oldest'},
{label = 'Badge', asc = 'None to D to S', dsc = 'S to D to None'}, {
label = 'Artist',
asc = '# to A to Z to かな to 漢字',
dsc = '漢字 to かな to Z to A to #'
}, {
label = 'Effector',
asc = '# to A to Z to かな to 漢字',
dsc = '漢字 to かな to Z to A to #'
}
}
local transitionEnterReverse = false;
local transitionEnterScale = 0;
local transitionEnterOffsetX = 0;
local previousActiveState = false;
-- Window variables
local resX, resY
-- Aspect Ratios
local landscapeWidescreenRatio = 16 / 9
local landscapeStandardRatio = 4 / 3
local portraitWidescreenRatio = 9 / 16 --+ 0.0035
-- Portrait sizes
local fullX, fullY
local resolutionChange = function(x, y)
resX = x
resY = y
fullX = portraitWidescreenRatio * y
fullY = y
game.Log('resX:' .. resX .. ' // resY:' .. resY .. ' // fullX:' .. fullX .. ' // fullY:' .. fullY, game.LOGGER_ERROR);
end
function tableContains(table, value)
for i, v in ipairs(table) do if v == value then return true end end
return false;
end
function drawButton(i, f, x, y)
local spaceAfter = (FONT_SIZE + MARGIN)
local sortOrder = 'asc';
if (string.find(f, 'v')) then sortOrder = 'dsc' end
local label = f:gsub(' ^', '')
label = label:gsub(' v', '')
if (string.find(sorts[selection], label) and sorts[selection] ~= f) then
-- If there is a button active with the same label, but different sort order, don't render this one
return 0;
else
-- If there is no active button with this label, if one with a label was already rendered, don't render this one
if (tableContains(renderedButtonLabels, label)) then return 0; end
table.insert(renderedButtonLabels, label);
end
if (i == selection) then
local ascLabelText = 'Ascending'
local dscLabelText = 'Descending'
for i, obj in ipairs(SORT_ORDER_LABEL_TEXTS) do
if (obj.label == label) then
ascLabelText = obj.asc;
dscLabelText = obj.dsc;
end
end
gfx.BeginPath()
gfx.ImageRect(x - 182, y - 38, 365, 82, activeItemBgImage, 1, 0)
gfx.BeginPath()
if sortOrder == 'asc' then
gfx.ImageRect(x - 150, y + FONT_SIZE + SUB_MARGIN * 2 - 31, 300, 67,
activeItemBgImage, 1, 0)
elseif sortOrder == 'dsc' then
gfx.ImageRect(x - 150, y + FONT_SIZE + SUB_MARGIN * 2 +
SUB_FONT_SIZE + SUB_MARGIN - 31, 300, 67,
activeItemBgImage, 1, 0)
end
gfx.Save()
gfx.FontSize(SUB_FONT_SIZE)
gfx.Text(ascLabelText, x, y + FONT_SIZE + SUB_MARGIN * 2);
gfx.Text(dscLabelText, x,
y + FONT_SIZE + SUB_MARGIN * 2 + SUB_FONT_SIZE + SUB_MARGIN);
gfx.Restore()
spaceAfter = spaceAfter + SUB_FONT_SIZE * 2 + SUB_MARGIN * 4;
end
gfx.BeginPath();
gfx.Text(label, x, y);
return spaceAfter;
end
function tickTransitions(deltaTime)
-- ENTRY TRANSITION
if transitionEnterReverse then
if transitionEnterScale > 0 then
transitionEnterScale = transitionEnterScale - deltaTime / 0.25 -- transition should last for that time in seconds
else
transitionEnterScale = 0
end
else
if transitionEnterScale < 1 then
transitionEnterScale = transitionEnterScale + deltaTime / 0.25 -- transition should last for that time in seconds
else
transitionEnterScale = 1
end
end
transitionEnterOffsetX = Easing.inOutQuad(1 - transitionEnterScale) * 416
end
local drawSortWheel = function (x,y,w,h, deltaTime)
gfx.Scissor(x,y,w,h);
gfx.Translate(x,y);
gfx.Scale(w/1080, h/1920);
gfx.Scissor(0,0,1080,1920);
-- Draw the dark overlay above song wheel
gfx.BeginPath();
gfx.FillColor(0, 0, 0, math.floor(transitionEnterScale * 192));
gfx.Rect(0, 0, 1080, 1920);
gfx.Fill();
-- Draw the panel background
gfx.BeginPath()
gfx.ImageRect(desw - 416 + transitionEnterOffsetX, 0, 416, desh,
panelBgImage, 1, 0)
gfx.LoadSkinFont("Digital-Serial-Bold.ttf");
gfx.FontSize(FONT_SIZE);
gfx.FillColor(255, 255, 255, 255);
gfx.TextAlign(gfx.TEXT_ALIGN_CENTER + gfx.TEXT_ALIGN_MIDDLE);
gfx.GlobalAlpha(transitionEnterScale)
-- Starting position of the first sort option
local x = 889 + transitionEnterOffsetX;
local y = desh / 2 - -- Center point
(#sorts / 2 / 2) * (FONT_SIZE + MARGIN) - -- Space taken up by half the sort options (we remove the duplicate one)
((SUB_FONT_SIZE * 2 + SUB_MARGIN * 4) / 2); -- Space for taken by order options
-- Draw the title image
gfx.BeginPath()
gfx.ImageRect(x - 72, y - 27, 144, 54, titleTextImage, 1, 0)
y = y + (54 + MARGIN)
-- Draw all the sorting options
for i, f in ipairs(sorts) do
local spaceAfter = drawButton(i, f, x, y);
y = y + spaceAfter;
end
end
function setSkinSetting()
for i, f in ipairs(sorts) do
if i == selection then
local label = f:gsub(' ^', '')
label = label:gsub(' v', '')
game.SetSkinSetting('_songWheelActiveSortOptionLabel', label);
end
end
end
function render(deltaTime, shown)
gfx.Save()
gfx.ResetTransform()
renderedButtonLabels = {};
if (shown ~= previousActiveState) then
if (shown) then
game.PlaySample('sort_wheel/enter.wav');
else
game.PlaySample('sort_wheel/leave.wav');
end
previousActiveState = shown;
end
-- detect resolution change
local resx, resy = game.GetResolution();
if resx ~= resX or resy ~= resY then
resolutionChange(resx, resy)
end
gfx.GlobalAlpha(1)
if not shown then
transitionEnterReverse = true
if (transitionEnterScale > 0) then drawSortWheel((resX - fullX) / 2, 0, fullX, fullY, deltaTime) end
else
transitionEnterReverse = false
drawSortWheel((resX - fullX) / 2, 0, fullX, fullY, deltaTime)
end
tickTransitions(deltaTime)
setSkinSetting();
gfx.Restore()
end
function set_selection(index) selection = index end
require('common')
local Easing = require('common.easing');
local resx, resy = game.GetResolution()
local desw, desh = 1080, 1920
-- AUDIO
game.LoadSkinSample('sort_wheel/enter.wav');
game.LoadSkinSample('sort_wheel/leave.wav');
-- IMAGES
local panelBgImage = gfx.CreateSkinImage('song_select/sort_wheel/bg.png', 0)
local activeItemBgImage = gfx.CreateSkinImage(
'song_select/sort_wheel/active_bg.png', 0)
local titleTextImage =
gfx.CreateSkinImage('song_select/sort_wheel/title.png', 0)
local selection = 1;
local renderedButtonLabels = {}
local FONT_SIZE = 32;
local MARGIN = 16;
local SUB_FONT_SIZE = 26;
local SUB_MARGIN = 8;
local SORT_ORDER_LABEL_TEXTS = {
{
label = 'Title',
asc = '# to A to Z to かな to 漢字',
dsc = '漢字 to かな to Z to A to #'
}, {label = 'Score', asc = 'Worst to best', dsc = 'Best to worst'},
{label = 'Date', asc = 'Oldest to newest', dsc = 'Newest to oldest'},
{label = 'Badge', asc = 'None to D to S', dsc = 'S to D to None'}, {
label = 'Artist',
asc = '# to A to Z to かな to 漢字',
dsc = '漢字 to かな to Z to A to #'
}, {
label = 'Effector',
asc = '# to A to Z to かな to 漢字',
dsc = '漢字 to かな to Z to A to #'
}
}
local transitionEnterReverse = false;
local transitionEnterScale = 0;
local transitionEnterOffsetX = 0;
local previousActiveState = false;
-- Window variables
local resX, resY
-- Aspect Ratios
local landscapeWidescreenRatio = 16 / 9
local landscapeStandardRatio = 4 / 3
local portraitWidescreenRatio = 9 / 16 --+ 0.0035
-- Portrait sizes
local fullX, fullY
local resolutionChange = function(x, y)
resX = x
resY = y
fullX = portraitWidescreenRatio * y
fullY = y
game.Log('resX:' .. resX .. ' // resY:' .. resY .. ' // fullX:' .. fullX .. ' // fullY:' .. fullY, game.LOGGER_ERROR);
end
function tableContains(table, value)
for i, v in ipairs(table) do if v == value then return true end end
return false;
end
function drawButton(i, f, x, y)
local spaceAfter = (FONT_SIZE + MARGIN)
local sortOrder = 'asc';
if (string.find(f, 'v')) then sortOrder = 'dsc' end
local label = f:gsub(' ^', '')
label = label:gsub(' v', '')
if (string.find(sorts[selection], label) and sorts[selection] ~= f) then
-- If there is a button active with the same label, but different sort order, don't render this one
return 0;
else
-- If there is no active button with this label, if one with a label was already rendered, don't render this one
if (tableContains(renderedButtonLabels, label)) then return 0; end
table.insert(renderedButtonLabels, label);
end
if (i == selection) then
local ascLabelText = 'Ascending'
local dscLabelText = 'Descending'
for i, obj in ipairs(SORT_ORDER_LABEL_TEXTS) do
if (obj.label == label) then
ascLabelText = obj.asc;
dscLabelText = obj.dsc;
end
end
gfx.BeginPath()
gfx.ImageRect(x - 182, y - 38, 365, 82, activeItemBgImage, 1, 0)
gfx.BeginPath()
if sortOrder == 'asc' then
gfx.ImageRect(x - 150, y + FONT_SIZE + SUB_MARGIN * 2 - 31, 300, 67,
activeItemBgImage, 1, 0)
elseif sortOrder == 'dsc' then
gfx.ImageRect(x - 150, y + FONT_SIZE + SUB_MARGIN * 2 +
SUB_FONT_SIZE + SUB_MARGIN - 31, 300, 67,
activeItemBgImage, 1, 0)
end
gfx.Save()
gfx.FontSize(SUB_FONT_SIZE)
gfx.Text(ascLabelText, x, y + FONT_SIZE + SUB_MARGIN * 2);
gfx.Text(dscLabelText, x,
y + FONT_SIZE + SUB_MARGIN * 2 + SUB_FONT_SIZE + SUB_MARGIN);
gfx.Restore()
spaceAfter = spaceAfter + SUB_FONT_SIZE * 2 + SUB_MARGIN * 4;
end
gfx.BeginPath();
gfx.Text(label, x, y);
return spaceAfter;
end
function tickTransitions(deltaTime)
-- ENTRY TRANSITION
if transitionEnterReverse then
if transitionEnterScale > 0 then
transitionEnterScale = transitionEnterScale - deltaTime / 0.25 -- transition should last for that time in seconds
else
transitionEnterScale = 0
end
else
if transitionEnterScale < 1 then
transitionEnterScale = transitionEnterScale + deltaTime / 0.25 -- transition should last for that time in seconds
else
transitionEnterScale = 1
end
end
transitionEnterOffsetX = Easing.inOutQuad(1 - transitionEnterScale) * 416
end
local drawSortWheel = function (x,y,w,h, deltaTime)
gfx.Scissor(x,y,w,h);
gfx.Translate(x,y);
gfx.Scale(w/1080, h/1920);
gfx.Scissor(0,0,1080,1920);
-- Draw the dark overlay above song wheel
gfx.BeginPath();
gfx.FillColor(0, 0, 0, math.floor(transitionEnterScale * 192));
gfx.Rect(0, 0, 1080, 1920);
gfx.Fill();
-- Draw the panel background
gfx.BeginPath()
gfx.ImageRect(desw - 416 + transitionEnterOffsetX, 0, 416, desh,
panelBgImage, 1, 0)
gfx.LoadSkinFont("Digital-Serial-Bold.ttf");
gfx.FontSize(FONT_SIZE);
gfx.FillColor(255, 255, 255, 255);
gfx.TextAlign(gfx.TEXT_ALIGN_CENTER + gfx.TEXT_ALIGN_MIDDLE);
gfx.GlobalAlpha(transitionEnterScale)
-- Starting position of the first sort option
local x = 889 + transitionEnterOffsetX;
local y = desh / 2 - -- Center point
(#sorts / 2 / 2) * (FONT_SIZE + MARGIN) - -- Space taken up by half the sort options (we remove the duplicate one)
((SUB_FONT_SIZE * 2 + SUB_MARGIN * 4) / 2); -- Space for taken by order options
-- Draw the title image
gfx.BeginPath()
gfx.ImageRect(x - 72, y - 27, 144, 54, titleTextImage, 1, 0)
y = y + (54 + MARGIN)
-- Draw all the sorting options
for i, f in ipairs(sorts) do
local spaceAfter = drawButton(i, f, x, y);
y = y + spaceAfter;
end
end
function setSkinSetting()
for i, f in ipairs(sorts) do
if i == selection then
local label = f:gsub(' ^', '')
label = label:gsub(' v', '')
game.SetSkinSetting('_songWheelActiveSortOptionLabel', label);
end
end
end
function render(deltaTime, shown)
gfx.Save()
gfx.ResetTransform()
renderedButtonLabels = {};
if (shown ~= previousActiveState) then
if (shown) then
game.PlaySample('sort_wheel/enter.wav');
else
game.PlaySample('sort_wheel/leave.wav');
end
previousActiveState = shown;
end
-- detect resolution change
local resx, resy = game.GetResolution();
if resx ~= resX or resy ~= resY then
resolutionChange(resx, resy)
end
gfx.GlobalAlpha(1)
if not shown then
transitionEnterReverse = true
if (transitionEnterScale > 0) then drawSortWheel((resX - fullX) / 2, 0, fullX, fullY, deltaTime) end
else
transitionEnterReverse = false
drawSortWheel((resX - fullX) / 2, 0, fullX, fullY, deltaTime)
end
tickTransitions(deltaTime)
setSkinSetting();
gfx.Restore()
end
function set_selection(index) selection = index end

View File

@ -1,215 +1,214 @@
local common = require('common.util');
local Sound = require("common.sound")
local Numbers = require('components.numbers')
game.LoadSkinSample('song_transition_screen/transition_enter.wav');
local backgroundImage = gfx.CreateSkinImage("bg_pattern.png", gfx.IMAGE_REPEATX | gfx.IMAGE_REPEATY)
local bgImage = gfx.CreateSkinImage("songtransition/bg.png", 0)
local glowOverlayImage = gfx.CreateSkinImage("songtransition/glowy.png", 0)
local frameOverlayImage = gfx.CreateSkinImage("songtransition/frames.png", 0)
local albumBgImage = gfx.CreateSkinImage("songtransition/album_crop.png", 0)
local infoOverlayPanel = gfx.CreateSkinImage("songtransition/info_panels_crop.png", 0)
local linkedHexagonsImage = gfx.CreateSkinImage("songtransition/linked_hexagons_crop.png", 0)
local hexagonImages = {
gfx.CreateSkinImage("songtransition/hex1.png", 0),
gfx.CreateSkinImage("songtransition/hex2.png", 0)
}
local difficultyNumbers;
local difficultyLabelImages = {
gfx.CreateSkinImage("songtransition/difficulty_labels/nov.png", 0),
gfx.CreateSkinImage("songtransition/difficulty_labels/adv.png", 0),
gfx.CreateSkinImage("songtransition/difficulty_labels/exh.png", 0),
gfx.CreateSkinImage("songtransition/difficulty_labels/mxm.png", 0),
gfx.CreateSkinImage("songtransition/difficulty_labels/inf.png", 0),
gfx.CreateSkinImage("songtransition/difficulty_labels/grv.png", 0),
gfx.CreateSkinImage("songtransition/difficulty_labels/hvn.png", 0),
gfx.CreateSkinImage("songtransition/difficulty_labels/vvd.png", 0),
}
local timer = 0
local transitionProgress = 0;
local outProgress = 0
local flickerTime = 0.050 --seconds (50ms)
-- Window variables
local resX, resY = game.GetResolution()
-- Aspect Ratios
local landscapeWidescreenRatio = 16 / 9
local landscapeStandardRatio = 4 / 3
local portraitWidescreenRatio = 9 / 16
-- Portrait sizes
local fullX, fullY
local desw = 1080
local desh = 1920
local noJacket = gfx.CreateSkinImage("song_select/loading.png", 0)
local wasEnterSfxPlayed = false;
function resetLayoutInformation()
resx, resy = game.GetResolution()
scale = resx / desw
end
function render(deltaTime)
if not wasEnterSfxPlayed then
Sound.stopMusic();
game.PlaySample('song_transition_screen/transition_enter.wav');
wasEnterSfxPlayed = true;
end
if not difficultyNumbers then
difficultyNumbers = Numbers.load_number_image('diff_num')
end
local x_offset = (resX - fullX) / 2
local y_offset = 0
gfx.BeginPath()
local bgImageWidth, bgImageHeight = gfx.ImageSize(backgroundImage)
gfx.Rect(0, 0, resX, resY)
gfx.FillPaint(gfx.ImagePattern(0, 0, bgImageWidth, bgImageHeight, 0, backgroundImage, 0.2))
gfx.Fill()
gfx.Translate(x_offset, y_offset);
gfx.Scale(fullX / 1080, fullY / 1920);
gfx.Scissor(0, 0, 1080, 1920);
render_screen();
transitionProgress = transitionProgress + deltaTime * 0.2
transitionProgress = math.min(transitionProgress,1)
if transitionProgress < 0.25 then
local whiteAlpha = math.max(0, (1-transitionProgress/0.25))
gfx.BeginPath();
gfx.FillColor(255,255,255,math.floor(255*whiteAlpha));
gfx.Rect(0,0,desw,desh);
gfx.Fill();
gfx.ClosePath();
end
if transitionProgress > 0.85 then
local blackAlpha = math.min(1, ((transitionProgress-0.85)/0.15))
gfx.BeginPath();
gfx.FillColor(0,0,0,math.floor(255*blackAlpha));
gfx.Rect(0,0,desw,desh);
gfx.Fill();
gfx.ClosePath();
end
timer = timer + deltaTime
return transitionProgress >= 1
end
function render_out(deltaTime)
outProgress = outProgress + deltaTime * 0.2
outProgress = math.min(outProgress, 1)
timer = timer + deltaTime
return outProgress >= 1;
end
function sign(x)
return x>0 and 1 or x<0 and -1 or 0
end
function render_screen()
gfx.BeginPath()
gfx.ImageRect(0, 0, 1080, 1920, bgImage,1,0);
if transitionProgress < 0.35 then
local hex1alpha = math.max(0, (1-transitionProgress/0.35))
local hex2alpha = math.max(0, (1-transitionProgress/0.3))
gfx.BeginPath()
gfx.ImageRect(0,0, desw, desh, hexagonImages[1], hex1alpha, 0)
gfx.BeginPath()
gfx.ImageRect(0,0, desw, desh, hexagonImages[2], hex2alpha, 0)
end
gfx.BeginPath()
gfx.ImageRect(0,0,1080,1920,frameOverlayImage,1,0);
gfx.BeginPath()
gfx.ImageRect(0, 0, 1080, 1920, glowOverlayImage,1,0);
gfx.BeginPath()
gfx.ImageRect(37.5, 1074, 1180*0.85, 343*0.85, infoOverlayPanel, 1, 0);
if (timer % flickerTime) < (flickerTime / 2) then --flicker with 20Hz (50ms), 50% duty cycle
gfx.BeginPath()
gfx.ImageRect(37.5, 1074, 1180*0.85, 189*0.85, linkedHexagonsImage, 0.1, 0);
end
gfx.BeginPath()
gfx.ImageRect(10, 195.5, 1060, 1015, albumBgImage,1,0);
local jacket = song.jacket == 0 and noJacket or song.jacket
gfx.BeginPath();
gfx.ImageRect(235, 385, 608, 608, jacket, 1, 0)
gfx.ClosePath();
gfx.TextAlign(gfx.TEXT_ALIGN_CENTER + gfx.TEXT_ALIGN_MIDDLE)
gfx.FontSize(55)
gfx.Text(song.title,desw/2, 1114)
gfx.FontSize(30)
gfx.Text(song.artist, desw/2 , 1182)
local EFFECTOR_LABEL_Y = 1288
local ILLUSTRATOR_LABEL_Y = 1347
gfx.FontSize(22)
gfx.Text(song.effector, desw/2+70 , EFFECTOR_LABEL_Y-1)
gfx.Text(song.illustrator, desw/2+70 , ILLUSTRATOR_LABEL_Y-3)
-- Draw song diff level
gfx.BeginPath();
Numbers.draw_number(933, 1140, 1.0, song.level, 2, difficultyNumbers, false, 1, 1)
-- Draw song diff label (NOV/ADV/EXH/MXM/etc.)
gfx.BeginPath();
local diffLabelImage = difficultyLabelImages[song.difficulty+1];
local diffLabelW, diffLabelH = gfx.ImageSize(diffLabelImage);
gfx.ImageRect(952-diffLabelW/2, 1154-diffLabelH/2, diffLabelW, diffLabelH, diffLabelImage,1,0);
gfx.ClosePath();
gfx.Save();
gfx.FontSize(24)
gfx.LoadSkinFont('Digital-Serial-Bold.ttf')
gfx.TextAlign(gfx.TEXT_ALIGN_CENTER + gfx.TEXT_ALIGN_MIDDLE)
gfx.BeginPath();
gfx.Text('BPM', 127, 1140)
gfx.Text(song.bpm, 127, 1167)
-- temp ref overlay
-- gfx.BeginPath()
-- gfx.ImageRect(0, 0, 1080, 1920, refBgImage,0.5,0);
gfx.ClosePath();
gfx.Restore();
end
function reset()
transitionProgress = 0
resX, resY = game.GetResolution()
fullX = portraitWidescreenRatio * resY
fullY = resY
outProgress = 0
wasEnterSfxPlayed = false;
local common = require('common.common');
local Numbers = require('common.numbers')
game.LoadSkinSample('song_transition_screen/transition_enter.wav');
local backgroundImage = gfx.CreateSkinImage("bg_pattern.png", gfx.IMAGE_REPEATX | gfx.IMAGE_REPEATY)
local bgImage = gfx.CreateSkinImage("songtransition/bg.png", 0)
local glowOverlayImage = gfx.CreateSkinImage("songtransition/glowy.png", 0)
local frameOverlayImage = gfx.CreateSkinImage("songtransition/frames.png", 0)
local albumBgImage = gfx.CreateSkinImage("songtransition/album_crop.png", 0)
local infoOverlayPanel = gfx.CreateSkinImage("songtransition/info_panels_crop.png", 0)
local linkedHexagonsImage = gfx.CreateSkinImage("songtransition/linked_hexagons_crop.png", 0)
local hexagonImages = {
gfx.CreateSkinImage("songtransition/hex1.png", 0),
gfx.CreateSkinImage("songtransition/hex2.png", 0)
}
local difficultyNumbers;
local difficultyLabelImages = {
gfx.CreateSkinImage("songtransition/difficulty_labels/nov.png", 0),
gfx.CreateSkinImage("songtransition/difficulty_labels/adv.png", 0),
gfx.CreateSkinImage("songtransition/difficulty_labels/exh.png", 0),
gfx.CreateSkinImage("songtransition/difficulty_labels/mxm.png", 0),
gfx.CreateSkinImage("songtransition/difficulty_labels/inf.png", 0),
gfx.CreateSkinImage("songtransition/difficulty_labels/grv.png", 0),
gfx.CreateSkinImage("songtransition/difficulty_labels/hvn.png", 0),
gfx.CreateSkinImage("songtransition/difficulty_labels/vvd.png", 0),
}
local timer = 0
local transitionProgress = 0;
local outProgress = 0
local flickerTime = 0.050 --seconds (50ms)
-- Window variables
local resX, resY = game.GetResolution()
-- Aspect Ratios
local landscapeWidescreenRatio = 16 / 9
local landscapeStandardRatio = 4 / 3
local portraitWidescreenRatio = 9 / 16
-- Portrait sizes
local fullX, fullY
local desw = 1080
local desh = 1920
local noJacket = gfx.CreateSkinImage("song_select/loading.png", 0)
local wasEnterSfxPlayed = false;
function resetLayoutInformation()
resx, resy = game.GetResolution()
scale = resx / desw
end
function render(deltaTime)
if not wasEnterSfxPlayed then
common.stopMusic();
game.PlaySample('song_transition_screen/transition_enter.wav');
wasEnterSfxPlayed = true;
end
if not difficultyNumbers then
difficultyNumbers = Numbers.load_number_image('diff_num')
end
local x_offset = (resX - fullX) / 2
local y_offset = 0
gfx.BeginPath()
local bgImageWidth, bgImageHeight = gfx.ImageSize(backgroundImage)
gfx.Rect(0, 0, resX, resY)
gfx.FillPaint(gfx.ImagePattern(0, 0, bgImageWidth, bgImageHeight, 0, backgroundImage, 0.2))
gfx.Fill()
gfx.Translate(x_offset, y_offset);
gfx.Scale(fullX / 1080, fullY / 1920);
gfx.Scissor(0, 0, 1080, 1920);
render_screen();
transitionProgress = transitionProgress + deltaTime * 0.2
transitionProgress = math.min(transitionProgress,1)
if transitionProgress < 0.25 then
local whiteAlpha = math.max(0, (1-transitionProgress/0.25))
gfx.BeginPath();
gfx.FillColor(255,255,255,math.floor(255*whiteAlpha));
gfx.Rect(0,0,desw,desh);
gfx.Fill();
gfx.ClosePath();
end
if transitionProgress > 0.85 then
local blackAlpha = math.min(1, ((transitionProgress-0.85)/0.15))
gfx.BeginPath();
gfx.FillColor(0,0,0,math.floor(255*blackAlpha));
gfx.Rect(0,0,desw,desh);
gfx.Fill();
gfx.ClosePath();
end
timer = timer + deltaTime
return transitionProgress >= 1
end
function render_out(deltaTime)
outProgress = outProgress + deltaTime * 0.2
outProgress = math.min(outProgress, 1)
timer = timer + deltaTime
return outProgress >= 1;
end
function sign(x)
return x>0 and 1 or x<0 and -1 or 0
end
function render_screen()
gfx.BeginPath()
gfx.ImageRect(0, 0, 1080, 1920, bgImage,1,0);
if transitionProgress < 0.35 then
local hex1alpha = math.max(0, (1-transitionProgress/0.35))
local hex2alpha = math.max(0, (1-transitionProgress/0.3))
gfx.BeginPath()
gfx.ImageRect(0,0, desw, desh, hexagonImages[1], hex1alpha, 0)
gfx.BeginPath()
gfx.ImageRect(0,0, desw, desh, hexagonImages[2], hex2alpha, 0)
end
gfx.BeginPath()
gfx.ImageRect(0,0,1080,1920,frameOverlayImage,1,0);
gfx.BeginPath()
gfx.ImageRect(0, 0, 1080, 1920, glowOverlayImage,1,0);
gfx.BeginPath()
gfx.ImageRect(37.5, 1074, 1180*0.85, 343*0.85, infoOverlayPanel, 1, 0);
if (timer % flickerTime) < (flickerTime / 2) then --flicker with 20Hz (50ms), 50% duty cycle
gfx.BeginPath()
gfx.ImageRect(37.5, 1074, 1180*0.85, 189*0.85, linkedHexagonsImage, 0.1, 0);
end
gfx.BeginPath()
gfx.ImageRect(10, 195.5, 1060, 1015, albumBgImage,1,0);
local jacket = song.jacket == 0 and noJacket or song.jacket
gfx.BeginPath();
gfx.ImageRect(235, 385, 608, 608, jacket, 1, 0)
gfx.ClosePath();
gfx.TextAlign(gfx.TEXT_ALIGN_CENTER + gfx.TEXT_ALIGN_MIDDLE)
gfx.FontSize(55)
gfx.Text(song.title,desw/2, 1114)
gfx.FontSize(30)
gfx.Text(song.artist, desw/2 , 1182)
local EFFECTOR_LABEL_Y = 1288
local ILLUSTRATOR_LABEL_Y = 1347
gfx.FontSize(22)
gfx.Text(song.effector, desw/2+70 , EFFECTOR_LABEL_Y-1)
gfx.Text(song.illustrator, desw/2+70 , ILLUSTRATOR_LABEL_Y-3)
-- Draw song diff level
gfx.BeginPath();
Numbers.draw_number(933, 1140, 1.0, song.level, 2, difficultyNumbers, false, 1, 1)
-- Draw song diff label (NOV/ADV/EXH/MXM/etc.)
gfx.BeginPath();
local diffLabelImage = difficultyLabelImages[song.difficulty+1];
local diffLabelW, diffLabelH = gfx.ImageSize(diffLabelImage);
gfx.ImageRect(952-diffLabelW/2, 1154-diffLabelH/2, diffLabelW, diffLabelH, diffLabelImage,1,0);
gfx.ClosePath();
gfx.Save();
gfx.FontSize(24)
gfx.LoadSkinFont('Digital-Serial-Bold.ttf')
gfx.TextAlign(gfx.TEXT_ALIGN_CENTER + gfx.TEXT_ALIGN_MIDDLE)
gfx.BeginPath();
gfx.Text('BPM', 127, 1140)
gfx.Text(song.bpm, 127, 1167)
-- temp ref overlay
-- gfx.BeginPath()
-- gfx.ImageRect(0, 0, 1080, 1920, refBgImage,0.5,0);
gfx.ClosePath();
gfx.Restore();
end
function reset()
transitionProgress = 0
resX, resY = game.GetResolution()
fullX = portraitWidescreenRatio * resY
fullY = resY
outProgress = 0
wasEnterSfxPlayed = false;
end

215
scripts/titlescreen OLD.lua Normal file
View File

@ -0,0 +1,215 @@
local mposx = 0;
local mposy = 0;
local hovered = nil;
local cursorIndex = 1
local buttonWidth = 250;
local buttonHeight = 50;
local buttonBorder = 2;
local label = -1;
local gr_r, gr_g, gr_b, gr_a = game.GetSkinSetting("col_test")
gfx.GradientColors(0,127,255,255,0,128,255,0)
local gradient = gfx.LinearGradient(0,0,0,1)
local bgPattern = gfx.CreateSkinImage("bg_pattern.png", gfx.IMAGE_REPEATX + gfx.IMAGE_REPEATY)
local bgAngle = 0.5
local bgPaint = gfx.ImagePattern(0,0, 256,256, bgAngle, bgPattern, 1.0)
local bgPatternTimer = 0
local cursorYs = {}
local buttons = nil
local resx, resy = game.GetResolution();
view_update = function()
if package.config:sub(1,1) == '\\' then --windows
updateUrl, updateVersion = game.UpdateAvailable()
os.execute("start " .. updateUrl)
else --unix
--TODO: Mac solution
os.execute("xdg-open " .. updateUrl)
end
end
mouse_clipped = function(x,y,w,h)
return mposx > x and mposy > y and mposx < x+w and mposy < y+h;
end;
draw_button = function(button, x, y)
local name = button[1]
local rx = x - (buttonWidth / 2);
local ty = y - (buttonHeight / 2);
gfx.BeginPath();
gfx.TextAlign(gfx.TEXT_ALIGN_CENTER + gfx.TEXT_ALIGN_MIDDLE);
gfx.FontSize(40);
if mouse_clipped(rx,ty, buttonWidth, buttonHeight) then
hovered = button[2];
r, b_g, b_b, b_a = game.GetSkinSetting("col_test")
gfx.FillColor(0, 125, 255);
gfx.Text(name, x+1, y+1);
gfx.Text(name, x-1, y+1);
gfx.Text(name, x+1, y-1);
gfx.Text(name, x-1, y-1);
end
gfx.FillColor(255,255,255);
gfx.Text(name, x, y);
return buttonHeight + 5
end;
function updateGradient()
gr_r, gr_g, gr_b, gr_a = game.GetSkinSetting("col_test")
if gr_r == nil then return end
gfx.GradientColors(gr_r,gr_g,gr_b,gr_a,0,128,255,0)
--gradient = gfx.LinearGradient(0,0,0,1)
end
function updatePattern(dt)
bgPatternTimer = (bgPatternTimer + dt) % 1.0
local bgx = math.cos(bgAngle) * (bgPatternTimer * 256)
local bgy = math.sin(bgAngle) * (bgPatternTimer * 256)
gfx.UpdateImagePattern(bgPaint, bgx, bgy, 256, 256, bgAngle, 1.0)
end
function setButtons()
if buttons == nil then
buttons = {}
buttons[1] = {"Start", Menu.Start}
buttons[2] = {"Multiplayer", Menu.Multiplayer}
buttons[3] = {"Challenges", Menu.Challenges}
buttons[4] = {"Get Songs", Menu.DLScreen}
buttons[5] = {"Settings", Menu.Settings}
buttons[6] = {"Exit", Menu.Exit}
end
end
local renderY = resy/2
function draw_cursor(x,y,deltaTime)
gfx.Save()
gfx.BeginPath()
local size = 8
renderY = renderY - (renderY - y) * deltaTime * 30
gfx.MoveTo(x-size,renderY-size)
gfx.LineTo(x,renderY)
gfx.LineTo(x-size,renderY+size)
gfx.StrokeWidth(3)
gfx.StrokeColor(255,255,255)
gfx.Stroke()
gfx.Restore()
end
function sign(x)
return x>0 and 1 or x<0 and -1 or 0
end
function roundToZero(x)
if x<0 then return math.ceil(x)
elseif x>0 then return math.floor(x)
else return 0 end
end
function deltaKnob(delta)
if math.abs(delta) > 1.5 * math.pi then
return delta + 2 * math.pi * sign(delta) * -1
end
return delta
end
local lastKnobs = nil
local knobProgress = 0
function handle_controller()
if lastKnobs == nil then
lastKnobs = {game.GetKnob(0), game.GetKnob(1)}
else
local newKnobs = {game.GetKnob(0), game.GetKnob(1)}
knobProgress = knobProgress - deltaKnob(lastKnobs[1] - newKnobs[1]) * 1.2
knobProgress = knobProgress - deltaKnob(lastKnobs[2] - newKnobs[2]) * 1.2
lastKnobs = newKnobs
if math.abs(knobProgress) > 1 then
cursorIndex = (((cursorIndex - 1) + roundToZero(knobProgress)) % #buttons) + 1
knobProgress = knobProgress - roundToZero(knobProgress)
end
end
end
render = function(deltaTime)
setButtons()
updateGradient()
updatePattern(deltaTime)
resx,resy = game.GetResolution();
mposx,mposy = game.GetMousePos();
gfx.Scale(resx, resy / 3)
gfx.Rect(0,0,1,1)
gfx.FillPaint(gradient)
gfx.Fill()
gfx.ResetTransform()
gfx.BeginPath()
gfx.Scale(0.5,0.5)
gfx.Rect(0,0,resx * 2,resy * 2)
gfx.GlobalCompositeOperation(gfx.BLEND_OP_DESTINATION_IN)
gfx.FillPaint(bgPaint)
gfx.Fill()
gfx.ResetTransform()
gfx.BeginPath()
gfx.GlobalCompositeOperation(gfx.BLEND_OP_SOURCE_OVER)
cursorGet = 1
buttonY = resy / 2;
hovered = nil;
gfx.LoadSkinFont("NotoSans-Regular.ttf");
for i=1,#buttons do
cursorYs[i] = buttonY
buttonY = buttonY + draw_button(buttons[i], resx / 2, buttonY);
if hovered == buttons[i][2] then
cursorIndex = i
end
end
handle_controller()
draw_cursor(resx/2 - 100, cursorYs[cursorIndex], deltaTime)
gfx.BeginPath();
gfx.FillColor(255,255,255);
gfx.FontSize(120);
if label == -1 then
label = gfx.CreateLabel("ExperimentalGear ALPHA 1.8.7 ''README.TXT''", 120, 0);
end
gfx.TextAlign(gfx.TEXT_ALIGN_CENTER + gfx.TEXT_ALIGN_MIDDLE);
gfx.DrawLabel(label, resx / 2, resy / 2 - 200, resx-40);
updateUrl, updateVersion = game.UpdateAvailable()
if updateUrl then
gfx.BeginPath()
gfx.TextAlign(gfx.TEXT_ALIGN_BOTTOM + gfx.TEXT_ALIGN_LEFT)
gfx.FontSize(30)
gfx.Text(string.format("Version %s is now available", updateVersion), 5, resy - buttonHeight - 10)
draw_button({"View", view_update}, buttonWidth / 2 + 5, resy - buttonHeight / 2 - 5);
draw_button({"Update", Menu.Update}, buttonWidth * 1.5 + 15, resy - buttonHeight / 2 - 5)
end
end;
mouse_pressed = function(button)
if hovered then
hovered()
end
return 0
end
function button_pressed(button)
if button == game.BUTTON_STA then
buttons[cursorIndex][2]()
elseif button == game.BUTTON_BCK then
Menu.Exit()
end
end

View File

@ -1,97 +1,446 @@
require("common.globals")
local Common = require("common.util")
local bootScreen = require('titlescreen.boot')
local splashScreen = require('titlescreen.splash')
local titleScreen = require('titlescreen.title')
local modeSelectScreen = require('titlescreen.modeselect')
local serviceScreen = require('titlescreen.service')
require('common')
local Footer = require('components.footer');
local Background = require('components.background');
local screens = {
boot = {
screen = bootScreen
},
splash = {
screen = splashScreen
},
title = {
screen = titleScreen
},
mode_select = {
screen = modeSelectScreen
},
service = {
screen = serviceScreen
local lang = require("language.call")
local cursorIndex = 3;
local buttonHeight = 128 + 16;
local SELECTOR_BAR_OFFSET_FROM_CENTER = 128;
local BAR_ALPHA = 191;
local HEADER_HEIGHT = 100
local buttons = nil
local resx, resy = game.GetResolution()
local desw = 1080
local desh = 1920
local scale;
local backgroundImage = gfx.CreateSkinImage("bg_pattern.png", gfx.IMAGE_REPEATX | gfx.IMAGE_REPEATY)
local headerTitleImage = gfx.CreateSkinImage('titlescreen/title.png', 0);
local selectorBgImage = gfx.CreateSkinImage('titlescreen/selector_bg.png', 0);
local selectorArrowsImage = gfx.CreateSkinImage(
'titlescreen/selector_arrows.png', 0);
local unselectedButtonImage = gfx.CreateSkinImage(
'titlescreen/unselected_button.png', 0);
local selectedButtonBgImage = gfx.CreateSkinImage(
'titlescreen/selected_button_bg.png', 0);
local selectedButtonOverImage = gfx.CreateSkinImage(
'titlescreen/selected_button_over.png', 0);
local skillLabelImage = gfx.CreateSkinImage('titlescreen/labels/skill.png', 0);
local friendLabelImage = gfx.CreateSkinImage('titlescreen/labels/friend.png', 0);
local normalLabelImage = gfx.CreateSkinImage('titlescreen/labels/normal.png', 0);
local nauticaLabelImage = gfx.CreateSkinImage('titlescreen/labels/nautica.png',
0);
local settingsLabelImage = gfx.CreateSkinImage(
'titlescreen/labels/settings.png', 0);
local exitLabelImage = gfx.CreateSkinImage('titlescreen/labels/exit.png', 0);
local creww = game.GetSkinSetting("single_idol")
-- ANIMS
local idolAnimation = gfx.LoadSkinAnimation('crew/anim/'..creww, 1 / 30, 0, true);
-- AUDIO
game.LoadSkinSample('titlescreen/bgm.wav');
game.LoadSkinSample('titlescreen/cursor_change.wav');
game.LoadSkinSample('titlescreen/cursor_select.wav');
local selectorDescriptionLabel = gfx.CreateLabel(lang.Start.desc , 22, 0);
local selectorLegendScrollLabel = gfx.CreateLabel(lang.Start.sc , 20, 0);
local selectorLegendSelectLabel = gfx.CreateLabel(lang.Start.st3 , 20, 0);
local scrollTransitionScale = 1; -- Goes from 0 to 1 when transition is happening, sits at 1 when it's not.
local buttonsMovementScale = 0; -- Basically same as `scrollTransitionScale` but with a +/- sign for the scroll direction and goes from 1 to 0
local idolAnimTransitionScale = 0;
local oldCursorIndex = 3;
local scrollingUp = false;
local playedBgm = false;
-- Window variables
local resX, resY
-- Aspect Ratios
local landscapeWidescreenRatio = 16 / 9
local landscapeStandardRatio = 4 / 3
local portraitWidescreenRatio = 9 / 16
-- Portrait sizes
local fullX, fullY
local resolutionChange = function(x, y)
resX = x
resY = y
fullX = portraitWidescreenRatio * y
fullY = y
end
function resetLayoutInformation()
resx, resy = game.GetResolution()
desw = 1080
desh = 1920
scale = resx / desw
end
draw_button = function(button, x, y, selected, index)
local labelImage = button[1];
local labelWidth = button[2];
local descriptionText = button[4];
if (selected) then
-- Draw button background
gfx.BeginPath();
gfx.ImageRect(x, y + (196 / 2 * (1 - scrollTransitionScale)), 505,
196 * scrollTransitionScale, selectedButtonBgImage, 1, 0);
-- Draw button main label
gfx.BeginPath();
gfx.ImageRect(x + 256 - (labelWidth / 2),
(y + 58) + (64 / 2 * (1 - scrollTransitionScale)),
labelWidth, 64 * scrollTransitionScale, labelImage, 1, 0);
-- Draw description
gfx.GlobalAlpha((scrollTransitionScale - 0.8) * 5)
gfx.TextAlign(gfx.TEXT_ALIGN_CENTER + gfx.TEXT_ALIGN_MIDDLE)
gfx.FontSize(40);
gfx.BeginPath();
gfx.Text(descriptionText, x + 256, y + 28);
gfx.GlobalAlpha(1)
-- Draw the glow overlay
gfx.BeginPath();
gfx.ImageRect(x + 2, (y - 42) + (277 / 2 * (1 - scrollTransitionScale)),
501, 277 * scrollTransitionScale, selectedButtonOverImage,
1, 0);
else
if scrollingUp then
if (index == 3 or index == 0) then
gfx.GlobalAlpha(1 - scrollTransitionScale);
end
if (index == 2 or index == 5) then
gfx.GlobalAlpha(scrollTransitionScale);
end
else
if (index == 3 or index == 6) then
gfx.GlobalAlpha(1 - scrollTransitionScale);
end
if (index == 1 or index == 4) then
gfx.GlobalAlpha(scrollTransitionScale);
end
end
-- Draw button background
gfx.BeginPath();
gfx.ImageRect(x, y + buttonsMovementScale * buttonHeight, 1026 / 2,
257 / 2, unselectedButtonImage, 1, 0);
-- Draw button main label
gfx.BeginPath();
gfx.ImageRect(x + 64, y + 28 + buttonsMovementScale * buttonHeight,
labelWidth, 64, labelImage, 1, 0);
-- Draw description
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_MIDDLE)
gfx.FontSize(28);
gfx.BeginPath();
gfx.Text(descriptionText, x + 64,
y + 18 + buttonsMovementScale * buttonHeight);
gfx.GlobalAlpha(1)
end
end;
draw_buttons = function()
indexes = {
getCorrectedButtonIndex(cursorIndex, -2),
getCorrectedButtonIndex(cursorIndex, -1), cursorIndex,
getCorrectedButtonIndex(cursorIndex, 1),
getCorrectedButtonIndex(cursorIndex, 2)
}
}
local currentScreen = game.GetSkinSetting("animations_skipIntro") and screens.title or screens.boot -- show boot screen if skipIntro is not set
local yBase = desh / 2 + SELECTOR_BAR_OFFSET_FROM_CENTER;
local function deltaKnob(delta)
centerButtonY = yBase - buttonHeight / 2 - 28; -- to fit with the selector bg
marginFromDesHCenter = 128;
if scrollingUp then
draw_button(buttons[indexes[5]], desw - 512,
yBase - marginFromDesHCenter - buttonHeight * 3, false, 0); -- Placeholder for fadeout transition
end
draw_button(buttons[indexes[1]], desw - 512,
yBase - marginFromDesHCenter - buttonHeight * 2, false, 1);
draw_button(buttons[indexes[2]], desw - 512,
yBase - marginFromDesHCenter - buttonHeight, false, 2);
draw_button(buttons[indexes[3]], desw - 512, centerButtonY, true); -- The main selected center button
if scrollingUp then
draw_button(buttons[indexes[3]], desw - 512,
yBase + marginFromDesHCenter - buttonHeight, false, 3); -- Placeholder for transition that goes to the bottom
else
draw_button(buttons[indexes[3]], desw - 512, centerButtonY, false, 3); -- Placeholder for transition that goes to the top
end
draw_button(buttons[indexes[4]], desw - 512,
yBase + marginFromDesHCenter + 10, false, 4);
draw_button(buttons[indexes[5]], desw - 512,
yBase + marginFromDesHCenter + buttonHeight + 10, false, 5);
if not scrollingUp then
draw_button(buttons[indexes[1]], desw - 512,
yBase + marginFromDesHCenter + buttonHeight * 2, false, 6);
end
end;
function getCorrectedButtonIndex(from, offset)
buttonsNum = #buttons;
index = from + offset;
if index < 1 then
index = buttonsNum + (from + offset) -- this only happens if the offset is negative
end
if index > buttonsNum then
indexesUntilEnd = buttonsNum - from;
index = offset - indexesUntilEnd -- this only happens if the offset is positive
end
return index;
end
function drawTexts()
currentFullDescriptionText = buttons[cursorIndex][5];
gfx.BeginPath();
gfx.UpdateLabel(selectorDescriptionLabel, currentFullDescriptionText, 22)
gfx.BeginPath();
-- gfx.UpdateLabel(selectorLegendScrollLabel, 'SCROLL', 20);
-- descriptionAlpha = math.abs(selectedButtonScaleY - 0.5) * 2;
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_MIDDLE);
-- Description
gfx.FillColor(255, 255, 255, math.floor(scrollTransitionScale * 255));
gfx.BeginPath();
gfx.DrawLabel(selectorDescriptionLabel, 64,
desh / 2 + SELECTOR_BAR_OFFSET_FROM_CENTER - 52);
-- Legend on the selector
gfx.FillColor(217, 177, 126);
gfx.BeginPath();
gfx.DrawLabel(selectorLegendScrollLabel, 118,
desh / 2 + SELECTOR_BAR_OFFSET_FROM_CENTER + 56);
gfx.BeginPath();
gfx.DrawLabel(selectorLegendSelectLabel, 360,
desh / 2 + SELECTOR_BAR_OFFSET_FROM_CENTER + 56);
gfx.FillColor(255, 255, 255);
end
function setButtons()
if buttons == nil then
buttons = {}
buttons[1] = {
skillLabelImage, 412, Menu.Challenges,
lang.Challanges.ch, lang.Challanges.ch1
}
buttons[2] = {
friendLabelImage, 169, Menu.Multiplayer,
lang.Multiplayer.mp, lang.Multiplayer.mp2
}
buttons[3] = {
normalLabelImage, 210, Menu.Start,
lang.Start.st, lang.Start.st2
}
buttons[4] = {
nauticaLabelImage, 230, Menu.DLScreen,
lang.Nautica.dls, lang.Nautica.dls2
}
buttons[5] = {
settingsLabelImage, 247, Menu.Settings,
lang.Settings.se, lang.Settings.se1
}
buttons[6] = {
exitLabelImage, 110, Menu.Exit,
lang.Exit.ex, lang.Exit.ex2
}
end
end
function drawHeader()
gfx.BeginPath();
gfx.FillColor(0, 0, 0, BAR_ALPHA);
gfx.Rect(0, 0, desw, HEADER_HEIGHT);
gfx.Fill();
gfx.ClosePath()
gfx.ImageRect(desw/2 - 200, HEADER_HEIGHT/2 - 20, 400, 40, headerTitleImage, 1, 0)
end
function sign(x) return x > 0 and 1 or x < 0 and -1 or 0 end
function roundToZero(x)
if x < 0 then
return math.ceil(x)
elseif x > 0 then
return math.floor(x)
else
return 0
end
end
function deltaKnob(delta)
if math.abs(delta) > 1.5 * math.pi then
return delta + 2 * math.pi * Common.sign(delta) * -1
return delta + 2 * math.pi * sign(delta) * -1
end
return delta
end
local lastKnobs = nil
local knobProgress = 0
local function handleKnobs()
if not currentScreen.screen.onKnobsChange then
return
end
function handle_controller()
if lastKnobs == nil then
lastKnobs = {game.GetKnob(0), game.GetKnob(1)}
else
local newKnobs = {game.GetKnob(0), game.GetKnob(1)}
knobProgress = knobProgress - deltaKnob(lastKnobs[1] - newKnobs[1]) * 1.2
knobProgress = knobProgress - deltaKnob(lastKnobs[2] - newKnobs[2]) * 1.2
knobProgress = knobProgress - deltaKnob(lastKnobs[1] - newKnobs[1]) *
1.2
knobProgress = knobProgress - deltaKnob(lastKnobs[2] - newKnobs[2]) *
1.2
lastKnobs = newKnobs
if math.abs(knobProgress) > 1 then
if (knobProgress < 0) then
-- Negative
currentScreen.screen.onKnobsChange(-1)
else
-- Positive
currentScreen.screen.onKnobsChange(1)
cursorIndex = (((cursorIndex - 1) + roundToZero(knobProgress)) %
#buttons) + 1
scrollTransitionScale = 0; -- Reset transitions and play them
scrollingUp = false;
if ((cursorIndex > oldCursorIndex and
not (cursorIndex == 6 and oldCursorIndex == 1)) or
(cursorIndex == 1 and oldCursorIndex == 6)) then
scrollingUp = true;
end
knobProgress = knobProgress - Common.roundToZero(knobProgress)
game.PlaySample('titlescreen/cursor_change.wav');
oldCursorIndex = cursorIndex;
knobProgress = knobProgress - roundToZero(knobProgress)
end
end
end
local function handleScreenResponse(res)
if res and res.eventType == 'switch' then
if not screens[res.toScreen] then
game.Log('Undefined screen ' .. res.toScreen, game.LOGGER_ERROR)
return
end
currentScreen = screens[res.toScreen]
if currentScreen.screen.reset then
currentScreen.screen.reset()
draw_titlescreen = function (x, y, w, h, deltaTime)
gfx.Scissor(x,y,w,h);
gfx.Translate(x,y);
gfx.Scale(w/1080, h/1920);
gfx.LoadSkinFont("segoeui.ttf")
-- Draw background
gfx.BeginPath();
Background.draw(deltaTime)
local idolAnimTickRes = gfx.TickAnimation(idolAnimation, deltaTime);
if idolAnimTickRes == 1 then
gfx.GlobalAlpha(idolAnimTransitionScale);
idolAnimTransitionScale = idolAnimTransitionScale + 1 / 60;
if (idolAnimTransitionScale > 1) then
idolAnimTransitionScale = 1;
end
gfx.BeginPath();
gfx.ImageRect(0, 0, desw, desh, idolAnimation, 1, 0);
gfx.GlobalAlpha(1);
end
-- Draw selector background
gfx.BeginPath();
gfx.ImageRect(0, (desh / 2 + SELECTOR_BAR_OFFSET_FROM_CENTER) - 280 / 2,
1079, 280, selectorBgImage, 1, 0);
setButtons()
buttonY = (desh / 2) - 2 * (257 + 5);
draw_buttons();
drawTexts();
-- Draw the arrows around the selected button
gfx.BeginPath();
gfx.ImageRect(desw - 512, desh / 2 + SELECTOR_BAR_OFFSET_FROM_CENTER -
buttonHeight - 8, 501, 300, selectorArrowsImage, 1, 0);
-- Draw top and bottom bars
drawHeader();
Footer.draw(deltaTime);
gfx.ResetTransform();
end
function render(deltaTime)
handleKnobs()
handleScreenResponse(currentScreen.screen.render(deltaTime))
end
function mouse_pressed(button)
if (currentScreen.screen.onMousePressed) then
currentScreen.screen.onMousePressed(button)
render = function(deltaTime)
if not playedBgm then
game.PlaySample('titlescreen/bgm.wav', true);
playedBgm = true;
end
return 0
end
game.SetSkinSetting('_currentScreen', 'title')
-- detect resolution change
local resx, resy = game.GetResolution();
if resx ~= resX or resy ~= resY then
resolutionChange(resx, resy)
end
gfx.BeginPath()
local bgImageWidth, bgImageHeight = gfx.ImageSize(backgroundImage)
gfx.Rect(0, 0, resX, resY)
gfx.FillPaint(gfx.ImagePattern(0, 0, bgImageWidth, bgImageHeight, 0, backgroundImage, 0.2))
gfx.Fill()
draw_titlescreen((resX - fullX) / 2, 0, fullX, fullY, deltaTime);
handle_controller()
scrollTransitionScale = scrollTransitionScale + 1 / 60 * 5;
if (scrollTransitionScale > 1) then scrollTransitionScale = 1; end
if scrollingUp then
buttonsMovementScale = 1 - scrollTransitionScale
else
buttonsMovementScale = -1 + scrollTransitionScale
end
gfx.BeginPath();
end;
mouse_pressed = function(button) return 0 end
function button_pressed(button)
if (currentScreen.screen.onButtonPressed) then
currentScreen.screen.onButtonPressed(button)
if button == game.BUTTON_STA then
game.PlaySample('titlescreen/cursor_select.wav');
game.StopSample('titlescreen/bgm.wav');
buttons[cursorIndex][3]()
elseif button == game.BUTTON_BCK then
Menu.Exit()
end
end
-- the thing is... titlescreen script does not have a call to reset function... WHYYYYY
function reset() playedBgm = false; end

View File

@ -1,28 +0,0 @@
local Dim = require("common.dimensions")
local Wallpaper = require("components.wallpaper")
local BootPage = require("titlescreen.pages.boot.bootpage")
local PageView = require("components.pager.pageview")
local bootpage = BootPage.new()
local pageview = PageView.new(bootpage)
local function render(deltaTime)
Dim.updateResolution()
Wallpaper.render()
Dim.transformToScreenSpace()
pageview:render(deltaTime)
--pageview will be empty when you `back()` out of the root page
if not pageview:get() then
return {eventType = "switch", toScreen = "splash"}
end
end
local function onButtonPressed(button)
pageview:get():handleButtonInput(button)
end
return {render = render, onButtonPressed = onButtonPressed}

View File

@ -1,71 +0,0 @@
require("common.class")
local Util = require("common.util")
local ServiceField = require("titlescreen.fields.service.servicefield")
---@class CheckUpdateField: ServiceField
---@field onUpdateAvailable nil|fun(url: string, version: string)
---@field _timer number
local CheckUpdateField = {
__tostring = function() return "CheckUpdateField" end,
PROGRESS_FREQ = 1 / 5, -- 5Hz
CHECK_UPDATE_TIMEOUT = 5, -- seconds
}
---Create a new CheckUpdateField instance
---@param o? table # initial parameters
---@return CheckUpdateField
function CheckUpdateField.new(o)
o = o or {}
o._timer = o._timer or 0
o.onUpdateAvailable = o.onUpdateAvailable or nil
local this = CreateInstance(CheckUpdateField, o, ServiceField)
this._url = nil
this._version = nil
this._onUpdateAvailableFired = false
return this
end
function CheckUpdateField:drawLabel(deltaTime)
local text = self.label
local progress = math.ceil(Util.lerp(self._timer % self.PROGRESS_FREQ,
0, 0, self.PROGRESS_FREQ, 4
))
text = text .. string.rep(".", progress)
gfx.FontSize(self.FONT_SIZE)
gfx.LoadSkinFont(self.FONT_FACE)
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT | gfx.TEXT_ALIGN_TOP)
gfx.FillColor(table.unpack(self.FONT_COLOR))
gfx.Text(text, 0, 0)
end
function CheckUpdateField:drawValue(deltaTime)
end
function CheckUpdateField:tick(deltaTime)
if not self._onUpdateAvailableFired then
if self._timer > self.CHECK_UPDATE_TIMEOUT then
self._url, self._version = game.UpdateAvailable()
-- self._url = "" -- debug code to force onUpdateAvailable()
if self._url then
self.onUpdateAvailable(self._url, self._version)
self._onUpdateAvailableFired = true
else
self:getParentPage().viewHandler:clear() -- Exit out of bootscreen
end
end
end
self._timer = self._timer + deltaTime
end
function CheckUpdateField:render(deltaTime)
self:tick(deltaTime)
ServiceField.render(self, deltaTime)
end
return CheckUpdateField

View File

@ -1,149 +0,0 @@
require("common.class")
local ContainerField = require("components.pager.containerfield")
---@class DialogField: ContainerField
---@field _symbolMargin number
---@field _symbolSize number
local DialogField = {
__tostring = function() return "ContainerField" end,
BGCOLOR = {0, 0, 0, 255}, --{r, g, b, a}
DEFAULT_WIDTH = 400,
DEFAULT_HEIGHT = 200,
FONT_SIZE = 16,
FONT_FACE = "dfmarugoth.ttf",
FONT_COLOR = {255, 255, 255, 255},
BORDERCOLOR = {255, 255, 255, 255},
BORDERRADII = 12,
BORDERWIDTH = 2,
HEADER = {
title = "Title",
code = "0-0000-0000"
},
TEXT = {
"Top text,",
"Sample text,",
"Bottom text."
},
LEGEND = {
{
label = "BUTTON",
text = "DESCRIPTION"
},
},
}
---Create a new DialogField instance
---
---Inherits from ContainerField
---@param o ContainerField
---@return DialogField
function DialogField.new(o)
o = o or {}
o.aabbW = o.aabbW or DialogField.DEFAULT_WIDTH
o.aabbH = o.aabbH or DialogField.DEFAULT_HEIGHT
local this = CreateInstance(DialogField, o, ContainerField)
this._symbolMargin = 8
this._symbolSize = 48
return this
end
---Draw the dialog symbol
---
---Default implementation is a yellow triangle with an exclamation mark
---@param deltaTime number # frametime in seconds
function DialogField:drawSymbol(deltaTime)
local symbolColor = {255, 255, 0, 255}
gfx.Save()
gfx.Translate(self._symbolMargin, self._symbolMargin)
gfx.FillColor(table.unpack(symbolColor))
gfx.BeginPath()
local symbolBottomY = math.sqrt(3) / 2 * self._symbolSize
gfx.MoveTo(0, symbolBottomY)
gfx.LineTo(self._symbolSize / 2, 0)
gfx.LineTo(self._symbolSize, symbolBottomY)
gfx.Fill()
-- exclamation mark
local excTopMargin = 10
local excBottomMargin = 4
local excThickness = 5
local excColor = {0, 0, 0, 255}
gfx.FillColor(table.unpack(excColor))
gfx.BeginPath()
gfx.Rect(
self._symbolSize / 2 - excThickness / 2, -- x
excTopMargin, -- y
excThickness, -- w
symbolBottomY - excTopMargin - excBottomMargin - 3 / 2 * excThickness -- h
)
gfx.Rect(
self._symbolSize / 2 - excThickness / 2, -- x
symbolBottomY - excBottomMargin - excThickness, -- y
excThickness, excThickness -- w, h
)
gfx.Fill()
gfx.Restore()
end
---@param deltaTime number # frametime in seconds
function DialogField:drawBackground(deltaTime)
local textMargin = 4
-- border
local borderH = self.aabbH - #self.LEGEND * self.FONT_SIZE - textMargin
gfx.BeginPath()
gfx.StrokeColor(table.unpack(self.BORDERCOLOR))
gfx.StrokeWidth(self.BORDERWIDTH)
gfx.FillColor(table.unpack(self.BGCOLOR))
gfx.RoundedRect(0, 0, self.aabbW, borderH, self.BORDERRADII)
gfx.Fill()
gfx.Stroke()
gfx.FontSize(self.FONT_SIZE)
gfx.LoadSkinFont(self.FONT_FACE)
-- draw symbol
self:drawSymbol(deltaTime)
-- legend
local legendX = 0
local legendY = borderH + textMargin
gfx.TextAlign(gfx.TEXT_ALIGN_TOP | gfx.TEXT_ALIGN_LEFT)
gfx.FillColor(table.unpack(self.FONT_COLOR))
for _, legend in ipairs(self.LEGEND) do
gfx.Text(legend.label .. " = " .. legend.text, legendX, legendY)
legendY = legendY + self.FONT_SIZE
end
-- header
local headerX = self._symbolSize + self._symbolMargin + 16
local headerY = self._symbolMargin
gfx.Save()
gfx.Translate(headerX, headerY)
gfx.Text(self.HEADER.title, 0, 0)
local separatorY = self.FONT_SIZE + textMargin
local separatorThickness = 1
gfx.StrokeWidth(separatorThickness)
gfx.BeginPath()
gfx.MoveTo(0, separatorY)
gfx.LineTo(self.aabbW - headerX - self._symbolMargin, separatorY)
gfx.Stroke()
local codeY = separatorY + textMargin
gfx.Text(self.HEADER.code, 0, codeY)
gfx.Restore()
end
---@param deltaTime number # frametime in seconds
function DialogField:drawForeground(deltaTime)
local textX = 12
local textY = 64
local lineHeight = self.FONT_SIZE + 4
for _, line in ipairs(self.TEXT) do
gfx.Text(line, textX, textY)
textY = textY + lineHeight
end
end
return DialogField

View File

@ -1,134 +0,0 @@
require("common.class")
local Util = require("common.util")
local ServiceField = require("titlescreen.fields.service.servicefield")
---@class SelfTestStatusEnum
SelfTestStatusEnum = {
IDLE = 1,
INPROGRESS = 2,
OK = 3,
PASS = 4,
ERROR = 5
}
local function statusToString(status)
local statusName = {"IDLE", "INPROGRESS", "OK", "PASS", "ERROR"}
return statusName[status]
end
---@class SelfTestField: ServiceField
---@field checkTask nil|fun(obj: any): SelfTestStatusEnum # a function that will run asynchronously on activating the Field
---@field status SelfTestStatusEnum
---@field onStatusChange nil|fun(status) # a callback function on finishing the checkTask
---@field _thread thread
---@field _timer number
local SelfTestField = {
__tostring = function () return "SelfTestField" end,
COLOR_INPROGRESS = {255, 255, 255, 255},
COLOR_OK = {0, 255, 0, 255},
COLOR_PASS = {255, 255, 0, 255},
COLOR_ERROR = {255, 0, 0, 255},
INPROGRESS_FREQ = 1 / 20, --20Hz
}
---Create a new SelfTestField instance
---@param o? table
---@return SelfTestField
function SelfTestField.new(o)
o = o or {}
o.status = o.status or SelfTestStatusEnum.IDLE
o._timer = 0
o._thread = nil
assert((not o.onStatusChange) or (o.checkTask and o.onStatusChange),
"Failed to construct SelfTestField, checkTask is mandatory when onStatusChange is defined!\n" .. debug.traceback()
)
return CreateInstance(SelfTestField, o, ServiceField)
end
function SelfTestField:_closeThread()
if self._thread and coroutine.status(self._thread) ~= "dead" then
coroutine.close(self._thread)
end
end
function SelfTestField:_resumeThread()
if self._thread and coroutine.status(self._thread) == "suspended" then
local success, status = coroutine.resume(self._thread)
game.Log(self.label .. ": success: " .. tostring(success) ..
", status: " .. status .. " (" .. statusToString(status) .. ")",
game.LOGGER_DEBUG
)
if success and status ~= self.status then
self.status = status
if self.onStatusChange then
game.Log("SKIN CONFIG: onStatusChange(" .. status .. ") (" ..
statusToString(status) .. ")",
game.LOGGER_DEBUG
)
self.onStatusChange(status)
end
end
end
end
function SelfTestField:activate(obj)
self:_closeThread()
if self.checkTask then
self._thread = coroutine.create(self.checkTask)
self:_resumeThread()
end
end
function SelfTestField:deactivate(obj)
self:_closeThread()
end
function SelfTestField:tick(deltaTime)
self:_resumeThread()
self._timer = self._timer + deltaTime
end
function SelfTestField:drawValue(deltaTime)
gfx.Translate(self.VALUE_OFFSETX, 0)
gfx.TextAlign(gfx.TEXT_ALIGN_RIGHT | gfx.TEXT_ALIGN_TOP)
gfx.FillColor(table.unpack(self.FONT_COLOR))
gfx.Text(": ", 0, 0)
local color, text
if self.status == SelfTestStatusEnum.IDLE then
color = self.FONT_COLOR
text = ""
elseif self.status == SelfTestStatusEnum.INPROGRESS then
local progress = math.ceil(Util.lerp(self._timer % self.INPROGRESS_FREQ,
0, 0, self.INPROGRESS_FREQ, 4
))
color = self.COLOR_INPROGRESS
text = string.rep(".", progress)
elseif self.status == SelfTestStatusEnum.OK then
color = self.COLOR_OK
text = "OK"
elseif self.status == SelfTestStatusEnum.PASS then
color = self.COLOR_PASS
text = "PASS"
elseif self.status == SelfTestStatusEnum.ERROR then
color = self.COLOR_ERROR
text = "ERROR"
end
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT | gfx.TEXT_ALIGN_TOP)
gfx.FillColor(table.unpack(color))
gfx.Text(text, 0, 0)
end
function SelfTestField:render(deltaTime)
self:tick(deltaTime)
ServiceField.render(self, deltaTime)
end
return SelfTestField

View File

@ -1,46 +0,0 @@
require("common.class")
local Util = require("common.util")
local ServiceField = require("titlescreen.fields.service.servicefield")
---@class ColorGradientField: ServiceField
local ColorGradientField = {
__tostring = function() return "ColorGradientField" end,
GRADIENT_X_OFFSET = 128,
GRADIENT_WIDTH = 576,
GRADIENT_STEPS = 32
}
---Create a new ColorGradientField instance
---@param o? table # initial parameters
---@return ColorGradientField
function ColorGradientField.new(o)
o = o or {}
o.value = o.value or {0, 0, 0, 255}
return CreateInstance(ColorGradientField, o, ServiceField)
end
---@param obj? any # message object for the field
function ColorGradientField:activate(obj) end
---@param obj? any # message object for the field
function ColorGradientField:focus(obj) end
---@param obj? any # message object for the field
function ColorGradientField:deactivate(obj) end
---@param deltaTime number # frametime in seconds
function ColorGradientField:drawValue(deltaTime)
local stepW = self.GRADIENT_WIDTH / self.GRADIENT_STEPS
for i = 0, self.GRADIENT_STEPS - 1 do
local posX = self.GRADIENT_X_OFFSET + i * stepW
local colorA = math.ceil(Util.lerp(i, 0, 0, self.GRADIENT_STEPS - 1, self.value[4]))
gfx.BeginPath()
gfx.Rect(posX, 0, stepW, self.aabbH)
gfx.FillColor(self.value[1], self.value[2], self.value[3], colorA)
gfx.Fill()
end
end
return ColorGradientField

View File

@ -1,43 +0,0 @@
require("common.class")
local ServiceField = require("titlescreen.fields.service.servicefield")
---@class InputButtonField: ServiceField
---@field button integer
local InputButtonField = {
__tostring = function() return "InputButtonField" end,
}
---Create a new InputButtonField instance
---@param o? table # initial parameters
---@return InputButtonField
function InputButtonField.new(o)
o = o or {}
o.button = o.button or nil
return CreateInstance(InputButtonField, o, ServiceField)
end
---@param obj? any # message object for the field
function InputButtonField:activate(obj) end
---@param obj? any # message object for the field
function InputButtonField:focus(obj) end
---@param obj? any # message object for the field
function InputButtonField:deactivate(obj) end
---@param deltaTime number # frametime in seconds
function InputButtonField:drawValue(deltaTime)
gfx.Translate(self.VALUE_OFFSETX, 0)
if not self.button then
gfx.Text("<BUTTON NOT SET>", 0, 0)
return
end
self.value = game.GetButton(self.button) and "ON" or "OFF"
gfx.Text(self.value, 0, 0)
end
return InputButtonField

View File

@ -1,80 +0,0 @@
require("common.class")
local Util = require("common.util")
local ServiceField = require("titlescreen.fields.service.servicefield")
---@class InputKnobField: ServiceField
---@field knob integer
local InputKnobField = {
__tostring = function() return "InputKnobField" end,
SLIDER_SIZE = {200, 16}, --{w, h}
SLIDER_BGCOLOR = {255, 0, 0, 255},
SLIDER_FRAME_COLOR = ServiceField.FONT_COLOR,
SLIDER_FRAME_WIDTH = 1,
SLIDER_OFFSETX = 64,
SLIDER_INDICATOR_COLOR = {0, 255, 0, 255},
SLIDER_INDICATOR_WIDTH = 4
}
---Create a new InputKnobField instance
---@param o? table # initial parameters
---@return InputKnobField
function InputKnobField.new(o)
o = o or {}
o.knob = o.knob or nil
return CreateInstance(InputKnobField, o, ServiceField)
end
---@param obj? any # message object for the field
function InputKnobField:activate(obj) end
---@param obj? any # message object for the field
function InputKnobField:focus(obj) end
---@param obj? any # message object for the field
function InputKnobField:deactivate(obj) end
---@param deltaTime number # frametime in seconds
function InputKnobField:drawValue(deltaTime)
gfx.Translate(self.VALUE_OFFSETX, 0)
if not self.knob then
gfx.Text("<KNOB NOT SET>", 0, 0)
return
end
local knobAngle = game.GetKnob(self.knob)
local sliderWidth = self.SLIDER_SIZE[1]
local sliderHeight = self.SLIDER_SIZE[2]
local sliderBgColor = self.SLIDER_BGCOLOR
local sliderFrameColor = self.SLIDER_FRAME_COLOR
local sliderFrameWidth = self.SLIDER_FRAME_WIDTH
local maxValue = 1024
self.value = math.floor(Util.lerp(knobAngle,0, 0, 2 * math.pi, maxValue)) % maxValue
--draw value
gfx.Text(self.value, 0, 0)
--draw slider
gfx.Translate(self.SLIDER_OFFSETX, 0)
gfx.BeginPath()
gfx.Rect(0, 0, sliderWidth, sliderHeight)
gfx.FillColor(table.unpack(sliderBgColor))
gfx.StrokeColor(table.unpack(sliderFrameColor))
gfx.StrokeWidth(sliderFrameWidth)
gfx.Fill()
gfx.Stroke()
local sliderIndicatorX = Util.lerp(self.value, 0, 0, maxValue, sliderWidth)
local sliderIndicatorWidth = self.SLIDER_INDICATOR_WIDTH
local sliderIndicatorColor = self.SLIDER_INDICATOR_COLOR
--draw indicator
gfx.BeginPath()
gfx.Rect(sliderIndicatorX, sliderFrameWidth, sliderIndicatorWidth, sliderHeight - 2 * sliderFrameWidth)
gfx.FillColor(table.unpack(sliderIndicatorColor))
gfx.Fill()
end
return InputKnobField

Some files were not shown because too many files have changed in this diff Show More