Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

Documentation for this module may be created at Module:Rankings/doc

local p = {}
local cargo = mw.ext.cargo
local html = mw.html

local function sqlEscape(s) if not s then return "" end return s:gsub("\\", "\\\\"):gsub("'", "\\'") end

local function getTimestamps(dateStr)
    if not dateStr or dateStr == "" then return 0 end
    local y, m, d = dateStr:match("(%d+)-(%d+)-(%d+)")
    if not y then return 0 end
    return os.time({year=y, month=m, day=d, hour=12, min=0, sec=0})
end

local function buildLogo(teamName, tData, mData)
    local lightFile = "Shield_team.png"
    local darkFile = "Shield_team_dark.png"

    if tData and tData.image and tData.image ~= "" then
        lightFile = tData.image; darkFile = (tData.image_dark and tData.image_dark ~= "") and tData.image_dark or lightFile
    elseif mData and mData.image and mData.image ~= "" then
        lightFile = mData.image; darkFile = (mData.image_dark and mData.image_dark ~= "") and mData.image_dark or lightFile
    end

    local container = html.create('span'):addClass('team-logo-wrapper')
    container:tag('span'):addClass('logo-lightmode'):wikitext('[[File:' .. lightFile .. '|24px|link=|class=team-logo]]')
    container:tag('span'):addClass('logo-darkmode'):wikitext('[[File:' .. darkFile .. '|24px|link=|class=team-logo]]')
    return tostring(container)
end

local function getTeamBasePts(tier, rank)
    local t = (tier or ""):lower()
    local col = 0
    if t:find("publisher") then col = 1
    elseif t:find("tier 1") then col = 2
    elseif t:find("tier 2") then col = 3
    elseif t:find("tier 3") then col = 4
    else return 0 end

    if rank == 1 then return ({1000, 800, 600, 400})[col]
    elseif rank == 2 then return ({800, 700, 500, 350})[col]
    elseif rank == 3 then return ({700, 600, 400, 300})[col]
    elseif rank == 4 then return ({600, 500, 300, 250})[col]
    elseif rank == 5 then return ({500, 400, 250, 200})[col]
    elseif rank >= 6 and rank <= 10 then return ({400, 300, 200, 150})[col]
    elseif rank >= 11 and rank <= 20 then return ({300, 200, 150, 100})[col]
    elseif rank >= 21 and rank <= 30 then return ({200, 100, 75, 50})[col]
    elseif rank >= 31 and rank <= 48 then return ({100, 50, 35, 25})[col]
    end return 0
end

local function getTeamDecay(days)
    if days <= 180 then return 1
    elseif days <= 270 then return 0.75
    elseif days <= 365 then return 0.5
    elseif days <= 1095 then return 0.1
    else return 0 end
end

local function getPlayerDecay(days)
    if days <= 180 then return 1
    elseif days <= 240 then return 0.75
    elseif days <= 300 then return 0.5
    elseif days <= 365 then return 0.25
    elseif days <= 1095 then return 0.1
    else return 0 end
end

