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

Module:Statistics: Difference between revisions

From eSportsAmaze
No edit summary
No edit summary
 
(46 intermediate revisions by 2 users not shown)
Line 2: Line 2:
local cargo = mw.ext.cargo
local cargo = mw.ext.cargo
local html = mw.html
local html = mw.html
local text = mw.text


-- ============================================================
local function sqlEscape(s)
-- CONFIGURATION
    if not s then return "" end
-- ============================================================
    return s:gsub("\\", "\\\\"):gsub("'", "\\'")
-- Heatmap Color (Blue: 59, 130, 246)
end
local HEATMAP_R, HEATMAP_G, HEATMAP_B = 59, 130, 246
 
-- Smart Logo Builder (ZERO Expensive Calls)
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 HEADERS = {
    local container = html.create('span'):addClass('team-logo-wrapper')
    -- BASICS
     container:tag('span'):addClass('logo-lightmode'):wikitext('[[File:' .. lightFile .. '|24px|link=|class=team-logo]]')
    rank = "#",
     container:tag('span'):addClass('logo-darkmode'):wikitext('[[File:' .. darkFile .. '|24px|link=|class=team-logo]]')
    team = "Team",
     return tostring(container)
    player = "Player",
end
    matches_played = "Matches",
   
    -- KILLS / OFFENSE
    finishes = "Finishes",
    fpm = "FPM",
    knocks = "Knocks",
    damage = "Damage",
    headshots = "Headshots",
    longest = "Longest",
    assists = "Assists",
    grenade_kills = "Grenade Kills",
    vehicle_kills = "Vehicle Kills",
    contribution = "Contrib %",
   
     -- SURVIVAL / SUPPORT
    survival = "Surv. Time",
    healings = "Heals",
    revives = "Revives",
    damage_taken = "Dmg Recv",
   
    -- UTILITY
    grenades_used = "Nades Used",
    smokes_used = "Smokes Used",
    utility_used = "Util Used",
   
     -- MOVEMENT
    dist_drive = "Drive Dist",
    dist_walk = "Walk Dist",
    dist_total = "Total Dist",
    bluezone = "Bluezone Time",
    air_drops = "Air Drops",
   
    -- TEAM SPECIFIC
    total_pts = "Total Pts",
     place_pts = "Place Pts",
    elims = "Elims",
   
    -- AVERAGES (Team)
    avg_place = "Avg Place",
    avg_place_pts = "Avg Place Pts",
    avg_elims = "Avg Elims",
    avg_total = "Avg Pts",
   
    -- PLACEMENT COUNTS
    wwcd = "🥇",
    place_2 = "🥈",
    place_3 = "🥉",
    top_5 = "Top 5",
    top_8 = "Top 8",
    place_low = "> 8th",
   
    -- POINT BUCKETS
    g_0 = "0",
    g_1_5 = "1–5",
    g_6_10 = "6–10",
    g_11_15 = "11–15",
    g_16_20 = "16–20",
    g_20_plus = "20+"
}


-- Columns that should NOT get a heatmap background
local function fmt(num, decimals)
local NO_HEATMAP = { rank=true, team=true, player=true, matches_played=true }
    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)
-- HELPER: Heatmap Style
     if not val or not min or not max or max == min then return "" end
-- ============================================================
     local ratio = (val - min) / (max - min)
local function getHeatmapStyle(value, maxVal)
     if ratio < 0 then ratio = 0 end
     if not tonumber(value) or not tonumber(maxVal) or maxVal == 0 then return "" end
    if ratio > 1 then ratio = 1 end
     local v = tonumber(value)
     local opacity = 0.02 + (ratio * 0.23)
     local ratio = v / maxVal
     if isGood then return string.format("background-color: rgba(var(--heatmap-good), %.2f);", opacity)
     local alpha = 0.05 + (ratio * 0.45)  
    else return string.format("background-color: rgba(var(--heatmap-bad), %.2f);", opacity) end
     return string.format("background-color: rgba(%d, %d, %d, %.2f);", HEATMAP_R, HEATMAP_G, HEATMAP_B, alpha)
