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:Statistics/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 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 then
        if mData.image and mData.image ~= "" then lightFile = mData.image else lightFile = teamName .. ".png" end
        if mData.image_dark and mData.image_dark ~= "" then darkFile = mData.image_dark else darkFile = lightFile end
    else
        lightFile = teamName .. ".png"; darkFile = teamName .. "_dark.png"
    end

    local container = html.create('span'):addClass('team-logo-wrapper')
    if not mw.title.new('File:' .. lightFile).exists then lightFile = "Shield_team.png" end
    if not mw.title.new('File:' .. darkFile).exists then 
        if mw.title.new('File:' .. lightFile).exists then darkFile = lightFile else darkFile = "Shield_team_dark.png" end
    end

    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 fmt(num, decimals)
    if not num then return "0" end
    if decimals and decimals > 0 then return string.format("%." .. decimals .. "f", num) else return tostring(math.floor(num + 0.5)) end
end

local function getHeatmap(val, min, max, isGood)
    if not val or not min or max == min then return "" end
    local ratio = (val - min) / (max - min)
    if ratio < 0 then ratio = 0 end
    if ratio > 1 then ratio = 1 end
    local opacity = 0.02 + (ratio * 0.23)
    if isGood then return string.format("background-color: rgba(var(--heatmap-good), %.2f);", opacity)
    else return string.format("background-color: rgba(var(--heatmap-bad), %.2f);", opacity) end
end

local function getMinMax(t)
    if not t or #t == 0 then return 0, 0 end
    local mn, mx = t[1], t[1]
    for i=2, #t do
        if t[i] < mn then mn = t[i] end
        if t[i] > mx then mx = t[i] end
    end
    return mn, mx
end

local function parseTime(s)
    if not s or s == "" then return 0 end
    if tonumber(s) then return tonumber(s) end
    
    local parts = {}
    for p in s:gmatch("%d+") do table.insert(parts, tonumber(p)) end
    if #parts == 1 then return parts[1] end
    if #parts == 2 then return parts[1] * 60 + parts[2] end
    if #parts == 3 then return parts[1] * 3600 + parts[2] * 60 + parts[3] end
    return 0
end

