diff --git a/browser.lua b/browser.lua new file mode 100644 index 0000000..538dd02 --- /dev/null +++ b/browser.lua @@ -0,0 +1,487 @@ +local leaderboard = nil +local maps +local mapIndex = 1 +local scrollPos = 1 +local modes = nil +local mode = 1 +local prefMode = nil + +-- Imported functions ------- +-- lb_common.lua +local ZoneAct = lb_ZoneAct +local TicsToTime = lb_TicsToTime +----------------------------- + +local cv_kartencore + +local function mapIndexOffset(n) + return (mapIndex + n + #maps - 1) % #maps + 1 +end + +local function getMap(offset) + return maps[mapIndexOffset(offset or 0)] +end + +local function updateModes() + -- set available modes for this map + modes = {} + for mode, scoreTable in pairs(leaderboard) do + if scoreTable[getMap()] then + table.insert(modes, mode) + end + end + table.sort(modes) + + mode = 1 + -- select pref mode + for i, m in ipairs(modes) do + if m == prefMode then + mode = i + break + end + end +end + +local function updateMapIndex(n) + mapIndex = mapIndexOffset(n) + scrollPos = 1 + + updateModes() +end + +local scalar = 2 +local hlfScrnWdth = 320 / 2 +local mappY = 26 +local ttlY = mappY + FixedMul(30, FRACUNIT / scalar) +local scoresY = ttlY + 16 + +local sin = sin +local function drawMapPatch(v, offset) + local scale = FRACUNIT / (abs(offset) + scalar) + local mapName = G_BuildMapName(getMap(offset)) + local patchName = mapName.."P" + local mapp = v.patchExists(patchName) and v.cachePatch(patchName) or v.cachePatch("BLANKLVL") + + local scaledWidth = FixedMul(mapp.width, scale) + local scaledHeight = FixedMul(mapp.height, scale) + + v.drawScaled( + (hlfScrnWdth + offset * scaledWidth - scaledWidth / 2) * FRACUNIT, + (mappY - scaledHeight / 2) * FRACUNIT, + scale, + mapp + ) + +end + +local function drawEncore(v) + if not cv_kartencore then + cv_kartencore = CV_FindVar("kartencore") + end + + if not cv_kartencore.value then + return + end + + local rubyp = v.cachePatch("RUBYICON") + local bob = sin(leveltime * ANG10) * 2 + v.drawScaled( + hlfScrnWdth * FRACUNIT, + mappY * FRACUNIT + bob, + FRACUNIT, + rubyp + ) +end + +local colors = { + [0] = 0, + [1] = 215 +} +local function drawMapBorder(v) + local mapWidth = FixedMul(160, FRACUNIT / scalar) + local mapHeight = FixedMul(100, FRACUNIT / scalar) + v.drawFill( + hlfScrnWdth - mapWidth / 2 - 1, + mappY - mapHeight / 2 -1, + mapWidth + 2, + mapHeight + 2, + colors[leveltime / 4 % 2] + ) +end + +local function drawMapStrings(v) + local map = mapheaderinfo[getMap()] + local titleWidth = v.stringWidth(map.lvlttl) + + -- title + v.drawString( + hlfScrnWdth, + ttlY, + map.lvlttl, + V_SKYMAP, + "center" + ) + + -- zone/act + local zone = ZoneAct(map) + local zoneWidth = v.stringWidth(zone) + v.drawString( + hlfScrnWdth + titleWidth / 2, + ttlY + 8, + zone, + V_SKYMAP, + "right" + ) + + -- subtitle + v.drawString( + hlfScrnWdth + titleWidth / 2 - zoneWidth, + ttlY + 8, + map.subttl, + V_MAGENTAMAP, + "small-right" + ) + + -- hell + if map.menuflags & LF2_HIDEINMENU then + v.drawString( + 300, + ttlY + 16, + "HELL", + V_REDMAP, + "right" + ) + end +end + +local F_SPBATK = 0x1 +local F_SPBJUS = 0x2 +local F_SPBBIG = 0x4 +local F_SPBEXP = 0x8 +local F_ENCORE = 0x80 + +local function drawGamemode(v) + local m = modes[mode] or 0 + + local modeX = 20 + local modeY = scoresY + local scale = FRACUNIT / 2 + + if m == 0 then + local clockp = v.cachePatch("K_LAPE02") + v.drawScaled( + modeX * FRACUNIT, + modeY * FRACUNIT, + scale, + clockp + ) + + v.drawString( + modeX, + modeY, + "Time Attack!" + ) + elseif m & F_SPBATK then + local scaledHalf = FixedMul(50 * FRACUNIT, scale) / 2 + local xoff = 0 + if m & F_SPBBIG then + xoff = $ + scaledHalf + end + if m & F_SPBEXP then + xoff = $ + scaledHalf + end + + if m & F_SPBBIG then + local growp = v.cachePatch("K_ITGROW") + v.drawScaled( + modeX * FRACUNIT - scaledHalf + xoff, + modeY * FRACUNIT - scaledHalf, + scale, + growp + ) + + xoff = $ - scaledHalf + end + + if m & F_SPBEXP then + local invp = v.cachePatch("K_ITINV"..(leveltime / 3 % 7 + 1)) + v.drawScaled( + modeX * FRACUNIT - scaledHalf + xoff, + modeY * FRACUNIT - scaledHalf, + scale, + invp + ) + end + + local spbp = v.cachePatch("K_ITSPB") + v.drawScaled( + modeX * FRACUNIT - scaledHalf, + modeY * FRACUNIT - scaledHalf, + scale, + spbp + ) + + v.drawString( + modeX, + modeY, + "SPB Attack!" + ) + end +end + +local function drawFlags(v, x, y, flags) + local nx = x * FRACUNIT + local ny = y * FRACUNIT + 2 * FRACUNIT + local margin = 4 * FRACUNIT + if flags & F_ENCORE then + local encp = v.cachePatch("RUBYICON") + v.drawScaled( + nx, + ny + 2 * FRACUNIT, + FRACUNIT / 5, + encp + ) + nx = $ + margin + end + if flags & F_SPBATK then + local scale = FRACUNIT / 3 + local shift = 6 * FRACUNIT + nx = $ - shift + ny = $ - shift + if flags & F_SPBJUS then + local hyup = v.cachePatch("K_ISHYUD") + v.drawScaled(nx, ny, scale, hyup) + nx = $ + margin + end + if flags & F_SPBBIG then + local growp = v.cachePatch("K_ISGROW") + v.drawScaled(nx - FRACUNIT / 2, ny, scale, growp) + nx = $ + margin + end + if flags & F_SPBEXP then + local invp = v.cachePatch("K_ISINV"..(leveltime / 3 % 6 + 1)) + v.drawScaled(nx, ny, scale, invp) + nx = $ + margin + end + end +end + +local function drawStats(v, x, y, skin, stats) + local s = skins[skin] + if not (s + and s.kartspeed == stats["speed"] + and s.kartweight == stats["weight"] + ) + and stats then + v.drawString(x-2, y-2, stats["speed"], V_ALLOWLOWERCASE, "thin") + v.drawString(x + 13, y + 9, stats["weight"], V_ALLOWLOWERCASE, "thin") + end +end + +-- draw in columns +-- pos, facerank, name, time, flags +-- ______________________________________________ +-- | 3|[O]|InsertNameHere | 01:02:03 | EXB | +-- ---------------------------------------------- +-- defined are widths of each column, x value is calculated below +local column = { + [1] = 18, -- facerank, pos, drawNum is right aligned + [2] = 170, -- name + [3] = 60, -- time + [4] = 0 -- flags +} +do + local w = 32 -- starting offset + local t + for i = 1, #column do + t = column[i] + column[i] = w + w = $ + t + end +end + +local colorFlags = { + [0] = V_SKYMAP, + [1] = 0 +} +local function drawScore(v, i, pos, score, highlight) + local y = scoresY + i * 18 + local textFlag = colorFlags[pos%2] + + -- position + v.drawNum(column[1], y, pos) + + -- facerank + local skin = skins[score["skin"]] + local facerank = skin and v.cachePatch(skin.facerank) or v.cachePatch("M_NORANK") + v.draw(column[1], y, facerank, 0, v.getColormap("sonic", score["color"])) + + -- chili + if highlight then + local chilip = v.cachePatch("K_CHILI"..leveltime/4%8+1) + v.draw(column[1], y, chilip) + textFlag = V_YELLOWMAP + end + + -- stats + drawStats(v, column[1], y, score["skin"], score["stat"]) + -- name + v.drawString(column[2], y, score["name"], V_ALLOWLOWERCASE | textFlag) + -- time + v.drawString(column[3], y, TicsToTime(score["time"]), textFlag) + -- flags + drawFlags(v, column[4], y, score["flags"]) +end + +local function drawBrowser(v, player) + if not leaderboard then return end + + v.fadeScreen(0xFF00, 16) + + -- previous, next maps + for i = 5, 1, -1 do + drawMapPatch(v, -i) + drawMapPatch(v, i) + end + + -- draw map border + drawMapBorder(v) + + -- current map + drawMapPatch(v, 0) + drawEncore(v) + drawMapStrings(v) + drawGamemode(v) + + if not modes then return end + + local gamemode = leaderboard[modes[mode]] + if not gamemode then return end + + local scoreTable = gamemode[getMap()] + if not scoreTable then return end + + local scores = #scoreTable + scrollPos = max(min(scrollPos, scores - 3), 1) + local endi = min(scrollPos + 7, scores) + for i = scrollPos, endi do + drawScore(v, i - scrollPos + 1, i, scoreTable[i], scoreTable[i].name == player.name) + end +end +rawset(_G, "DrawBrowser", drawBrowser) + +local function initBrowser(lb) + leaderboard = lb + + -- set mapIndex to current map + for i, m in ipairs(maps) do + if m == gamemap then + mapIndex = i + break + end + end + + scrollPos = 1 + updateModes() +end +rawset(_G, "InitBrowser", initBrowser) + +-- initialize maps with racemaps only +local function loadMaps() + maps = {} + local hell = {} + for i = 0, #mapheaderinfo do + local map = mapheaderinfo[i] + if map and map.typeoflevel & TOL_RACE then + if map.menuflags & LF2_HIDEINMENU then + table.insert(hell, i) + else + table.insert(maps, i) + end + end + end + + -- append hell maps + for _, map in ipairs(hell) do + table.insert(maps, map) + end +end +addHook("MapLoad", loadMaps) + +local repeatCount = 0 +local keyRepeat = 0 + +local function updateKeyRepeat() + S_StartSound(nil, 143) + if repeatCount < 1 then + keyRepeat = TICRATE / 4 + else + keyRepeat = TICRATE / 15 + end + repeatCount = $ + 1 +end + +local function resetKeyRepeat() + keyRepeat = 0 + repeatCount = 0 +end + +local ValidButtons = BT_ACCELERATE | BT_BRAKE | BT_FORWARD | BT_BACKWARD | BT_DRIFT | BT_ATTACK + +-- return value indicates we want to exit the browser +local function controller(player) + keyRepeat = max(0, $ - 1) + + if not (player.cmd.driftturn or player.cmd.buttons) then + resetKeyRepeat() + end + + local cmd = player.cmd + if not keyRepeat then + if not (cmd.buttons & ValidButtons or cmd.driftturn) then + return + end + + updateKeyRepeat() + + if cmd.buttons & BT_BRAKE then + S_StartSound(nil, 115) + return true + elseif cmd.buttons & BT_ACCELERATE then + COM_BufInsertText(player, "changelevel "..G_BuildMapName(maps[mapIndex])) + return true + elseif cmd.buttons & BT_ATTACK then + COM_BufInsertText(player, "encore") + elseif cmd.driftturn then + local dir = cmd.driftturn > 0 and -1 or 1 + + if encoremode then + updateMapIndex(-dir) + else + updateMapIndex(dir) + end + elseif cmd.buttons & BT_FORWARD then + scrollPos = $ - 1 + elseif cmd.buttons & BT_BACKWARD then + scrollPos = $ + 1 + elseif cmd.buttons & BT_DRIFT then + scrollPos = 1 + if modes and #modes then + mode = $ % #modes + 1 + prefMode = modes[mode] + end + end + end +end +rawset(_G, "BrowserController", controller) + +local function netvars(net) + maps = net($) + mapIndex = net($) + modes = net($) + mode = net($) + prefMode = net($) + scrollPos = net($) + leaderboard = net($) +end +addHook("NetVars", netvars) diff --git a/lb_common.lua b/lb_common.lua new file mode 100644 index 0000000..ac7f479 --- /dev/null +++ b/lb_common.lua @@ -0,0 +1,26 @@ +rawset(_G, "lb_TicsToTime", function(tics, pure) + if tics == 0 and pure then + return "-:--:--" + end + + return string.format( + "%d:%02d:%02d", + G_TicsToMinutes(tics, true), + G_TicsToSeconds(tics), + G_TicsToCentiseconds(tics) + ) +end) + +rawset(_G, "lb_ZoneAct", function(map) + local z = "" + if map.zonttl != "" then + z = " " + map.zonttl + elseif not(map.levelflags & LF_NOZONE) then + z = " Zone" + end + if map.actnum != "" then + z = $ + " " + map.actnum + end + + return z +end) diff --git a/leaderboard.lua b/leaderboard.lua index 87b8696..cdb4a78 100644 --- a/leaderboard.lua +++ b/leaderboard.lua @@ -33,7 +33,7 @@ local RedFlash = { local StatTrack = false local UNCLAIMED = "Unclaimed Record" -local HELP_MESSAGE = "\x89Leaderboard Commands:\nretry exit findmap changelevel spba_clearcheats lb_gui rival scroll encore records" +local HELP_MESSAGE = "\x89Leaderboard Commands:\nretry exit findmap changelevel spba_clearcheats lb_gui rival scroll encore records levelselect" local FILENAME = "leaderboard.txt" -- Retry / changelevel map @@ -55,6 +55,7 @@ local clearcheats = false local START_TIME = 6 * TICRATE + (3 * TICRATE / 4) + 1 local AFK_TIMEOUT = TICRATE * 5 +local AFK_BROWSER = TICRATE * 15 local AFK_BALANCE = TICRATE * 60 local AFK_BALANCE_WARN = AFK_BALANCE - TICRATE * 10 local PREVENT_JOIN_TIME = START_TIME + TICRATE * 5 @@ -68,6 +69,7 @@ local DS_DEFAULT = 0x0 local DS_SCROLL = 0x1 local DS_AUTO = 0x2 local DS_SCRLTO = 0x4 +local DS_BROWSER = 0x8 local drawState = DS_DEFAULT @@ -85,9 +87,18 @@ local clamp local scroll_to -local ticsToTime - local allowJoin + +-- Imported functions -- + +-- lb_common.lua +local ticsToTime = lb_TicsToTime +local zoneAct = lb_ZoneAct + +-- browser.lua +local InitBrowser = InitBrowser +local DrawBrowser = DrawBrowser +local BrowserController = BrowserController --------------- -- cvars @@ -396,19 +407,26 @@ local function exitlevel(player, ...) end COM_AddCommand("exit", exitlevel) -local function zoneAct(map) - local z = "" - if map.zonttl != "" then - z = " " + map.zonttl - elseif not(map.levelflags & LF_NOZONE) then - z = " Zone" - end - if map.actnum != "" then - z = $ + " " + map.actnum +local function initBrowser(player) + if not doyoudare(player) then return end + + -- TODO: allow in battle + if mapheaderinfo[gamemap].typeoflevel & TOL_MATCH then + CONS_Printf(player, "Please exit battle first") + return end - return z + if not InitBrowser then + print("Browser is not loaded") + return + end + + InitBrowser(lb) + drawState = DS_BROWSER + + player.afkTime = leveltime end +COM_AddCommand("levelselect", initBrowser) local function findMap(player, ...) local search = ... @@ -823,19 +841,6 @@ addHook("MapLoad", function() end ) -function ticsToTime(tics, pure) - if tics == 0 and pure then - return "-:--:--" - end - - return string.format( - "%d:%02d:%02d", - G_TicsToMinutes(tics, true), - G_TicsToSeconds(tics), - G_TicsToCentiseconds(tics) - ) -end - -- Item patches have the amazing property of being displaced 12x 13y pixels local iXoffset = 13 * FRACUNIT local iYoffset = 12 * FRACUNIT @@ -858,7 +863,7 @@ local modePatches = { local function modePatch(flag) if flag == F_SPBEXP then - return PATCH[modePatches[flag]][(leveltime / 4) % 6] + return PATCH[modePatches[flag]][(leveltime / 3) % 6] end return PATCH[modePatches[flag]] end @@ -1116,11 +1121,16 @@ local function drawScrollTo(v, player, scoreTable, gui) drawScroll(v, player, scoreTable, gui) end +local function drawBrowser(v, player) + DrawBrowser(v, player) +end + local stateFunctions = { [DS_DEFAULT] = drawDefault, [DS_SCROLL] = drawScroll, [DS_AUTO] = drawAuto, - [DS_SCRLTO] = drawScrollTo + [DS_SCRLTO] = drawScrollTo, + [DS_BROWSER] = drawBrowser } -- Draw mode and return pos + 1 if success @@ -1332,7 +1342,7 @@ local function think() if not singleplayer() then for p in players.iterate do if p.valid and not p.spectator and not p.exiting and p.lives > 0 then - if p.cmd.buttons then + if p.cmd.buttons or p.cmd.driftturn then p.afkTime = leveltime end @@ -1457,13 +1467,33 @@ local function think() scrollAcc = 0 end end - end + elseif drawState == DS_BROWSER then + if BrowserController(p) then + drawState = DS_DEFAULT + end - if p.lives == 0 then + -- prevent intermission while browsing + if p.exiting then + p.exiting = $ + 1 + end + + -- disable spba hud + if server.SPBAdone then + server.SPBArunning = false + p.pflags = $ & !(PF_TIMEOVER) + p.exiting = 100 + end + + -- prevent softlocking the server + if p.afkTime + AFK_BROWSER < leveltime then + drawState = DS_DEFAULT + S_StartSound(nil, 100) + end + elseif p.lives == 0 then drawState = DS_SCROLL end - if p.cmd.buttons then + if p.cmd.buttons or p.cmd.driftturn then p.afkTime = leveltime end