end
end


-- ============================================================
local function getMinMax(t)
-- HELPER: Get Team Logo
     if not t or #t == 0 then return 0, 0 end
-- ============================================================
     local mn, mx = t[1], t[1]
local function getTeamLogo(teamName)
     for i=2, #t do
     if not teamName then return "" end
        if t[i] < mn then mn = t[i] end
     local cleanName = teamName:gsub("'", "")
         if t[i] > mx then mx = t[i] end
     local lightFile = cleanName .. '.png'
    local darkFile = cleanName .. '_dark.png'
   
    local hasLight = mw.title.new('File:' .. lightFile).exists
    local hasDark = mw.title.new('File:' .. darkFile).exists
   
    local html = ""
    -- Light Mode
    if hasLight then
        html = html .. '[[File:' .. lightFile .. '|25px|link=' .. teamName .. '|class=logo-lightmode]]'
    else
         html = html .. '[[File:Shield_team.png|25px|link=' .. teamName .. '|class=logo-lightmode]]'
     end
     end
     -- Dark Mode
     return mn, mx
     if hasDark then
end
        html = html .. '[[File:' .. darkFile .. '|25px|link=' .. teamName .. '|class=logo-darkmode]]'
 
     elseif hasLight then
local function parseTime(s)
        html = html .. '[[File:' .. lightFile .. '|25px|link=' .. teamName .. '|class=logo-darkmode]]'
     if not s or s == "" then return 0 end
     else
    local parts = {}
        html = html .. '[[File:Shield_team_dark.png|25px|link=' .. teamName .. '|class=logo-darkmode]]'
    for p in s:gmatch("%d+") do table.insert(parts, tonumber(p)) end
     end
    if #parts == 1 then return parts[1] end
     return html .. " "
     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
end