local function formatTimeStr(totalSeconds)
    if not totalSeconds or totalSeconds <= 0 then return "00:00" end
    local s = math.floor(totalSeconds + 0.5)
    local m = math.floor(s / 60)
    local sec = s % 60
    return string.format("%02d:%02d", m, sec)
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 tournamentName = args.tournament or ""
    local limit = tonumber(args.limit) or (statType == "player" and 100 or 500)
    
    local function makeTH(row, classes, text, isCorner)
        local z = isCorner and '30' or '20'
        local cell = row:tag('th'):addClass(classes):css('position', 'sticky'):css('top', '0'):css('z-index', z)
        if text and text ~= "" then cell:wikitext(text) end
        return cell
    end
    
    local optColumns = {
        "damage", "survival", "rdamage", "headshots", "assists", "knockouts", "long_elim",
        "vehicle_elim", "grenade_elim", "smokes", "grenades", "molotovs", "flash",
        "utilities", "airdrops", "rescues", "dist_drove", "dist_walk", "total_dist"
    }

    local colNames = {
        damage = "Dmg", rdamage = "Dmg Recv", headshots = "Headshots", assists = "Assists",
        knockouts = "Knocks", long_elim = "Long Elim", vehicle_elim = "Veh Elims",
        grenade_elim = "Nade Elims", smokes = "Smokes", grenades = "Nades",
        molotovs = "Mollys", flash = "Flashes", utilities = "Utils",
        airdrops = "Drops", rescues = "Rescues", dist_drove = "Drive Dist",
        dist_walk = "Walk Dist", total_dist = "Total Dist", survival = "Survival Time"
    }

    local inverseCols = { rdamage = true }
    
    local activeCols = {}
    for _, col in ipairs(optColumns) do
        local showAvg = args["show_avg_" .. col] == "true"
        local showMax = args["show_max_" .. col] == "true"
        local showSum = args["show_sum_" .. col] == "true"
        if col == "damage" and args.show_damage == "true" then showAvg = true end
        if col == "assists" and args.show_assists == "true" then showSum = true end
        if col == "headshots" and args.show_headshots == "true" then showSum = true end
        if col == "knockouts" and args.show_knocks == "true" then showSum = true end
        if showAvg or showMax or showSum then activeCols[col] = { avg = showAvg, max = showMax, sum = showSum } end
    end
    
    local showMvps = args.show_mvps == "true"
    local showMvpScore = args.show_mvp_score == "true"
    local showAvgRank = args.show_avg_rank == "true"
    local showIglScore = args.show_igl_score == "true"
    local cumulativePts = args.cumulative_pts == "true"
    local formatSurv = (args.survival_format == "mm:ss" or args.survival_format == "mmss" or args.survival_format == "time")
    
    local filterRole = args.role and args.role:lower() or nil
    
    local neededForScore = { stage = true }
    if showMvpScore then 
        neededForScore.damage = true; neededForScore.knockouts = true; neededForScore.survival = true 
    end
    if showIglScore then
        neededForScore.damage = true
    end
    
    local mvpStages = {}
    if args.mvp_stage and args.mvp_stage ~= "" then
        for v in args.mvp_stage:gmatch("[^,]+") do mvpStages[v:match("^%s*(.-)%s*$")] = true end
    end
    
    local whereParts = {}
    local filters = {"tournament", "tournament_type", "stage", "group_name", "map", "match_type", "tournament_day", "date", "time", "team", "player"}
    for _, f in ipairs(filters) do
        local val = args[f] or args[f:gsub("_name", "")] 
        if val and val ~= "" then
            if val:find(",") then
                local inVals = {}
                for v in val:gmatch("[^,]+") do table.insert(inVals, "'" .. sqlEscape(v:match("^%s*(.-)%s*$")) .. "'") end
                table.insert(whereParts, string.format("%s IN (%s)", f, table.concat(inVals, ",")))
            else
                table.insert(whereParts, string.format("%s = '%s'", f, sqlEscape(val)))
            end
        end
    end
    local whereClause = #whereParts > 0 and table.concat(whereParts, " AND ") or "1=1"
    
    local root = html.create('div'):addClass('standings-wrapper stats-wrapper')
    local tbl = root:tag('table'):addClass('standings-card-table stats-card-table sortable')

    local tData = {}; local pData = {}; local uniqueTeams = {}; local uniquePlayers = {}
    local g = { surv = 0, dmg = 0, elims = 0, knocks = 0, matches = 0 }
    
    local teamStatsForIgl = {}
    local g_igl = { t_pts = 0, t_wwcd = 0, t_top5 = 0, t_surv = 0, t_matches = 0, num_teams = 0, p_elims = 0, p_dmg = 0, p_matches = 0 }

    local tSelect = "team, short_name, map, rank, wwcd, place_pts, elim_pts, total_pts, number_of_teams"
    local pSelect = "player, team, player_elims, team_elims, mvp, role"
    
    local pSet = { player=true, team=true, player_elims=true, team_elims=true, mvp=true, role=true }
    for col, _ in pairs(activeCols) do
        tSelect = tSelect .. ", " .. col
        if not pSet[col] then pSelect = pSelect .. ", " .. col; pSet[col] = true end
    end
    for col, _ in pairs(neededForScore) do
        if not pSet[col] then pSelect = pSelect .. ", " .. col; pSet[col] = true end
    end

    if statType == "team" then
        local results = {}
        local offset = 0
        while true do
            local batch = cargo.query("MatchStats_Team", tSelect, { where = whereClause, limit = 5000, offset = offset })
            if not batch or #batch == 0 then break end
            for i = 1, #batch do results[#results + 1] = batch[i] end
            if #batch < 5000 then break end
            offset = offset + 5000
        end
        if #results == 0 then return '<div style="padding:20px; color:var(--text-muted); font-style:italic;">No team statistics available.</div>' end
        
        for _, r in ipairs(results) do
            local t = r.team
            uniqueTeams[t] = true
            if not tData[t] then 
                tData[t] = { 
                    team = t, short = r.short_name, matches = 0, rank_sum = 0, place = 0, elims = 0, total = 0, wwcd = 0, 
                    place_2_5 = 0, place_6_10 = 0, place_11_plus = 0,
                    pts_0 = 0, pts_1_4 = 0, pts_5_9 = 0, pts_10_16 = 0, pts_17_23 = 0, pts_24_plus = 0,
                    map_pts = {}, map_matches = {} 
                } 
                for col, _ in pairs(activeCols) do tData[t]["sum_"..col] = 0; tData[t]["max_"..col] = 0 end
            end
            
            local d = tData[t]
            d.matches = d.matches + 1
            local rank = tonumber(r.rank or 99)
            d.rank_sum = d.rank_sum + rank
            d.place = d.place + tonumber(r.place_pts or 0); d.elims = d.elims + tonumber(r.elim_pts or 0)
            local pts = tonumber(r.total_pts or 0); d.total = d.total + pts
            d.wwcd = d.wwcd + tonumber(r.wwcd or 0)
            
            for col, _ in pairs(activeCols) do
                local val = 0
                if col == "survival" then val = parseTime(r.survival) else val = tonumber(r[col]) or 0 end
                d["sum_"..col] = d["sum_"..col] + val
                if val > d["max_"..col] then d["max_"..col] = val end
            end
            
            if rank >= 2 and rank <= 5 then d.place_2_5 = d.place_2_5 + 1
            elseif rank >= 6 and rank <= 10 then d.place_6_10 = d.place_6_10 + 1
            elseif rank >= 11 then d.place_11_plus = d.place_11_plus + 1 end
            
            if pts == 0 then d.pts_0 = d.pts_0 + 1 
            elseif pts < 5 then d.pts_1_4 = d.pts_1_4 + 1 
            elseif pts < 10 then d.pts_5_9 = d.pts_5_9 + 1 
            elseif pts < 17 then d.pts_10_16 = d.pts_10_16 + 1 
            elseif pts < 24 then d.pts_17_23 = d.pts_17_23 + 1
            else d.pts_24_plus = d.pts_24_plus + 1 end
            
            if r.map and r.map ~= "" then
                d.map_pts[r.map] = (d.map_pts[r.map] or 0) + pts
                d.map_matches[r.map] = (d.map_matches[r.map] or 0) + 1
            end
        end
    elseif statType == "player" or statType == "igl" then
        
        if showIglScore or statType == "igl" then
            local tResults = cargo.query("MatchStats_Team", "stage, team, total_pts, wwcd, rank, survival", { where = whereClause, limit = 5000 })
            if tResults then
                for _, r in ipairs(tResults) do
                    local isValidMvp = true
                    if next(mvpStages) and not mvpStages[r.stage] then isValidMvp = false end
                    
                    if isValidMvp then
                        local t = r.team
                        if not teamStatsForIgl[t] then
                            teamStatsForIgl[t] = { matches = 0, pts = 0, wwcd = 0, top5 = 0, surv = 0 }
                            g_igl.num_teams = g_igl.num_teams + 1
                        end
                        local d = teamStatsForIgl[t]
                        d.matches = d.matches + 1
                        g_igl.t_matches = g_igl.t_matches + 1
                        
                        local pts = tonumber(r.total_pts) or 0
                        d.pts = d.pts + pts
                        g_igl.t_pts = g_igl.t_pts + pts
                        
                        local wwcd = tonumber(r.wwcd) or 0
                        d.wwcd = d.wwcd + wwcd
                        g_igl.t_wwcd = g_igl.t_wwcd + wwcd
                        
                        local rank = tonumber(r.rank) or 99
                        if rank <= 5 then
                            d.top5 = d.top5 + 1
                            g_igl.t_top5 = g_igl.t_top5 + 1
                        end
                        
                        local surv = parseTime(r.survival)
                        d.surv = d.surv + surv
                        g_igl.t_surv = g_igl.t_surv + surv
                    end
                end
            end
        end

        local results = {}
        local offset = 0
        while true do
            local batch = cargo.query("MatchStats_Player", pSelect, { where = whereClause, limit = 5000, offset = offset })
            if not batch or #batch == 0 then break end
            
            for i = 1, #batch do
                results[#results + 1] = batch[i]
            end
            
            if #batch < 5000 then break end
            offset = offset + 5000
        end

        if #results == 0 then return '<div style="padding:20px; color:var(--text-muted); font-style:italic;">No player statistics available.</div>' end        

        for _, r in ipairs(results) do
            local isValidMvp = true
            if next(mvpStages) and not mvpStages[r.stage] then isValidMvp = false end
            
            if isValidMvp then
                local p = r.player
                uniqueTeams[r.team] = true
                uniquePlayers[p] = true
                
                if not pData[p] then 
                    pData[p] = { player = p, team = r.team, matches = 0, elims = 0, t_elims = 0, max_elims = 0, elims_5plus = 0, elims_0 = 0, mvps = 0, t_surv = 0, t_dmg = 0, t_knocks = 0 } 
                    for col, _ in pairs(activeCols) do pData[p]["sum_"..col] = 0; pData[p]["max_"..col] = 0 end
                end
                
                local d = pData[p]
                if r.role and r.role ~= "" and r.role:lower() ~= "player" then d.role = r.role end
                
                d.matches = d.matches + 1
                local e = tonumber(r.player_elims or 0)
                local te = tonumber(r.team_elims or 0)
                
                d.elims = d.elims + e
                d.t_elims = d.t_elims + te
                d.mvps = d.mvps + tonumber(r.mvp or 0)
                
                local surv = parseTime(r.survival)
                local dmg = tonumber(r.damage) or 0
                local knocks = tonumber(r.knockouts) or 0
                
                g.matches = g.matches + 1
                g.surv = g.surv + surv; g.dmg = g.dmg + dmg; g.elims = g.elims + e; g.knocks = g.knocks + knocks
                
                d.t_surv = d.t_surv + surv; d.t_dmg = d.t_dmg + dmg; d.t_knocks = d.t_knocks + knocks
                
                if showIglScore or statType == "igl" then
                    g_igl.p_elims = g_igl.p_elims + e
                    g_igl.p_dmg = g_igl.p_dmg + dmg
                    g_igl.p_matches = g_igl.p_matches + 1
                end
                
                for col, _ in pairs(activeCols) do
                    local val = 0
                    if col == "survival" then val = parseTime(r.survival) else val = tonumber(r[col]) or 0 end
                    d["sum_"..col] = d["sum_"..col] + val
                    if val > d["max_"..col] then d["max_"..col] = val end
                end
                
                if e > d.max_elims then d.max_elims = e end
                if e >= 5 then d.elims_5plus = d.elims_5plus + 1 end
                if e == 0 then d.elims_0 = d.elims_0 + 1 end
            end
        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 tResults = cargo.query("Tournament_Teams", "team, display_name, image, image_dark", { where = "tournament='" .. sqlEscape(tournamentName) .. "' AND team IN (" .. teamSql .. ")", limit = 500 })
        if tResults then for _, row in ipairs(tResults) do tourneyDb[row.team:lower()] = row end end
        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
    end

    if statType == "team" then
        local list = {}
        local extremes = { 
            avg_rank={}, avg_place={}, avg_elims={}, avg_total={}, win_pct={}, 
            place_2_5_pct={}, place_6_10_pct={}, place_11_plus_pct={}, 
            pts_0_pct={}, pts_1_4_pct={}, pts_5_9_pct={}, pts_10_16_pct={}, pts_17_23_pct={}, pts_24_plus_pct={} 
        }
        for col, flags in pairs(activeCols) do
            if flags.avg then extremes["avg_"..col] = {} end
            if flags.max then extremes["max_"..col] = {} end
            if flags.sum then extremes["sum_"..col] = {} end
        end
        
        local mapSet = {}
        for _, d in pairs(tData) do for m, _ in pairs(d.map_matches) do mapSet[m] = true end end
        local mapList = {}
        for m in pairs(mapSet) do table.insert(mapList, m); extremes["map_"..m] = {} end
        table.sort(mapList)

        for _, d in pairs(tData) do
            if d.matches > 0 then
                d.avg_rank = d.rank_sum / d.matches
                d.avg_place = d.place / d.matches; d.avg_elims = d.elims / d.matches; d.avg_total = d.total / d.matches
                
                d.win_pct = (d.wwcd / d.matches) * 100
                d.place_2_5_pct = (d.place_2_5 / d.matches) * 100
                d.place_6_10_pct = (d.place_6_10 / d.matches) * 100
                d.place_11_plus_pct = (d.place_11_plus / d.matches) * 100
                
                d.pts_0_pct = (d.pts_0 / d.matches) * 100
                d.disp_pts_0 = d.pts_0
                
                if cumulativePts then
                    local sum_lt_5 = d.pts_0 + d.pts_1_4
                    local sum_5 = d.pts_5_9 + d.pts_10_16 + d.pts_17_23 + d.pts_24_plus
                    local sum_10 = d.pts_10_16 + d.pts_17_23 + d.pts_24_plus
                    local sum_17 = d.pts_17_23 + d.pts_24_plus
                    local sum_24 = d.pts_24_plus
                    
                    d.pts_1_4_pct = (sum_lt_5 / d.matches) * 100
                    d.pts_5_9_pct = (sum_5 / d.matches) * 100
                    d.pts_10_16_pct = (sum_10 / d.matches) * 100
                    d.pts_17_23_pct = (sum_17 / d.matches) * 100
                    d.pts_24_plus_pct = (sum_24 / d.matches) * 100
                    
                    d.disp_pts_1_4 = sum_lt_5
                    d.disp_pts_5_9 = sum_5
                    d.disp_pts_10_16 = sum_10
                    d.disp_pts_17_23 = sum_17
                    d.disp_pts_24_plus = sum_24
                else
                    d.pts_1_4_pct = (d.pts_1_4 / d.matches) * 100
                    d.pts_5_9_pct = (d.pts_5_9 / d.matches) * 100
                    d.pts_10_16_pct = (d.pts_10_16 / d.matches) * 100
                    d.pts_17_23_pct = (d.pts_17_23 / d.matches) * 100
                    d.pts_24_plus_pct = (d.pts_24_plus / d.matches) * 100
                    
                    d.disp_pts_1_4 = d.pts_1_4
                    d.disp_pts_5_9 = d.pts_5_9
                    d.disp_pts_10_16 = d.pts_10_16
                    d.disp_pts_17_23 = d.pts_17_23
                    d.disp_pts_24_plus = d.pts_24_plus
                end

                d.tiebreaker = tonumber(args["tiebreaker_" .. d.team]) or 0
                
                if showAvgRank then table.insert(extremes.avg_rank, d.avg_rank) end
                table.insert(extremes.avg_place, d.avg_place); table.insert(extremes.avg_elims, d.avg_elims); table.insert(extremes.avg_total, d.avg_total)
                table.insert(extremes.win_pct, d.win_pct)
                table.insert(extremes.place_2_5_pct, d.place_2_5_pct); table.insert(extremes.place_6_10_pct, d.place_6_10_pct); table.insert(extremes.place_11_plus_pct, d.place_11_plus_pct)
                table.insert(extremes.pts_0_pct, d.pts_0_pct); table.insert(extremes.pts_1_4_pct, d.pts_1_4_pct); table.insert(extremes.pts_5_9_pct, d.pts_5_9_pct)
                table.insert(extremes.pts_10_16_pct, d.pts_10_16_pct); table.insert(extremes.pts_17_23_pct, d.pts_17_23_pct); table.insert(extremes.pts_24_plus_pct, d.pts_24_plus_pct)

                for col, flags in pairs(activeCols) do
                    d["avg_"..col] = d["sum_"..col] / d.matches
                    if flags.avg then table.insert(extremes["avg_"..col], d["avg_"..col]) end
                    if flags.max then table.insert(extremes["max_"..col], d["max_"..col]) end
                    if flags.sum then table.insert(extremes["sum_"..col], d["sum_"..col]) end
                end

                for _, m in ipairs(mapList) do
                    if d.map_matches[m] and d.map_matches[m] > 0 then
                        d["avg_map_"..m] = d.map_pts[m] / d.map_matches[m]
                        table.insert(extremes["map_"..m], d["avg_map_"..m])
                    end
                end
                table.insert(list, d)
            end
        end
        
        table.sort(list, function(a, b)
            if a.avg_total ~= b.avg_total then return a.avg_total > b.avg_total end
            if a.win_pct ~= b.win_pct then return a.win_pct > b.win_pct end
            if a.avg_elims ~= b.avg_elims then return a.avg_elims > b.avg_elims end
            return a.tiebreaker > b.tiebreaker
        end)
        
        local bounds = {}
        for k, vals in pairs(extremes) do if #vals > 0 then local mn, mx = getMinMax(vals); bounds[k] = { min = mn, max = mx } end end

        local th = tbl:tag('tr')
        makeTH(th, 'sticky-col sticky-1', '#', true)
        makeTH(th, 'sticky-col sticky-2 stat-team', 'Team', true)
        makeTH(th, 'stat-col', 'M')
        
        if showAvgRank then makeTH(th, 'stat-col', 'Avg Rank') end
        makeTH(th, 'stat-col', 'Avg Place Pts')
        makeTH(th, 'stat-col', 'Avg Elims')
        makeTH(th, 'stat-col', 'Avg Total')
        
        for _, col in ipairs(optColumns) do
            if activeCols[col] then
                local cName = colNames[col] or col
                if activeCols[col].avg then makeTH(th, 'stat-col', 'Avg ' .. cName) end
                if activeCols[col].max then makeTH(th, 'stat-col', 'Max ' .. cName) end
                if activeCols[col].sum then makeTH(th, 'stat-col', 'Total ' .. cName) end
            end
        end

        for _, m in ipairs(mapList) do makeTH(th, 'stat-col', m) end
        
        makeTH(th, 'stat-col', 'Win %')
        makeTH(th, 'stat-col', '2nd-5th %')
        makeTH(th, 'stat-col', '6th-10th %')
        makeTH(th, 'stat-col', '11th-16th+ %')
        
        if cumulativePts then
            makeTH(th, 'stat-col', '0 Pts')
            makeTH(th, 'stat-col', '&lt; 5 Pts')
            makeTH(th, 'stat-col', '5+ Pts')
            makeTH(th, 'stat-col', '10+ Pts')
            makeTH(th, 'stat-col', '17+ Pts')
            makeTH(th, 'stat-col', '24+ Pts')
        else
            makeTH(th, 'stat-col', '0 Pts')
            makeTH(th, 'stat-col', '1-4 Pts')
            makeTH(th, 'stat-col', '5-9 Pts')
            makeTH(th, 'stat-col', '10-16 Pts')
            makeTH(th, 'stat-col', '17-23 Pts')
            makeTH(th, 'stat-col', '24+ Pts')
        end
        
        for i = 1, math.min(#list, limit) do
            local d = list[i]
            local tr = tbl:tag('tr'):addClass('standings-row')
            tr:tag('td'):addClass('sticky-col sticky-1 dyn-rank'):css('text-align','center'):css('font-weight','800')
            
            local tD = tourneyDb[d.team:lower()]; local mD = masterDb[d.team:lower()]
            local dispName = (tD and tD.display_name and tD.display_name ~= "") and tD.display_name or d.team
            local shortName = (d.short and d.short ~= "") and d.short or dispName
            
            local teamCell = tr:tag('td'):addClass('sticky-col sticky-2 stat-team team-name-cell')
            teamCell:wikitext(buildLogo(d.team, tD, mD))
            teamCell:tag('span'):addClass('pc-only'):css('font-size','0.95em'):css('font-weight','700'):wikitext('[[' .. d.team .. '|' .. dispName .. ']]')
            teamCell:tag('span'):addClass('mobile-only'):css('font-size','0.85em'):css('font-weight','700'):wikitext('[[' .. d.team .. '|' .. shortName .. ']]')
            
            tr:tag('td'):addClass('stat-col'):wikitext(d.matches)
            
            if showAvgRank then
                tr:tag('td'):addClass('stat-col'):attr('style', getHeatmap(d.avg_rank, bounds.avg_rank.min, bounds.avg_rank.max, false)):wikitext(fmt(d.avg_rank, 1))
            end
            
            tr:tag('td'):addClass('stat-col'):attr('title', tostring(d.place)):attr('style', getHeatmap(d.avg_place, bounds.avg_place.min, bounds.avg_place.max, true)):wikitext(fmt(d.avg_place, 1))
            tr:tag('td'):addClass('stat-col'):attr('title', tostring(d.elims)):attr('style', getHeatmap(d.avg_elims, bounds.avg_elims.min, bounds.avg_elims.max, true)):wikitext(fmt(d.avg_elims, 1))
            tr:tag('td'):addClass('stat-col'):css('font-weight','800'):attr('title', tostring(d.total)):attr('style', getHeatmap(d.avg_total, bounds.avg_total.min, bounds.avg_total.max, true)):wikitext(fmt(d.avg_total, 1))
            
            for _, col in ipairs(optColumns) do
                if activeCols[col] then
                    local isGood = not inverseCols[col]; local isDist = col:find("dist"); local isSurv = col == "survival"
                    local d_avg = isDist and 2 or 1; local d_ms = isDist and 1 or 0
                    if isSurv then d_avg = 0 end 

                    local txt_avg = (isSurv and formatSurv) and formatTimeStr(d["avg_"..col]) or fmt(d["avg_"..col], d_avg)
                    local txt_max = (isSurv and formatSurv) and formatTimeStr(d["max_"..col]) or fmt(d["max_"..col], d_ms)
                    local txt_sum = (isSurv and formatSurv) and formatTimeStr(d["sum_"..col]) or fmt(d["sum_"..col], d_ms)

                    if activeCols[col].avg then tr:tag('td'):addClass('stat-col'):attr('style', getHeatmap(d["avg_"..col], bounds["avg_"..col].min, bounds["avg_"..col].max, isGood)):wikitext(txt_avg) end
                    if activeCols[col].max then tr:tag('td'):addClass('stat-col'):attr('style', getHeatmap(d["max_"..col], bounds["max_"..col].min, bounds["max_"..col].max, isGood)):wikitext(txt_max) end
                    if activeCols[col].sum then tr:tag('td'):addClass('stat-col'):attr('style', getHeatmap(d["sum_"..col], bounds["sum_"..col].min, bounds["sum_"..col].max, isGood)):wikitext(txt_sum) end
                end
            end
            
            for _, m in ipairs(mapList) do
                local mapVal = d["avg_map_"..m]; local mapCell = tr:tag('td'):addClass('stat-col')
                if mapVal then mapCell:attr('title', tostring(d.map_pts[m])):attr('style', getHeatmap(mapVal, bounds["map_"..m].min, bounds["map_"..m].max, true)):wikitext(fmt(mapVal, 1)) else mapCell:css('opacity','0.3'):wikitext('-') end
            end
            
            tr:tag('td'):addClass('stat-col'):attr('title', tostring(d.wwcd)):attr('style', getHeatmap(d.win_pct, bounds.win_pct.min, bounds.win_pct.max, true)):wikitext(fmt(d.win_pct, 0) .. '%')
            tr:tag('td'):addClass('stat-col'):attr('title', tostring(d.place_2_5)):attr('style', getHeatmap(d.place_2_5_pct, bounds.place_2_5_pct.min, bounds.place_2_5_pct.max, true)):wikitext(fmt(d.place_2_5_pct, 0) .. '%')
            tr:tag('td'):addClass('stat-col'):attr('title', tostring(d.place_6_10)):attr('style', getHeatmap(d.place_6_10_pct, bounds.place_6_10_pct.min, bounds.place_6_10_pct.max, false)):wikitext(fmt(d.place_6_10_pct, 0) .. '%')
            tr:tag('td'):addClass('stat-col'):attr('title', tostring(d.place_11_plus)):attr('style', getHeatmap(d.place_11_plus_pct, bounds.place_11_plus_pct.min, bounds.place_11_plus_pct.max, false)):wikitext(fmt(d.place_11_plus_pct, 0) .. '%')
            
            tr:tag('td'):addClass('stat-col'):attr('title', tostring(d.disp_pts_0)):attr('style', getHeatmap(d.pts_0_pct, bounds.pts_0_pct.min, bounds.pts_0_pct.max, false)):wikitext(fmt(d.pts_0_pct, 0) .. '%')
            tr:tag('td'):addClass('stat-col'):attr('title', tostring(d.disp_pts_1_4)):attr('style', getHeatmap(d.pts_1_4_pct, bounds.pts_1_4_pct.min, bounds.pts_1_4_pct.max, false)):wikitext(fmt(d.pts_1_4_pct, 0) .. '%')
            tr:tag('td'):addClass('stat-col'):attr('title', tostring(d.disp_pts_5_9)):attr('style', getHeatmap(d.pts_5_9_pct, bounds.pts_5_9_pct.min, bounds.pts_5_9_pct.max, true)):wikitext(fmt(d.pts_5_9_pct, 0) .. '%')
            tr:tag('td'):addClass('stat-col'):attr('title', tostring(d.disp_pts_10_16)):attr('style', getHeatmap(d.pts_10_16_pct, bounds.pts_10_16_pct.min, bounds.pts_10_16_pct.max, true)):wikitext(fmt(d.pts_10_16_pct, 0) .. '%')
            tr:tag('td'):addClass('stat-col'):attr('title', tostring(d.disp_pts_17_23)):attr('style', getHeatmap(d.pts_17_23_pct, bounds.pts_17_23_pct.min, bounds.pts_17_23_pct.max, true)):wikitext(fmt(d.pts_17_23_pct, 0) .. '%')
            tr:tag('td'):addClass('stat-col'):attr('title', tostring(d.disp_pts_24_plus)):attr('style', getHeatmap(d.pts_24_plus_pct, bounds.pts_24_plus_pct.min, bounds.pts_24_plus_pct.max, true)):wikitext(fmt(d.pts_24_plus_pct, 0) .. '%')
        end

    elseif statType == "player" or statType == "igl" then
        local list = {}
        local ext = { elims={}, epm={}, max_elims={}, elims_5plus={}, zero_pct={}, team_elims_pct={}, mvps={}, mvp_score={}, igl_score={}, t_avg_pts={}, t_wwcd={}, t_top5={}, t_avg_surv={} }
        for col, flags in pairs(activeCols) do
            if flags.avg then ext["avg_"..col] = {} end
            if flags.max then ext["max_"..col] = {} end
            if flags.sum then ext["sum_"..col] = {} end
        end

        for _, d in pairs(pData) do
            local keep = true
            local pRole = d.role or ""
            if filterRole and not pRole:lower():find(filterRole, 1, true) then keep = false end

            if d.matches > 0 and keep then
                d.epm = d.elims / d.matches; 
                d.zero_pct = (d.elims_0 / d.matches) * 100; 
                d.team_elims_pct = (d.t_elims > 0) and ((d.elims / d.t_elims) * 100) or 0
                d.tiebreaker = tonumber(args["tiebreaker_" .. d.player]) or 0
                
                if showMvpScore then
                    local score = 0
                    if g.surv > 0 then score = score + (d.t_surv / g.surv) * 0.2 end
                    if g.dmg > 0 then score = score + (d.t_dmg / g.dmg) * 0.3 end
                    if g.elims > 0 then score = score + (d.elims / g.elims) * 0.4 end
                    if g.knocks > 0 then score = score + (d.t_knocks / g.knocks) * 0.1 end
                    d.mvp_score = score * 100
                    table.insert(ext.mvp_score, d.mvp_score)
                end
                
                if showIglScore or statType == "igl" then
                    local tStats = teamStatsForIgl[d.team]
                    if tStats and tStats.matches > 0 and g_igl.t_matches > 0 and g_igl.p_matches > 0 then
                        d.t_avg_pts = tStats.pts / tStats.matches
                        d.t_wwcd = tStats.wwcd
                        d.t_top5 = tStats.top5
                        d.t_avg_surv = tStats.surv / tStats.matches
                        
                        local global_team_avg_pts = g_igl.t_pts / g_igl.t_matches
                        local pt_ratio = (global_team_avg_pts > 0) and (d.t_avg_pts / global_team_avg_pts) or 0
                        
                        local wwcd_ratio = tStats.wwcd / tStats.matches
                        
                        local global_avg_top5 = g_igl.t_top5 / g_igl.t_matches
                        local top5_ratio = (global_avg_top5 > 0) and ((tStats.top5 / tStats.matches) / global_avg_top5) or 0
                        
                        local global_avg_surv = g_igl.t_surv / g_igl.t_matches
                        local surv_ratio = (global_avg_surv > 0) and (d.t_avg_surv / global_avg_surv) or 0
                        
                        local team_score = (pt_ratio * 0.3) + (wwcd_ratio * 0.3) + (top5_ratio * 0.3) + (surv_ratio * 0.1)
                        
                        local p_avg_elims = d.elims / d.matches
                        local g_avg_elims = g_igl.p_elims / g_igl.p_matches
                        local elim_ratio = (g_avg_elims > 0) and (p_avg_elims / g_avg_elims) or 0
                        
                        local p_avg_dmg = d.t_dmg / d.matches
                        local g_avg_dmg = g_igl.p_dmg / g_igl.p_matches
                        local dmg_ratio = (g_avg_dmg > 0) and (p_avg_dmg / g_avg_dmg) or 0
                        
                        local player_score = (elim_ratio * 0.5) + (dmg_ratio * 0.5)
                        
                        d.igl_score = ((0.7 * team_score) + (0.3 * player_score)) * 100
                    else
                        d.t_avg_pts = 0; d.t_wwcd = 0; d.t_top5 = 0; d.t_avg_surv = 0; d.igl_score = 0
                    end
                    
                    if statType == "igl" then
                        table.insert(ext.t_avg_pts, d.t_avg_pts)
                        table.insert(ext.t_wwcd, d.t_wwcd)
                        table.insert(ext.t_top5, d.t_top5)
                        table.insert(ext.t_avg_surv, d.t_avg_surv)
                        table.insert(ext.igl_score, d.igl_score)
                    end
                end

                table.insert(ext.elims, d.elims); table.insert(ext.epm, d.epm); table.insert(ext.max_elims, d.max_elims)
                table.insert(ext.elims_5plus, d.elims_5plus); table.insert(ext.zero_pct, d.zero_pct); table.insert(ext.mvps, d.mvps)
                table.insert(ext.team_elims_pct, d.team_elims_pct)
                
                for col, flags in pairs(activeCols) do
                    d["avg_"..col] = d["sum_"..col] / d.matches
                    if flags.avg then table.insert(ext["avg_"..col], d["avg_"..col]) end
                    if flags.max then table.insert(ext["max_"..col], d["max_"..col]) end
                    if flags.sum then table.insert(ext["sum_"..col], d["sum_"..col]) end
                end
                
                table.insert(list, d)
            end
        end
        
        table.sort(list, function(a, b)
            if statType == "igl" then
                if a.igl_score ~= b.igl_score then return a.igl_score > b.igl_score end
            else
                if showMvpScore and a.mvp_score ~= b.mvp_score then return a.mvp_score > b.mvp_score end
                if a.elims ~= b.elims then return a.elims > b.elims end
                if a.epm ~= b.epm then return a.epm > b.epm end
                if a.max_elims ~= b.max_elims then return a.max_elims > b.max_elims end
            end
            return a.tiebreaker > b.tiebreaker
        end)
        
        local bounds = {}
        for k, vals in pairs(ext) do if #vals > 0 then local mn, mx = getMinMax(vals); bounds[k] = { min = mn, max = mx } end end

        local th = tbl:tag('tr')
        makeTH(th, 'sticky-col sticky-1', '#', true)
        makeTH(th, 'sticky-col sticky-logo', '', true)
        makeTH(th, 'sticky-col sticky-player stat-team', 'Player', true)
        makeTH(th, 'stat-col', 'M')
        
        if statType == "igl" then
            makeTH(th, 'stat-col', 'IGL Rating')
            makeTH(th, 'stat-col', 'Team Avg Pts')
            makeTH(th, 'stat-col', 'WWCDs')
            makeTH(th, 'stat-col', 'Top Fives')
            makeTH(th, 'stat-col', 'Team Avg Surv')
            
            if args.show_elims == "true" then makeTH(th, 'stat-col', 'Elims') end
            if args.show_epm == "true" or args.show_fpm == "true" then makeTH(th, 'stat-col', 'EPM') end
        else
            if showIglScore then makeTH(th, 'stat-col', 'IGL Rating') end
            if showMvpScore then makeTH(th, 'stat-col', 'MVP Rating') end
            makeTH(th, 'stat-col', 'Elims')
            makeTH(th, 'stat-col', 'EPM')
        end
        
        for _, col in ipairs(optColumns) do
            if activeCols[col] then
                local cName = colNames[col] or col
                if activeCols[col].avg then makeTH(th, 'stat-col', 'Avg ' .. cName) end
                if activeCols[col].max then makeTH(th, 'stat-col', 'Max ' .. cName) end
                if activeCols[col].sum then makeTH(th, 'stat-col', 'Total ' .. cName) end
            end
        end

        if statType == "igl" then
            if args.show_max_elims == "true" then makeTH(th, 'stat-col', 'Max Elims') end
            if args.show_elims_5plus == "true" then makeTH(th, 'stat-col', '5+ Elims') end
            if args.show_zero_pct == "true" then makeTH(th, 'stat-col', '0 Elims %') end
            if args.show_team_elims_pct == "true" then makeTH(th, 'stat-col', '% Team Elims') end
        else
            makeTH(th, 'stat-col', 'Max Elims')
            makeTH(th, 'stat-col', '5+ Elims')
            makeTH(th, 'stat-col', '0 Elims %')
            if args.show_team_elims_pct == "true" then makeTH(th, 'stat-col', '% Team Elims') end
            if showMvps then makeTH(th, 'stat-col', 'Match MVPs') end
        end
        
        for i = 1, math.min(#list, limit) do
            local d = list[i]
            local tr = tbl:tag('tr'):addClass('standings-row')
            
            tr:tag('td'):addClass('sticky-col sticky-1 dyn-rank'):css('text-align','center'):css('font-weight','800')
            
            local tD = tourneyDb[d.team:lower()]; local mD = masterDb[d.team:lower()]
            local dispTeamName = (tD and tD.display_name and tD.display_name ~= "") and tD.display_name or d.team
            
            local logoCell = tr:tag('td'):addClass('sticky-col sticky-logo')
            logoCell:attr('data-sort-value', d.team):wikitext(buildLogo(d.team, tD, mD))
            
            local pCell = tr:tag('td'):addClass('sticky-col sticky-player stat-team team-name-cell')
            local infoDiv = pCell:tag('div'):css('line-height','1.1')
            
            infoDiv:tag('div'):addClass('player-name-txt'):wikitext('[[' .. d.player .. ']]')
            infoDiv:tag('div'):addClass('pc-only'):css('font-size','0.7em'):css('color','var(--text-muted)'):css('text-transform','uppercase'):css('font-weight','700'):css('margin-top','3px'):wikitext('[[' .. d.team .. '|' .. dispTeamName .. ']]')
            
            tr:tag('td'):addClass('stat-col'):wikitext(d.matches)
            
            if statType == "igl" then
                tr:tag('td'):addClass('stat-col'):css('font-weight','800'):attr('style', getHeatmap(d.igl_score, bounds.igl_score.min, bounds.igl_score.max, true)):wikitext(fmt(d.igl_score, 2))
                tr:tag('td'):addClass('stat-col'):attr('style', getHeatmap(d.t_avg_pts, bounds.t_avg_pts.min, bounds.t_avg_pts.max, true)):wikitext(fmt(d.t_avg_pts, 1))
                tr:tag('td'):addClass('stat-col'):attr('style', getHeatmap(d.t_wwcd, bounds.t_wwcd.min, bounds.t_wwcd.max, true)):wikitext(d.t_wwcd)
                tr:tag('td'):addClass('stat-col'):attr('style', getHeatmap(d.t_top5, bounds.t_top5.min, bounds.t_top5.max, true)):wikitext(d.t_top5)
                
                local txt_t_avg_surv = formatSurv and formatTimeStr(d.t_avg_surv) or fmt(d.t_avg_surv, 0)
                tr:tag('td'):addClass('stat-col'):attr('style', getHeatmap(d.t_avg_surv, bounds.t_avg_surv.min, bounds.t_avg_surv.max, true)):wikitext(txt_t_avg_surv)

                if args.show_elims == "true" then tr:tag('td'):addClass('stat-col'):css('font-weight','800'):attr('style', getHeatmap(d.elims, bounds.elims.min, bounds.elims.max, true)):wikitext(d.elims) end
                
                if args.show_epm == "true" or args.show_fpm == "true" then tr:tag('td'):addClass('stat-col'):attr('style', getHeatmap(d.epm, bounds.epm.min, bounds.epm.max, true)):wikitext(fmt(d.epm, 2)) end
            else
                if showIglScore then 
                    tr:tag('td'):addClass('stat-col'):css('font-weight','800'):attr('style', getHeatmap(d.igl_score, bounds.igl_score.min, bounds.igl_score.max, true)):wikitext(fmt(d.igl_score, 2))
                end
                
                if showMvpScore then 
                    tr:tag('td'):addClass('stat-col'):css('font-weight','800'):attr('style', getHeatmap(d.mvp_score, bounds.mvp_score.min, bounds.mvp_score.max, true)):wikitext(fmt(d.mvp_score, 2))
                end

                tr:tag('td'):addClass('stat-col'):css('font-weight','800'):attr('style', getHeatmap(d.elims, bounds.elims.min, bounds.elims.max, true)):wikitext(d.elims)
                
                tr:tag('td'):addClass('stat-col'):attr('style', getHeatmap(d.epm, bounds.epm.min, bounds.epm.max, true)):wikitext(fmt(d.epm, 2))
            end
            
            for _, col in ipairs(optColumns) do
                if activeCols[col] then
                    local isGood = not inverseCols[col]; local isDist = col:find("dist"); local isSurv = col == "survival"
                    local d_avg = isDist and 2 or 1; local d_ms = isDist and 1 or 0
                    if isSurv then d_avg = 0 end 

                    local txt_avg = (isSurv and formatSurv) and formatTimeStr(d["avg_"..col]) or fmt(d["avg_"..col], d_avg)
                    local txt_max = (isSurv and formatSurv) and formatTimeStr(d["max_"..col]) or fmt(d["max_"..col], d_ms)
                    local txt_sum = (isSurv and formatSurv) and formatTimeStr(d["sum_"..col]) or fmt(d["sum_"..col], d_ms)

                    if activeCols[col].avg then tr:tag('td'):addClass('stat-col'):attr('style', getHeatmap(d["avg_"..col], bounds["avg_"..col].min, bounds["avg_"..col].max, isGood)):wikitext(txt_avg) end
                    if activeCols[col].max then tr:tag('td'):addClass('stat-col'):attr('style', getHeatmap(d["max_"..col], bounds["max_"..col].min, bounds["max_"..col].max, isGood)):wikitext(txt_max) end
                    if activeCols[col].sum then tr:tag('td'):addClass('stat-col'):attr('style', getHeatmap(d["sum_"..col], bounds["sum_"..col].min, bounds["sum_"..col].max, isGood)):wikitext(txt_sum) end
                end
            end
            
            if statType == "igl" then
                if args.show_max_elims == "true" then tr:tag('td'):addClass('stat-col'):attr('style', getHeatmap(d.max_elims, bounds.max_elims.min, bounds.max_elims.max, true)):wikitext(d.max_elims) end
                if args.show_elims_5plus == "true" then tr:tag('td'):addClass('stat-col'):attr('style', getHeatmap(d.elims_5plus, bounds.elims_5plus.min, bounds.elims_5plus.max, true)):wikitext(d.elims_5plus) end
                if args.show_zero_pct == "true" then tr:tag('td'):addClass('stat-col'):attr('title', tostring(d.elims_0)):attr('style', getHeatmap(d.zero_pct, bounds.zero_pct.min, bounds.zero_pct.max, false)):wikitext(fmt(d.zero_pct, 0) .. '%') end
                
                if args.show_team_elims_pct == "true" then
                    tr:tag('td'):addClass('stat-col'):attr('title', d.elims .. ' / ' .. d.t_elims):attr('style', getHeatmap(d.team_elims_pct, bounds.team_elims_pct.min, bounds.team_elims_pct.max, true)):wikitext(fmt(d.team_elims_pct, 0) .. '%')
                end
            else
                tr:tag('td'):addClass('stat-col'):attr('style', getHeatmap(d.max_elims, bounds.max_elims.min, bounds.max_elims.max, true)):wikitext(d.max_elims)
                tr:tag('td'):addClass('stat-col'):attr('style', getHeatmap(d.elims_5plus, bounds.elims_5plus.min, bounds.elims_5plus.max, true)):wikitext(d.elims_5plus)
                tr:tag('td'):addClass('stat-col'):attr('title', tostring(d.elims_0)):attr('style', getHeatmap(d.zero_pct, bounds.zero_pct.min, bounds.zero_pct.max, false)):wikitext(fmt(d.zero_pct, 0) .. '%')
                
                if args.show_team_elims_pct == "true" then
                    tr:tag('td'):addClass('stat-col'):attr('title', d.elims .. ' / ' .. d.t_elims):attr('style', getHeatmap(d.team_elims_pct, bounds.team_elims_pct.min, bounds.team_elims_pct.max, true)):wikitext(fmt(d.team_elims_pct, 0) .. '%')
                end
                
                if showMvps then tr:tag('td'):addClass('stat-col'):attr('style', getHeatmap(d.mvps, bounds.mvps.min, bounds.mvps.max, true)):wikitext(d.mvps) end
            end
        end
    end
    
    return tostring(root)
end

return p