function p.getRankings(statType, targetDateStr)
    local targetTs = os.time() 
    if targetDateStr and targetDateStr ~= "" then
        local targetVal = getTimestamps(targetDateStr)
        if targetVal > 0 then targetTs = targetVal end
    end
    
    statType = (statType or "team"):lower()
    local list = {}
    
    if statType == "team" then
        local results = cargo.query("RankingData_Team", "tournament, tier, end_date, team, rank", { orderBy = "end_date DESC", limit = 5000 })
        if not results then return {} end
        local tData = {}
        for _, r in ipairs(results) do
            local t = r.team
            local endTs = getTimestamps(r.end_date)
            if t and t ~= "" and endTs > 0 and endTs <= targetTs then
                if not tData[t] then tData[t] = { name = t, team = t, points = 0, events = 0, latest_tourney = r.tournament, p2024 = 0, p2025 = 0, p2026 = 0 } end
                
                local diff = os.difftime(targetTs, endTs)
                local days = math.max(0, math.floor(diff / 86400))
                
                local base = getTeamBasePts(r.tier, tonumber(r.rank) or 99)
                local finalPts = base * getTeamDecay(days)
                
                local year = r.end_date:match("^(%d+)")
                if year == "2024" then tData[t].p2024 = tData[t].p2024 + base
                elseif year == "2025" then tData[t].p2025 = tData[t].p2025 + base
                elseif year == "2026" then tData[t].p2026 = tData[t].p2026 + base end
                
                if finalPts > 0 then
                    tData[t].points = tData[t].points + finalPts
                    tData[t].events = tData[t].events + 1
                end
            end
        end
        for _, d in pairs(tData) do if d.points > 0 then table.insert(list, d) end end

    elseif statType == "player" then
        local results = cargo.query("RankingData_Player", "tournament, tier, end_date, player, team, finishes, mvp_tourney, igl, survivor, mvp_finals, emerging", { orderBy = "end_date DESC", limit = 5000 })
        if not results then return {} end
        local pData = {}
        for _, r in ipairs(results) do
            local p = r.player
            local endTs = getTimestamps(r.end_date)
            if p and p ~= "" and endTs > 0 and endTs <= targetTs then
                if not pData[p] then pData[p] = { name = p, player = p, team = r.team, latest_tourney = r.tournament, points = 0, events = 0, total_finishes = 0 } 
                else
                    if (not pData[p].team or pData[p].team == "") and r.team and r.team ~= "" then pData[p].team = r.team end
                end
                
                local diff = os.difftime(targetTs, endTs)
                local days = math.max(0, math.floor(diff / 86400))
                
                local t = (r.tier or ""):lower()
                local mult = 1
                if t:find("publisher") then mult = 2 elseif t:find("tier 1") then mult = 1.5 end
                
                local rawFinishes = tonumber(r.finishes) or 0
                local base = rawFinishes * mult
                
                if tonumber(r.mvp_tourney) == 1 then base = base + 20 end
                if tonumber(r.mvp_finals) == 1 then base = base + 10 end
                if tonumber(r.igl) == 1 then base = base + 10 end
                if tonumber(r.survivor) == 1 then base = base + 10 end
                if tonumber(r.emerging) == 1 then base = base + 5 end
                
                local finalPts = base * getPlayerDecay(days)
                if finalPts > 0 then
                    pData[p].total_finishes = pData[p].total_finishes + rawFinishes
                    pData[p].points = pData[p].points + finalPts
                    pData[p].events = pData[p].events + 1
                end
            end
        end
        for _, d in pairs(pData) do if d.points > 0 then table.insert(list, d) end end
    end
    
    table.sort(list, function(a, b) return a.points > b.points end)
    for i, d in ipairs(list) do d.rank = i end
    return list
end

function p.getRank(name, statType, targetDateStr)
    local list = p.getRankings(statType, targetDateStr)
    for _, d in ipairs(list) do
        if d.name:lower() == name:lower() then return d.rank, d.points end
    end
    return nil, nil
end