-- ============================================================
-- MAIN GENERATOR
-- ============================================================
function p.main(frame)
function p.main(frame)
     local args = frame:getParent().args
     local args = frame:getParent().args
     local type = args.type or "player"
     if not args.type then args = frame.args end
    local tournament = args.tournament or mw.title.getCurrentTitle().subpageText
    local map = args.map or "All"
      
      
     -- 1. Determine Columns to Show
     local statType = (args.type or "team"):lower()
     local colsInput = args.columns or ""
     local tournamentName = args.tournament or ""
     local colKeys = {}
     local limit = tonumber(args.limit) or (statType == "player" and 100 or 500)
      
      
     if colsInput == "" then
     local optColumns = {
         if type == "player" then
        "damage", "rdamage", "headshots", "assists", "knockouts", "long_elim",
            colKeys = {"rank", "player", "team", "matches_played", "finishes", "damage"}
         "vehicle_elim", "grenade_elim", "smokes", "grenades", "molotovs", "flash",
         else
        "utilities", "airdrops", "rescues", "dist_drove", "dist_walk", "total_dist", "survival"
            colKeys = {"rank", "team", "matches_played", "total_pts", "wwcd"}
    }
         end
 
    else
    local colNames = {
         colKeys = text.split(colsInput, ",")
         damage = "Dmg", rdamage = "Dmg Recv", headshots = "Headshots", assists = "Assists",
     end
        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 }
      
      
    -- 2. Build Query Fields (Exclude 'rank' from SQL)
     local activeCols = {}
     local queryFields = {}
     for _, col in ipairs(optColumns) do
     for _, k in ipairs(colKeys) do
         local showAvg = args["show_avg_" .. col] == "true"
         local cleanK = k:match("^%s*(.-)%s*$")
        local showMax = args["show_max_" .. col] == "true"
         if cleanK ~= "rank" then -- Don't ask DB for 'rank'
        local showSum = args["show_sum_" .. col] == "true"
            table.insert(queryFields, cleanK)
        if col == "damage" and args.show_damage == "true" then showAvg = true end
         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
     end
      
      
     local table_name = (type == "player") and "Player_Stats" or "Team_Stats"
     local showMvps = args.show_mvps == "true"
     local fieldsSQL = table.concat(queryFields, ",")
    local showMvpScore = args.show_mvp_score == "true"
     local where = string.format("tournament='%s' AND map='%s'", tournament:gsub("'", "\\'"), map)
     local showAvgRank = args.show_avg_rank == "true"
     local formatSurv = (args.survival_format == "mm:ss" or args.survival_format == "mmss" or args.survival_format == "time")
      
      
    -- 3. Fetch Data
     local neededForScore = { damage = true, knockouts = true, survival = true }
     local results = cargo.query(table_name, fieldsSQL .. ", team", {
        where = where,
        orderBy = (type == "player" and "finishes DESC" or "total_pts DESC"),
        limit = 100
    })
      
      
     if not results or #results == 0 then
     local whereParts = {}
        return '<div style="padding:20px; color:#666;">No statistics available for ' .. map .. '.</div>'
    local filters = {"tournament", "tournament_type", "stage", "group_name", "map", "match_type", "tournament_day", "date", "time", "team", "player"}
    end
     for _, f in ipairs(filters) do
   
         local val = args[f] or args[f:gsub("_name", "")]
    -- 4. Find Max Values
        if val and val ~= "" then
    local maxValues = {}
             if val:find(",") then
     for _, row in ipairs(results) do
                local inVals = {}
         for _, key in ipairs(queryFields) do
                for v in val:gmatch("[^,]+") do table.insert(inVals, "'" .. sqlEscape(v:match("^%s*(.-)%s*$")) .. "'") end
            local val = tonumber(row[key]) or 0
                table.insert(whereParts, string.format("%s IN (%s)", f, table.concat(inVals, ",")))
             if val > (maxValues[key] or 0) then maxValues[key] = val end
            else
                table.insert(whereParts, string.format("%s = '%s'", f, sqlEscape(val)))
            end
         end
         end
     end
     end
    local whereClause = #whereParts > 0 and table.concat(whereParts, " AND ") or "1=1"
      
      
    -- 5. Build Table
     local root = html.create('div'):addClass('standings-wrapper')
     local root = html.create('div'):addClass('stats-table-wrapper')
     local tbl = root:tag('table'):addClass('standings-card-table stats-card-table sortable')
     local tbl = root:tag('table'):addClass('wikitable flat-table sortable stats-table')
 
    local tData = {}; local pData = {}; local uniqueTeams = {}
    local g = { surv = 0, dmg = 0, elims = 0, knocks = 0 }
 
    local tSelect = "team, short_name, map, rank, wwcd, place_pts, elim_pts, total_pts, number_of_teams"
    local pSelect = "player, team, player_elims, mvp"
      
      
    -- Header Row
     local pSet = { player=true, team=true, player_elims=true, mvp=true }
     local trHead = tbl:tag('tr')
     for col, _ in pairs(activeCols) do
     for _, key in ipairs(colKeys) do
         tSelect = tSelect .. ", " .. col
         local label = HEADERS[key:match("^%s*(.-)%s*$")] or key:upper()
        if not pSet[col] then pSelect = pSelect .. ", " .. col; pSet[col] = true end
         trHead:tag('th'):wikitext(label)
    end
    if showMvpScore then
        for col, _ in pairs(neededForScore) do
            if not pSet[col] then pSelect = pSelect .. ", " .. col; pSet[col] = true end
         end
     end
     end
      
 
    -- Data Rows
     if statType == "team" then
    for i, row in ipairs(results) do
        local results = cargo.query("MatchStats_Team", tSelect, { where = whereClause, limit = 5000 })
         local tr = tbl:tag('tr')
        if not results or #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, top5 = 0, bot5 = 0, pts_0_5 = 0, pts_6_12 = 0, pts_13_20 = 0, pts_21_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
           
            local totalTeams = tonumber(r.number_of_teams) or 16
            if rank <= 5 then d.top5 = d.top5 + 1 end
            if rank >= (totalTeams - 4) then d.bot5 = d.bot5 + 1 end
           
            if pts <= 5 then d.pts_0_5 = d.pts_0_5 + 1
            elseif pts <= 12 then d.pts_6_12 = d.pts_6_12 + 1
            elseif pts <= 20 then d.pts_13_20 = d.pts_13_20 + 1
            else d.pts_21_plus = d.pts_21_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" then
         local results = cargo.query("MatchStats_Player", pSelect, { where = whereClause, limit = 5000 })
        if not results or #results == 0 then return '<div style="padding:20px; color:var(--text-muted); font-style:italic;">No player statistics available.</div>' end
          
          
         for _, key in ipairs(colKeys) do
         for _, r in ipairs(results) do
             local cleanKey = key:match("^%s*(.-)%s*$")
             local p = r.player
             local cell = tr:tag('td')
            uniqueTeams[r.team] = true
            if not pData[p] then
                pData[p] = { player = p, team = r.team, matches = 0, 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
              
              
             if cleanKey == "rank" then
             local d = pData[p]
                 -- Auto-generate Rank (1, 2, 3...)
            d.matches = d.matches + 1
                cell:wikitext(i .. '.')  
            local e = tonumber(r.player_elims or 0)
                cell:css("font-weight", "bold")
            d.elims = d.elims + e
                cell:css("text-align", "center")
            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.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
           
            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
 
    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={}, top5_pct={}, bot5_pct={}, pts_0_5_pct={}, pts_6_12_pct={}, pts_13_20_pct={}, pts_21_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.top5_pct = (d.top5 / d.matches) * 100; d.bot5_pct = (d.bot5 / d.matches) * 100
                d.pts_0_5_pct = (d.pts_0_5 / d.matches) * 100; d.pts_6_12_pct = (d.pts_6_12 / d.matches) * 100;
                d.pts_13_20_pct = (d.pts_13_20 / d.matches) * 100; d.pts_21_plus_pct = (d.pts_21_plus / d.matches) * 100
                d.tiebreaker = tonumber(args["tiebreaker_" .. d.team]) or 0
                  
                  
             elseif cleanKey == "team" then
                if showAvgRank then table.insert(extremes.avg_rank, d.avg_rank) end
                 local teamName = row.team or ""
                table.insert(extremes.avg_place, d.avg_place); table.insert(extremes.avg_elims, d.avg_elims); table.insert(extremes.avg_total, d.avg_total)
                cell:wikitext(getTeamLogo(teamName) .. '[[' .. teamName .. ']]')
                table.insert(extremes.win_pct, d.win_pct); table.insert(extremes.top5_pct, d.top5_pct); table.insert(extremes.bot5_pct, d.bot5_pct)
                 cell:css("text-align", "left")
                table.insert(extremes.pts_0_5_pct, d.pts_0_5_pct); table.insert(extremes.pts_6_12_pct, d.pts_6_12_pct);
                 cell:css("white-space", "nowrap")
                table.insert(extremes.pts_13_20_pct, d.pts_13_20_pct); table.insert(extremes.pts_21_plus_pct, d.pts_21_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')
        th:tag('th'):addClass('sticky-col sticky-1'):wikitext('#')
        th:tag('th'):addClass('sticky-col sticky-2 stat-team'):wikitext('Team')
        th:tag('th'):addClass('stat-col'):wikitext('M')
       
        if showAvgRank then th:tag('th'):addClass('stat-col'):wikitext('Avg Rank') end
        th:tag('th'):addClass('stat-col'):wikitext('Avg Place Pts')
        th:tag('th'):addClass('stat-col'):wikitext('Avg Elims')
        th:tag('th'):addClass('stat-col'):wikitext('Avg Total')
       
        for _, col in ipairs(optColumns) do
            if activeCols[col] then
                local cName = colNames[col] or col
                if activeCols[col].avg then th:tag('th'):addClass('stat-col'):wikitext('Avg ' .. cName) end
                if activeCols[col].max then th:tag('th'):addClass('stat-col'):wikitext('Max ' .. cName) end
                 if activeCols[col].sum then th:tag('th'):addClass('stat-col'):wikitext('Total ' .. cName) end
            end
        end
 
        for _, m in ipairs(mapList) do th:tag('th'):addClass('stat-col'):wikitext(m) end
        th:tag('th'):addClass('stat-col'):wikitext('Win %')
        th:tag('th'):addClass('stat-col'):wikitext('Top 5 %')
        th:tag('th'):addClass('stat-col'):wikitext('Bot 5 %')
        th:tag('th'):addClass('stat-col'):wikitext('0-5 Pts')
        th:tag('th'):addClass('stat-col'):wikitext('6-12 Pts')
        th:tag('th'):addClass('stat-col'):wikitext('13-20 Pts')
        th:tag('th'):addClass('stat-col'):wikitext('21+ Pts')
       
        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('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('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('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('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('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('style', getHeatmap(d.top5_pct, bounds.top5_pct.min, bounds.top5_pct.max, true)):wikitext(fmt(d.top5_pct, 0) .. '%')
            tr:tag('td'):addClass('stat-col'):attr('style', getHeatmap(d.bot5_pct, bounds.bot5_pct.min, bounds.bot5_pct.max, false)):wikitext(fmt(d.bot5_pct, 0) .. '%')
            tr:tag('td'):addClass('stat-col'):attr('style', getHeatmap(d.pts_0_5_pct, bounds.pts_0_5_pct.min, bounds.pts_0_5_pct.max, false)):wikitext(fmt(d.pts_0_5_pct, 0) .. '%')
            tr:tag('td'):addClass('stat-col'):attr('style', getHeatmap(d.pts_6_12_pct, bounds.pts_6_12_pct.min, bounds.pts_6_12_pct.max, true)):wikitext(fmt(d.pts_6_12_pct, 0) .. '%')
            tr:tag('td'):addClass('stat-col'):attr('style', getHeatmap(d.pts_13_20_pct, bounds.pts_13_20_pct.min, bounds.pts_13_20_pct.max, true)):wikitext(fmt(d.pts_13_20_pct, 0) .. '%')
            tr:tag('td'):addClass('stat-col'):attr('style', getHeatmap(d.pts_21_plus_pct, bounds.pts_21_plus_pct.min, bounds.pts_21_plus_pct.max, true)):wikitext(fmt(d.pts_21_plus_pct, 0) .. '%')
        end
 
    elseif statType == "player" then
        local list = {}
        local ext = { elims={}, fpm={}, max_elims={}, elims_5plus={}, zero_pct={}, mvps={}, mvp_score={} }
        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
            if d.matches > 0 then
                d.fpm = d.elims / d.matches; d.zero_pct = (d.elims_0 / d.matches) * 100; d.tiebreaker = tonumber(args["tiebreaker_" .. d.player]) or 0
                  
                  
            elseif cleanKey == "player" then
                if showMvpScore then
                cell:wikitext('[[' .. (row.player or "") .. ']]')
                    local score = 0
                 cell:css("text-align", "left")
                    if g.surv > 0 then score = score + (d.t_surv / g.surv) * 0.2 end
                 cell:css("font-weight", "bold")
                    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
 
                table.insert(ext.elims, d.elims); table.insert(ext.fpm, d.fpm); 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)
                  
                  
            else
                 for col, flags in pairs(activeCols) do
                 -- Numeric Data
                    d["avg_"..col] = d["sum_"..col] / d.matches
                local rawVal = row[cleanKey]
                    if flags.avg then table.insert(ext["avg_"..col], d["avg_"..col]) end
                cell:wikitext(rawVal)
                    if flags.max then table.insert(ext["max_"..col], d["max_"..col]) end
                cell:css("text-align", "center")
                    if flags.sum then table.insert(ext["sum_"..col], d["sum_"..col]) end
                end
                  
                  
                 -- Heatmap
                 table.insert(list, d)
                 if not NO_HEATMAP[cleanKey] then
            end
                     local style = getHeatmapStyle(rawVal, maxValues[cleanKey])
        end
                     if style ~= "" then
       
                        cell:attr("style", style .. " text-align:center;")
        table.sort(list, function(a, b)
                     end
            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.fpm ~= b.fpm then return a.fpm > b.fpm end
            if a.max_elims ~= b.max_elims then return a.max_elims > b.max_elims 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')
        th:tag('th'):addClass('sticky-col sticky-1'):wikitext('#')
        th:tag('th'):addClass('sticky-col sticky-logo'):wikitext('Team')
        th:tag('th'):addClass('sticky-col sticky-player stat-team'):wikitext('Player')
        th:tag('th'):addClass('stat-col'):wikitext('M')
       
        if showMvpScore then th:tag('th'):addClass('stat-col'):wikitext('MVP Rating') end
 
        th:tag('th'):addClass('stat-col'):wikitext('Elims')
        th:tag('th'):addClass('stat-col'):wikitext('FPM')
       
        for _, col in ipairs(optColumns) do
            if activeCols[col] then
                local cName = colNames[col] or col
                 if activeCols[col].avg then th:tag('th'):addClass('stat-col'):wikitext('Avg ' .. cName) end
                if activeCols[col].max then th:tag('th'):addClass('stat-col'):wikitext('Max ' .. cName) end
                if activeCols[col].sum then th:tag('th'):addClass('stat-col'):wikitext('Total ' .. cName) end
            end
        end
 
        th:tag('th'):addClass('stat-col'):wikitext('Max Elims')
        th:tag('th'):addClass('stat-col'):wikitext('5+ Elims')
        th:tag('th'):addClass('stat-col'):wikitext('0 Elims %')
        if showMvps then th:tag('th'):addClass('stat-col'):wikitext('Match MVPs') 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 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.fpm, bounds.fpm.min, bounds.fpm.max, true)):wikitext(fmt(d.fpm, 2))
           
            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
             end
             end
           
            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('style', getHeatmap(d.zero_pct, bounds.zero_pct.min, bounds.zero_pct.max, false)):wikitext(fmt(d.zero_pct, 0) .. '%')
            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
     end

Latest revision as of 14:08, 22 May 2026

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

-- Smart Logo Builder (ZERO Expensive Calls)
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 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 not max 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
    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 optColumns = {
        "damage", "rdamage", "headshots", "assists", "knockouts", "long_elim",
        "vehicle_elim", "grenade_elim", "smokes", "grenades", "molotovs", "flash",
        "utilities", "airdrops", "rescues", "dist_drove", "dist_walk", "total_dist", "survival"
    }

    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 formatSurv = (args.survival_format == "mm:ss" or args.survival_format == "mmss" or args.survival_format == "time")
    
    local neededForScore = { damage = true, knockouts = true, survival = true }
    
    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')
    local tbl = root:tag('table'):addClass('standings-card-table stats-card-table sortable')

    local tData = {}; local pData = {}; local uniqueTeams = {}
    local g = { surv = 0, dmg = 0, elims = 0, knocks = 0 }

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

    if statType == "team" then
        local results = cargo.query("MatchStats_Team", tSelect, { where = whereClause, limit = 5000 })
        if not results or #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, top5 = 0, bot5 = 0, pts_0_5 = 0, pts_6_12 = 0, pts_13_20 = 0, pts_21_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
            
            local totalTeams = tonumber(r.number_of_teams) or 16
            if rank <= 5 then d.top5 = d.top5 + 1 end
            if rank >= (totalTeams - 4) then d.bot5 = d.bot5 + 1 end
            
            if pts <= 5 then d.pts_0_5 = d.pts_0_5 + 1 
            elseif pts <= 12 then d.pts_6_12 = d.pts_6_12 + 1 
            elseif pts <= 20 then d.pts_13_20 = d.pts_13_20 + 1 
            else d.pts_21_plus = d.pts_21_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" then
        local results = cargo.query("MatchStats_Player", pSelect, { where = whereClause, limit = 5000 })
        if not results or #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 p = r.player
            uniqueTeams[r.team] = true
            if not pData[p] then 
                pData[p] = { player = p, team = r.team, matches = 0, 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]
            d.matches = d.matches + 1
            local e = tonumber(r.player_elims or 0)
            d.elims = d.elims + e
            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.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
            
            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

    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={}, top5_pct={}, bot5_pct={}, pts_0_5_pct={}, pts_6_12_pct={}, pts_13_20_pct={}, pts_21_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.top5_pct = (d.top5 / d.matches) * 100; d.bot5_pct = (d.bot5 / d.matches) * 100
                d.pts_0_5_pct = (d.pts_0_5 / d.matches) * 100; d.pts_6_12_pct = (d.pts_6_12 / d.matches) * 100; 
                d.pts_13_20_pct = (d.pts_13_20 / d.matches) * 100; d.pts_21_plus_pct = (d.pts_21_plus / d.matches) * 100
                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.top5_pct, d.top5_pct); table.insert(extremes.bot5_pct, d.bot5_pct)
                table.insert(extremes.pts_0_5_pct, d.pts_0_5_pct); table.insert(extremes.pts_6_12_pct, d.pts_6_12_pct); 
                table.insert(extremes.pts_13_20_pct, d.pts_13_20_pct); table.insert(extremes.pts_21_plus_pct, d.pts_21_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')
        th:tag('th'):addClass('sticky-col sticky-1'):wikitext('#')
        th:tag('th'):addClass('sticky-col sticky-2 stat-team'):wikitext('Team')
        th:tag('th'):addClass('stat-col'):wikitext('M')
        
        if showAvgRank then th:tag('th'):addClass('stat-col'):wikitext('Avg Rank') end
        th:tag('th'):addClass('stat-col'):wikitext('Avg Place Pts')
        th:tag('th'):addClass('stat-col'):wikitext('Avg Elims')
        th:tag('th'):addClass('stat-col'):wikitext('Avg Total')
        
        for _, col in ipairs(optColumns) do
            if activeCols[col] then
                local cName = colNames[col] or col
                if activeCols[col].avg then th:tag('th'):addClass('stat-col'):wikitext('Avg ' .. cName) end
                if activeCols[col].max then th:tag('th'):addClass('stat-col'):wikitext('Max ' .. cName) end
                if activeCols[col].sum then th:tag('th'):addClass('stat-col'):wikitext('Total ' .. cName) end
            end
        end

        for _, m in ipairs(mapList) do th:tag('th'):addClass('stat-col'):wikitext(m) end
        th:tag('th'):addClass('stat-col'):wikitext('Win %')
        th:tag('th'):addClass('stat-col'):wikitext('Top 5 %')
        th:tag('th'):addClass('stat-col'):wikitext('Bot 5 %')
        th:tag('th'):addClass('stat-col'):wikitext('0-5 Pts')
        th:tag('th'):addClass('stat-col'):wikitext('6-12 Pts')
        th:tag('th'):addClass('stat-col'):wikitext('13-20 Pts')
        th:tag('th'):addClass('stat-col'):wikitext('21+ Pts')
        
        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('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('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('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('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('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('style', getHeatmap(d.top5_pct, bounds.top5_pct.min, bounds.top5_pct.max, true)):wikitext(fmt(d.top5_pct, 0) .. '%')
            tr:tag('td'):addClass('stat-col'):attr('style', getHeatmap(d.bot5_pct, bounds.bot5_pct.min, bounds.bot5_pct.max, false)):wikitext(fmt(d.bot5_pct, 0) .. '%')
            tr:tag('td'):addClass('stat-col'):attr('style', getHeatmap(d.pts_0_5_pct, bounds.pts_0_5_pct.min, bounds.pts_0_5_pct.max, false)):wikitext(fmt(d.pts_0_5_pct, 0) .. '%')
            tr:tag('td'):addClass('stat-col'):attr('style', getHeatmap(d.pts_6_12_pct, bounds.pts_6_12_pct.min, bounds.pts_6_12_pct.max, true)):wikitext(fmt(d.pts_6_12_pct, 0) .. '%')
            tr:tag('td'):addClass('stat-col'):attr('style', getHeatmap(d.pts_13_20_pct, bounds.pts_13_20_pct.min, bounds.pts_13_20_pct.max, true)):wikitext(fmt(d.pts_13_20_pct, 0) .. '%')
            tr:tag('td'):addClass('stat-col'):attr('style', getHeatmap(d.pts_21_plus_pct, bounds.pts_21_plus_pct.min, bounds.pts_21_plus_pct.max, true)):wikitext(fmt(d.pts_21_plus_pct, 0) .. '%')
        end

    elseif statType == "player" then
        local list = {}
        local ext = { elims={}, fpm={}, max_elims={}, elims_5plus={}, zero_pct={}, mvps={}, mvp_score={} }
        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
            if d.matches > 0 then
                d.fpm = d.elims / d.matches; d.zero_pct = (d.elims_0 / d.matches) * 100; 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

                table.insert(ext.elims, d.elims); table.insert(ext.fpm, d.fpm); 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)
                
                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 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.fpm ~= b.fpm then return a.fpm > b.fpm end
            if a.max_elims ~= b.max_elims then return a.max_elims > b.max_elims 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')
        th:tag('th'):addClass('sticky-col sticky-1'):wikitext('#')
        th:tag('th'):addClass('sticky-col sticky-logo'):wikitext('Team')
        th:tag('th'):addClass('sticky-col sticky-player stat-team'):wikitext('Player')
        th:tag('th'):addClass('stat-col'):wikitext('M')
        
        if showMvpScore then th:tag('th'):addClass('stat-col'):wikitext('MVP Rating') end

        th:tag('th'):addClass('stat-col'):wikitext('Elims')
        th:tag('th'):addClass('stat-col'):wikitext('FPM')
        
        for _, col in ipairs(optColumns) do
            if activeCols[col] then
                local cName = colNames[col] or col
                if activeCols[col].avg then th:tag('th'):addClass('stat-col'):wikitext('Avg ' .. cName) end
                if activeCols[col].max then th:tag('th'):addClass('stat-col'):wikitext('Max ' .. cName) end
                if activeCols[col].sum then th:tag('th'):addClass('stat-col'):wikitext('Total ' .. cName) end
            end
        end

        th:tag('th'):addClass('stat-col'):wikitext('Max Elims')
        th:tag('th'):addClass('stat-col'):wikitext('5+ Elims')
        th:tag('th'):addClass('stat-col'):wikitext('0 Elims %')
        if showMvps then th:tag('th'):addClass('stat-col'):wikitext('Match MVPs') 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 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.fpm, bounds.fpm.min, bounds.fpm.max, true)):wikitext(fmt(d.fpm, 2))
            
            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
            
            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('style', getHeatmap(d.zero_pct, bounds.zero_pct.min, bounds.zero_pct.max, false)):wikitext(fmt(d.zero_pct, 0) .. '%')
            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
    
    return tostring(root)
end

return p