function p.main(frame)
    local args = frame:getParent().args
    if not args.type then args = frame.args end
    local statType = (args.type or "team"):lower()
    local limit = tonumber(args.limit) or 100
    
    local isCompact = false
    if args.compact == "true" or args.compact == "1" or args.compact == "yes" then isCompact = true end
    
    local list = p.getRankings(statType, args.as_of_date)
    if #list == 0 then return 'No data.' end

    local uniqueTeams = {}
    for _, d in ipairs(list) do
        if d.team and d.team ~= "" then uniqueTeams[d.team] = true end
    end
    
    local tourneyDb = {}; local masterDb = {}; local teamListQuoted = {}
    for t, _ in pairs(uniqueTeams) do table.insert(teamListQuoted, "'" .. sqlEscape(t) .. "'") end
    local teamSql = table.concat(teamListQuoted, ",")
    
    if teamSql ~= "" and cargo and cargo.query then
        local mResults = cargo.query("Teams", "name, image, image_dark", { where = "name IN (" .. teamSql .. ")", limit = 500 })
        if mResults then for _, row in ipairs(mResults) do masterDb[row.name:lower()] = row end end
        local tResults = cargo.query("Tournament_Teams", "tournament, team, image, image_dark", { where = "team IN (" .. teamSql .. ")", limit = 5000 })
        if tResults then for _, row in ipairs(tResults) do tourneyDb[row.tournament:lower() .. "|" .. row.team:lower()] = row end end
    end

    local root = html.create('div'):addClass('rk-table-wrapper')
    local tbl = root:tag('table'):addClass('rk-table sortable')
    local th = tbl:tag('tr')
    
    if statType == "team" then
        th:tag('th'):addClass('rk-center'):wikitext('#')
        th:tag('th'):wikitext('Team')
        
        if not isCompact then
            th:tag('th'):addClass('rk-center'):wikitext('Tournaments')
            th:tag('th'):addClass('rk-center'):wikitext('2024 Pts')
            th:tag('th'):addClass('rk-center'):wikitext('2025 Pts')
            th:tag('th'):addClass('rk-center'):wikitext('2026 Pts')
        end
        th:tag('th'):addClass('rk-right'):wikitext('Total Points')
        
        for i = 1, math.min(#list, limit) do
            local d = list[i]
            local tr = tbl:tag('tr'):addClass('rk-data-row')
            
            if i == 1 then tr:addClass('rk-gold')
            elseif i == 2 then tr:addClass('rk-silver')
            elseif i == 3 then tr:addClass('rk-bronze') end

            tr:tag('td'):addClass('rk-center rk-rank-cell')
            
            local tD = tourneyDb[d.latest_tourney:lower() .. "|" .. d.team:lower()]
            local mD = masterDb[d.team:lower()]
            
            local teamCell = tr:tag('td')
            local flexDiv = teamCell:tag('div'):addClass('rk-flex-cell')
            flexDiv:wikitext(buildLogo(d.team, tD, mD))
            flexDiv:tag('div'):addClass('rk-name-col'):wikitext('[[' .. d.team .. ']]')
            
            if not isCompact then
                tr:tag('td'):addClass('rk-center rk-num'):wikitext(d.events)
                tr:tag('td'):addClass('rk-center rk-num'):wikitext(d.p2024)
                tr:tag('td'):addClass('rk-center rk-num'):wikitext(d.p2025)
                tr:tag('td'):addClass('rk-center rk-num'):wikitext(d.p2026)
            end
            
            tr:tag('td'):addClass('rk-right rk-total'):wikitext(string.format("%.2f", d.points))
        end

    elseif statType == "player" then
        th:tag('th'):addClass('rk-center'):wikitext('#')
        th:tag('th'):wikitext('Player')
        
        if not isCompact then
            th:tag('th'):addClass('rk-center'):wikitext('Tournaments') 
            th:tag('th'):addClass('rk-center'):wikitext('Finishes')
        end
        th:tag('th'):addClass('rk-right'):wikitext('Total Points')
        
        for i = 1, math.min(#list, limit) do
            local d = list[i]
            local tr = tbl:tag('tr'):addClass('rk-data-row')
            
            if i == 1 then tr:addClass('rk-gold')
            elseif i == 2 then tr:addClass('rk-silver')
            elseif i == 3 then tr:addClass('rk-bronze') end

            tr:tag('td'):addClass('rk-center rk-rank-cell')
            
            local pCell = tr:tag('td')
            local flexDiv = pCell:tag('div'):addClass('rk-flex-cell')
            
            if d.team and d.team ~= "" then
                local tD = tourneyDb[d.latest_tourney:lower() .. "|" .. d.team:lower()]
                local mD = masterDb[d.team:lower()]
                flexDiv:wikitext(buildLogo(d.team, tD, mD))
            else
                flexDiv:wikitext(buildLogo("", nil, nil))
            end

            local infoDiv = flexDiv:tag('div'):css('line-height','1.1')
            infoDiv:tag('div'):addClass('rk-name-col'):wikitext('[[' .. d.player .. ']]')
            
            if d.team and d.team ~= "" then
                infoDiv:tag('div'):addClass('rk-sub-name'):wikitext('[[' .. d.team .. ']]')
            else
                infoDiv:tag('div'):addClass('rk-sub-name'):css('opacity','0.5'):wikitext('Free Agent')
            end
            
            if not isCompact then
                tr:tag('td'):addClass('rk-center rk-num'):wikitext(d.events)
                tr:tag('td'):addClass('rk-center rk-num'):wikitext(d.total_finishes)
            end
            tr:tag('td'):addClass('rk-right rk-total'):wikitext(string.format("%.2f", d.points))
        end
    end
    return tostring(root)
end

return p