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
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)
local function sqlEscape(s)
     if not s then return "" end
     if not s then return "" end
     s = s:gsub("\\", "\\\\")
     return s:gsub("\\", "\\\\"):gsub("'", "\\'")
    s = s:gsub("'", "\\'")
    return s
end
 
-- ============================================================
-- HELPER: Argument Fetcher
-- ============================================================
local function getArgs(frame)
    local args = {}
    for k, v in pairs(frame.args) do args[k] = v end
    if frame:getParent() then
        for k, v in pairs(frame:getParent().args) do args[k] = v end
    end
    return args
end
 
-- ============================================================
-- CONFIGURATION
-- ============================================================
local HEADERS = {
    rank = "#", team = "Team", player = "Player", matches_played = "Matches",
    finishes = "Finishes", fpm = "FPM", knocks = "Knocks", damage = "Damage",
    headshots = "Headshots", longest = "Longest", assists = "Assists",
    grenade_kills = "Grenade Fin", vehicle_kills = "Vehicle Fin", contribution = "Contrib %",
    survival = "Surv. Time", healings = "Heals", revives = "Revives", damage_taken = "Dmg Recv",
    grenades_used = "Nades Used", smokes_used = "Smokes Used", utility_used = "Util Used",
    dist_drive = "Drive Dist", dist_walk = "Walk Dist", dist_total = "Total Dist",
    bluezone = "Bluezone Time", air_drops = "Air Drops",
    total_pts = "Total Pts", place_pts = "Place Pts", elims = "Elims",
    avg_place = "Avg Place", avg_place_pts = "Avg Place Pts", avg_elims = "Avg Elims", avg_total = "Avg Pts",
    wwcd = "🥇", place_2 = "🥈", place_3 = "🥉", top_5 = "Top 5", top_8 = "Top 8", place_low = "> 8th",
    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+"
}
 
-- ============================================================
-- HELPER FUNCTIONS
-- ============================================================
 
local function formatNumber(val)
    if not val then return "" end
    local n = tonumber(val)
    if not n then return val end
    if n == math.floor(n) then return math.floor(n) else return string.format("%.2f", n) end
end
end


-- Team Logo Fetcher
local function getTeamLogo(teamName)
local function getTeamLogo(teamName)
     if not teamName then return "" end
     if not teamName or teamName == "" then return "" end
     local cleanName = teamName:gsub("'", "")
     local cleanName = teamName:gsub("'", "")
     local lightFile = cleanName .. '.png'
     local lightFile = cleanName .. '.png'
     local darkFile = cleanName .. '_dark.png'
     local darkFile = cleanName .. '_dark.png'
    local hasLight = mw.title.new('File:' .. lightFile).exists
    local hasDark = mw.title.new('File:' .. darkFile).exists
      
      
     local html = ""
     local str = '<span class="team-logo-wrapper">'
     if hasLight then html = html .. '[[File:' .. lightFile .. '|25px|link=' .. teamName .. '|class=logo-lightmode]]'
     str = str .. '<span class="logo-lightmode">[[File:' .. lightFile .. '|24px|link=]]</span>'
     else html = html .. '[[File:Shield_team.png|25px|link=' .. teamName .. '|class=logo-lightmode]]' end
     str = str .. '<span class="logo-darkmode">[[File:' .. darkFile .. '|24px|link=]]</span>'
    if hasDark then html = html .. '[[File:' .. darkFile .. '|25px|link=' .. teamName .. '|class=logo-darkmode]]'
     str = str .. '</span>'
     elseif hasLight then html = html .. '[[File:' .. lightFile .. '|25px|link=' .. teamName .. '|class=logo-darkmode]]'
    return str
     else html = html .. '[[File:Shield_team_dark.png|25px|link=' .. teamName .. '|class=logo-darkmode]]' end
end
     return html .. " "
 
-- Format Number Helper
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
end


-- ============================================================
-- Heatmap Color Generator (Subtle)
-- MAIN GENERATOR
local function getHeatmap(val, min, max, isGood)
-- ============================================================
     if not val or not min or not max or max == min then return "" end
function p.main(frame)
     local ratio = (val - min) / (max - min)
    local args = getArgs(frame)
     if ratio < 0 then ratio = 0 end
     local type = args.type or "player"
     if ratio > 1 then ratio = 1 end
    local tournament = args.tournament
    if not tournament or tournament == "" then tournament = mw.title.getCurrentTitle().text end
     local map = args.map or "All"
     local stage = args.stage
     local group = args.group
      
      
     local colsInput = args.columns or ""
     -- Very subtle opacity (Max 25% opacity)
     local colKeys = {}
     local opacity = 0.02 + (ratio * 0.23)
    if colsInput == "" then
        if type == "player" then colKeys = {"rank", "player", "team", "matches_played", "finishes", "damage"}
        else colKeys = {"rank", "team", "matches_played", "total_pts", "wwcd"} end
    else colKeys = text.split(colsInput, ",") end
      
      
     local queryFields = {}
     -- Green for Good stats, Red for Bad stats
     for _, k in ipairs(colKeys) do
     if isGood then
         local cleanK = k:match("^%s*(.-)%s*$")
         return string.format("background-color: rgba(16, 185, 129, %.2f);", opacity)
         if cleanK ~= "rank" then table.insert(queryFields, cleanK) end
    else
         return string.format("background-color: rgba(239, 68, 68, %.2f);", opacity)
     end
     end
end
function p.main(frame)
    local args = frame:getParent().args
    if not args.type then args = frame.args end -- fallback
      
      
    local statType = (args.type or "team"):lower()
   
    -- 1. Build Dynamic Where Clause (Filter based on what user asked for)
     local whereParts = {}
     local whereParts = {}
     table.insert(whereParts, string.format("tournament='%s'", sqlEscape(tournament)))
     local filters = {"tournament", "tournament_type", "stage", "group_name", "map", "match_type", "tournament_day", "date", "time"}
    if map ~= "Any" then table.insert(whereParts, string.format("map='%s'", sqlEscape(map))) end
    for _, f in ipairs(filters) do
    if stage and stage ~= "" then table.insert(whereParts, string.format("stage='%s'", sqlEscape(stage))) end
        local val = args[f] or args[f:gsub("_name", "")]
    if group and group ~= "" then table.insert(whereParts, string.format("groupname='%s'", sqlEscape(group))) end
        if val and val ~= "" then
            table.insert(whereParts, string.format("%s = '%s'", f, sqlEscape(val)))
        end
    end
      
      
     local table_name = (type == "player") and "Player_Stats" or "Team_Stats"
     local whereClause = #whereParts > 0 and table.concat(whereParts, " AND ") or "1=1"
    local selectString = table.concat(queryFields, ",") .. ", team"
   
    local results = cargo.query(table_name, selectString, {
        where = table.concat(whereParts, " AND "),
        orderBy = (type == "player" and "finishes DESC" or "total_pts DESC"),
        limit = 100
    })
   
    if not results or #results == 0 then
        return '<div style="padding:20px; color:var(--text-muted); font-style:italic;">No statistics available yet for this tournament.</div>'
    end
      
      
     local root = html.create('div'):addClass('stats-table-wrapper')
     local root = html.create('div'):addClass('stats-table-wrapper')
     local tbl = root:tag('table'):addClass('flat-data-table sortable')
     local tbl = root:tag('table'):addClass('flat-data-table sortable'):css('width', 'auto'):css('min-width', '100%')
    tbl:css('width', 'auto')
 
      
    -- ==========================================
     local trHead = tbl:tag('tr')
     -- TEAM STATISTICS AGGREGATION
    for _, key in ipairs(colKeys) do
    -- ==========================================
        local cleanKey = key:match("^%s*(.-)%s*$")
     if statType == "team" then
        local label = HEADERS[cleanKey] or cleanKey:upper()
        local results = cargo.query("MatchStats_Team",
            "team, short_name, map, rank, wwcd, place_pts, elim_pts, total_pts, number_of_teams",
            { 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 for this selection.</div>'
        end
       
        local tData = {}
        local mapSet = {}
       
        -- Loop all raw match rows
        for _, r in ipairs(results) do
            local t = r.team
            if not tData[t] then
                tData[t] = {
                    team = t, short = r.short_name, matches = 0,
                    place = 0, elims = 0, total = 0, wwcd = 0,
                    top5 = 0, bot5 = 0,
                    pts_lt10 = 0, pts_10_25 = 0, pts_gt25 = 0,
                    map_pts = {}, map_matches = {}
                }
            end
           
            local d = tData[t]
            d.matches = d.matches + 1
            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)
           
            local rank = tonumber(r.rank or 99)
            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 < 10 then d.pts_lt10 = d.pts_lt10 + 1
            elseif pts <= 25 then d.pts_10_25 = d.pts_10_25 + 1
            else d.pts_gt25 = d.pts_gt25 + 1 end
           
            if r.map and r.map ~= "" then
                mapSet[r.map] = true
                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
          
          
         local th = trHead:tag('th'):wikitext(label):css("text-align", "center"):css("white-space", "nowrap")
         local mapList = {}
        for m in pairs(mapSet) do table.insert(mapList, m) end
        table.sort(mapList)
 
        -- Calculate Averages & Percentages
        local list = {}
        local extremes = {
            avg_place={}, avg_elims={}, avg_total={}, win_pct={},
            top5_pct={}, bot5_pct={}, pts_lt10_pct={}, pts_10_25_pct={}, pts_gt25_pct={}
        }
        for _, m in ipairs(mapList) do extremes["map_"..m] = {} end
 
        local function addExt(key, val)
            table.insert(extremes[key], val)
        end
          
          
         if cleanKey == "rank" then
         for _, d in pairs(tData) do
            th:addClass('sticky-col sticky-1')
            if d.matches > 0 then
        elseif type == "team" and cleanKey == "team" then
                d.avg_place = d.place / d.matches
            -- TEAM STATS (PC): Width 220px to fit long names
                d.avg_elims = d.elims / d.matches
            th:addClass('sticky-col sticky-2'):css('width', '220px'):css('min-width', '220px'):css('max-width', '220px')
                d.avg_total = d.total / d.matches
        elseif type == "player" and cleanKey == "player" then
                d.win_pct = (d.wwcd / d.matches) * 100
            -- PLAYER STATS (PC): Width 180px for player names
                d.top5_pct = (d.top5 / d.matches) * 100
            th:addClass('sticky-col sticky-2'):css('width', '180px'):css('min-width', '180px'):css('max-width', '180px')
                d.bot5_pct = (d.bot5 / d.matches) * 100
                d.pts_lt10_pct = (d.pts_lt10 / d.matches) * 100
                d.pts_10_25_pct = (d.pts_10_25 / d.matches) * 100
                d.pts_gt25_pct = (d.pts_gt25 / d.matches) * 100
               
                -- Force Tiebreakers if provided
                d.tiebreaker = tonumber(args["tiebreaker_" .. d.team]) or 0
               
                addExt("avg_place", d.avg_place); addExt("avg_elims", d.avg_elims); addExt("avg_total", d.avg_total)
                addExt("win_pct", d.win_pct); addExt("top5_pct", d.top5_pct); addExt("bot5_pct", d.bot5_pct)
                addExt("pts_lt10_pct", d.pts_lt10_pct); addExt("pts_10_25_pct", d.pts_10_25_pct); addExt("pts_gt25_pct", d.pts_gt25_pct)
 
                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]
                        addExt("map_"..m, d["avg_map_"..m])
                    end
                end
               
                table.insert(list, d)
            end
         end
         end
    end
       
   
        -- Sort Data (Avg Total > Win Pct > Avg Elims > Tiebreaker)
    for i, row in ipairs(results) do
         table.sort(list, function(a, b)
         local tr = tbl:tag('tr')
             if a.avg_total ~= b.avg_total then return a.avg_total > b.avg_total end
        for _, key in ipairs(colKeys) do
             if a.win_pct ~= b.win_pct then return a.win_pct > b.win_pct end
             local cleanKey = key:match("^%s*(.-)%s*$")
             if a.avg_elims ~= b.avg_elims then return a.avg_elims > b.avg_elims end
             local cell = tr:tag('td')
            return a.tiebreaker > b.tiebreaker
           
        end)
             if cleanKey == "rank" then
       
                cell:addClass('sticky-col sticky-1')
        -- Find Min/Max for Heatmaps
            elseif type == "team" and cleanKey == "team" then
        local bounds = {}
                cell:addClass('sticky-col sticky-2'):css('width', '220px'):css('min-width', '220px'):css('max-width', '220px')
        for k, vals in pairs(extremes) do
             elseif type == "player" and cleanKey == "player" then
             if #vals > 0 then
                 cell:addClass('sticky-col sticky-2'):css('width', '180px'):css('min-width', '180px'):css('max-width', '180px')
                 bounds[k] = { min = math.min(unpack(vals)), max = math.max(unpack(vals)) }
             end
             end
        end


             if cleanKey == "rank" then
        -- Build Table Headers
                cell:wikitext(i .. '.'):css("font-weight", "bold"):css("text-align", "center"):css("white-space", "nowrap"):css("padding", "5px 0")
        local th = tbl:tag('tr')
             elseif cleanKey == "team" then
        th:tag('th'):addClass('sticky-1'):wikitext('#')
                 local teamName = row.team or ""
        th:tag('th'):addClass('sticky-2'):wikitext('Team')
                 cell:attr("data-sort-value", teamName)
        th:tag('th'):wikitext('M')
                 if type == "player" then
        th:tag('th'):wikitext('Avg Place')
                     cell:wikitext(getTeamLogo(teamName)):css("text-align", "center")
        th:tag('th'):wikitext('Avg Elims')
        th:tag('th'):wikitext('Avg Total')
        for _, m in ipairs(mapList) do th:tag('th'):wikitext(m) end
        th:tag('th'):wikitext('Win %')
        th:tag('th'):wikitext('Top 5 %')
        th:tag('th'):wikitext('Bot 5 %')
        th:tag('th'):wikitext('< 10 Pts')
        th:tag('th'):wikitext('10-25 Pts')
        th:tag('th'):wikitext('25+ Pts')
       
        -- Build Rows
        for i, d in ipairs(list) do
            local tr = tbl:tag('tr')
            tr:tag('td'):addClass('sticky-1'):css('text-align','center'):css('font-weight','800'):wikitext(i)
           
            -- Team Logic (PC vs Mobile)
            local teamCell = tr:tag('td'):addClass('sticky-2'):css('display','flex'):css('align-items','center'):css('gap','8px')
             teamCell:wikitext(getTeamLogo(d.team))
            teamCell:tag('span'):addClass('pc-only'):wikitext('[[' .. d.team .. ']]')
            local short = (d.short and d.short ~= "") and d.short or d.team
            teamCell:tag('span'):addClass('mobile-only'):wikitext('[[' .. d.team .. '|' .. short .. ']]')
           
            tr:tag('td'):css('text-align','center'):wikitext(d.matches)
            tr:tag('td'):css('text-align','center'):attr('style', getHeatmap(d.avg_place, bounds.avg_place.min, bounds.avg_place.max, true)):wikitext(fmt(d.avg_place, 1))
            tr:tag('td'):css('text-align','center'):attr('style', getHeatmap(d.avg_elims, bounds.avg_elims.min, bounds.avg_elims.max, true)):wikitext(fmt(d.avg_elims, 1))
            tr:tag('td'):css('text-align','center'):css('font-weight','bold'):attr('style', getHeatmap(d.avg_total, bounds.avg_total.min, bounds.avg_total.max, true)):wikitext(fmt(d.avg_total, 1))
              
            for _, m in ipairs(mapList) do
                 local mapVal = d["avg_map_"..m]
                 local mapCell = tr:tag('td'):css('text-align','center')
                 if mapVal then
                     mapCell:attr('style', getHeatmap(mapVal, bounds["map_"..m].min, bounds["map_"..m].max, true)):wikitext(fmt(mapVal, 1))
                 else
                 else
                     cell:wikitext(getTeamLogo(teamName) .. '[[' .. teamName .. ']]'):css("text-align", "left"):css("white-space", "nowrap")
                     mapCell:css('opacity','0.3'):wikitext('-')
                 end
                 end
            elseif cleanKey == "player" then
                cell:wikitext('[[' .. (row.player or "") .. ']]'):css("text-align", "left"):css("font-weight", "bold")
            else
                cell:wikitext(formatNumber(row[cleanKey])):css("text-align", "center")
             end
             end
           
            tr:tag('td'):css('text-align','center'):attr('style', getHeatmap(d.win_pct, bounds.win_pct.min, bounds.win_pct.max, true)):wikitext(fmt(d.win_pct, 0) .. '%')
            tr:tag('td'):css('text-align','center'):attr('style', getHeatmap(d.top5_pct, bounds.top5_pct.min, bounds.top5_pct.max, true)):wikitext(fmt(d.top5_pct, 0) .. '%')
            tr:tag('td'):css('text-align','center'):attr('style', getHeatmap(d.bot5_pct, bounds.bot5_pct.min, bounds.bot5_pct.max, false)):wikitext(fmt(d.bot5_pct, 0) .. '%') -- False = Red for Bot 5
           
            tr:tag('td'):css('text-align','center'):attr('style', getHeatmap(d.pts_lt10_pct, bounds.pts_lt10_pct.min, bounds.pts_lt10_pct.max, false)):wikitext(fmt(d.pts_lt10_pct, 0) .. '%') -- False = Red
            tr:tag('td'):css('text-align','center'):attr('style', getHeatmap(d.pts_10_25_pct, bounds.pts_10_25_pct.min, bounds.pts_10_25_pct.max, true)):wikitext(fmt(d.pts_10_25_pct, 0) .. '%')
            tr:tag('td'):css('text-align','center'):attr('style', getHeatmap(d.pts_gt25_pct, bounds.pts_gt25_pct.min, bounds.pts_gt25_pct.max, true)):wikitext(fmt(d.pts_gt25_pct, 0) .. '%')
        end
    -- ==========================================
    -- PLAYER STATISTICS AGGREGATION
    -- ==========================================
    elseif statType == "player" then
        local results = cargo.query("MatchStats_Player",
            "player, team, player_elims",
            { 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 for this selection.</div>'
        end
       
        local pData = {}
        for _, r in ipairs(results) do
            local p = r.player
            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
                }
            end
           
            local d = pData[p]
            d.matches = d.matches + 1
            local e = tonumber(r.player_elims or 0)
            d.elims = d.elims + e
            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
       
        local list = {}
        local ext = { elims={}, fpm={}, max_elims={}, elims_5plus={}, zero_pct={} }
        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
               
                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(list, d)
            end
        end
       
        -- Sort: Elims > FPM > Max Elims
        table.sort(list, function(a, b)
            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 bounds[k] = { min = math.min(unpack(vals)), max = math.max(unpack(vals)) } end
        -- Player Headers
        local th = tbl:tag('tr')
        th:tag('th'):addClass('sticky-1'):wikitext('#')
        th:tag('th'):addClass('sticky-2'):wikitext('Player')
        th:tag('th'):wikitext('M')
        th:tag('th'):wikitext('Elims')
        th:tag('th'):wikitext('FPM')
        th:tag('th'):wikitext('Max Match Elims')
        th:tag('th'):wikitext('5+ Elim Matches')
        th:tag('th'):wikitext('0 Elims %')
       
        for i, d in ipairs(list) do
            local tr = tbl:tag('tr')
            tr:tag('td'):addClass('sticky-1'):css('text-align','center'):css('font-weight','800'):wikitext(i)
           
            -- Player Cell (Name + Small Team Name + Logo)
            local pCell = tr:tag('td'):addClass('sticky-2'):css('display','flex'):css('align-items','center'):css('gap','12px')
            pCell:wikitext(getTeamLogo(d.team))
            local infoDiv = pCell:tag('div'):css('line-height','1.1')
            infoDiv:tag('div'):css('font-weight','bold'):css('font-size','1.1em'):wikitext('[[' .. d.player .. ']]')
            infoDiv:tag('div'):css('font-size','0.75em'):css('color','var(--text-muted)'):css('text-transform','uppercase'):wikitext('[[' .. d.team .. ']]')
           
            tr:tag('td'):css('text-align','center'):wikitext(d.matches)
            tr:tag('td'):css('text-align','center'):css('font-weight','bold'):attr('style', getHeatmap(d.elims, bounds.elims.min, bounds.elims.max, true)):wikitext(d.elims)
            tr:tag('td'):css('text-align','center'):attr('style', getHeatmap(d.fpm, bounds.fpm.min, bounds.fpm.max, true)):wikitext(fmt(d.fpm, 2))
            tr:tag('td'):css('text-align','center'):attr('style', getHeatmap(d.max_elims, bounds.max_elims.min, bounds.max_elims.max, true)):wikitext(d.max_elims)
            tr:tag('td'):css('text-align','center'):attr('style', getHeatmap(d.elims_5plus, bounds.elims_5plus.min, bounds.elims_5plus.max, true)):wikitext(d.elims_5plus)
            tr:tag('td'):css('text-align','center'):attr('style', getHeatmap(d.zero_pct, bounds.zero_pct.min, bounds.zero_pct.max, false)):wikitext(fmt(d.zero_pct, 0) .. '%') -- False = Red heatmap
         end
         end
     end
     end
   
     return tostring(root)
     return tostring(root)
end
end


return p
return p

Revision as of 02:05, 19 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

-- Team Logo Fetcher
local function getTeamLogo(teamName)
    if not teamName or teamName == "" then return "" end
    local cleanName = teamName:gsub("'", "")
    local lightFile = cleanName .. '.png'
    local darkFile = cleanName .. '_dark.png'
    
    local str = '<span class="team-logo-wrapper">'
    str = str .. '<span class="logo-lightmode">[[File:' .. lightFile .. '|24px|link=]]</span>'
    str = str .. '<span class="logo-darkmode">[[File:' .. darkFile .. '|24px|link=]]</span>'
    str = str .. '</span>'
    return str
end

-- Format Number Helper
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

-- Heatmap Color Generator (Subtle)
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
    
    -- Very subtle opacity (Max 25% opacity)
    local opacity = 0.02 + (ratio * 0.23)
    
    -- Green for Good stats, Red for Bad stats
    if isGood then
        return string.format("background-color: rgba(16, 185, 129, %.2f);", opacity)
    else
        return string.format("background-color: rgba(239, 68, 68, %.2f);", opacity)
    end
end

function p.main(frame)
    local args = frame:getParent().args
    if not args.type then args = frame.args end -- fallback
    
    local statType = (args.type or "team"):lower()
    
    -- 1. Build Dynamic Where Clause (Filter based on what user asked for)
    local whereParts = {}
    local filters = {"tournament", "tournament_type", "stage", "group_name", "map", "match_type", "tournament_day", "date", "time"}
    for _, f in ipairs(filters) do
        local val = args[f] or args[f:gsub("_name", "")] 
        if val and val ~= "" then
            table.insert(whereParts, string.format("%s = '%s'", f, sqlEscape(val)))
        end
    end
    
    local whereClause = #whereParts > 0 and table.concat(whereParts, " AND ") or "1=1"
    
    local root = html.create('div'):addClass('stats-table-wrapper')
    local tbl = root:tag('table'):addClass('flat-data-table sortable'):css('width', 'auto'):css('min-width', '100%')

    -- ==========================================
    -- TEAM STATISTICS AGGREGATION
    -- ==========================================
    if statType == "team" then
        local results = cargo.query("MatchStats_Team", 
            "team, short_name, map, rank, wwcd, place_pts, elim_pts, total_pts, number_of_teams", 
            { 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 for this selection.</div>'
        end
        
        local tData = {}
        local mapSet = {}
        
        -- Loop all raw match rows
        for _, r in ipairs(results) do
            local t = r.team
            if not tData[t] then
                tData[t] = { 
                    team = t, short = r.short_name, matches = 0, 
                    place = 0, elims = 0, total = 0, wwcd = 0,
                    top5 = 0, bot5 = 0, 
                    pts_lt10 = 0, pts_10_25 = 0, pts_gt25 = 0,
                    map_pts = {}, map_matches = {}
                }
            end
            
            local d = tData[t]
            d.matches = d.matches + 1
            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)
            
            local rank = tonumber(r.rank or 99)
            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 < 10 then d.pts_lt10 = d.pts_lt10 + 1
            elseif pts <= 25 then d.pts_10_25 = d.pts_10_25 + 1
            else d.pts_gt25 = d.pts_gt25 + 1 end
            
            if r.map and r.map ~= "" then
                mapSet[r.map] = true
                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
        
        local mapList = {}
        for m in pairs(mapSet) do table.insert(mapList, m) end
        table.sort(mapList)

        -- Calculate Averages & Percentages
        local list = {}
        local extremes = { 
            avg_place={}, avg_elims={}, avg_total={}, win_pct={}, 
            top5_pct={}, bot5_pct={}, pts_lt10_pct={}, pts_10_25_pct={}, pts_gt25_pct={} 
        }
        for _, m in ipairs(mapList) do extremes["map_"..m] = {} end

        local function addExt(key, val)
            table.insert(extremes[key], val)
        end
        
        for _, d in pairs(tData) do
            if d.matches > 0 then
                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_lt10_pct = (d.pts_lt10 / d.matches) * 100
                d.pts_10_25_pct = (d.pts_10_25 / d.matches) * 100
                d.pts_gt25_pct = (d.pts_gt25 / d.matches) * 100
                
                -- Force Tiebreakers if provided
                d.tiebreaker = tonumber(args["tiebreaker_" .. d.team]) or 0
                
                addExt("avg_place", d.avg_place); addExt("avg_elims", d.avg_elims); addExt("avg_total", d.avg_total)
                addExt("win_pct", d.win_pct); addExt("top5_pct", d.top5_pct); addExt("bot5_pct", d.bot5_pct)
                addExt("pts_lt10_pct", d.pts_lt10_pct); addExt("pts_10_25_pct", d.pts_10_25_pct); addExt("pts_gt25_pct", d.pts_gt25_pct)

                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]
                        addExt("map_"..m, d["avg_map_"..m])
                    end
                end
                
                table.insert(list, d)
            end
        end
        
        -- Sort Data (Avg Total > Win Pct > Avg Elims > Tiebreaker)
        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)
        
        -- Find Min/Max for Heatmaps
        local bounds = {}
        for k, vals in pairs(extremes) do
            if #vals > 0 then
                bounds[k] = { min = math.min(unpack(vals)), max = math.max(unpack(vals)) }
            end
        end

        -- Build Table Headers
        local th = tbl:tag('tr')
        th:tag('th'):addClass('sticky-1'):wikitext('#')
        th:tag('th'):addClass('sticky-2'):wikitext('Team')
        th:tag('th'):wikitext('M')
        th:tag('th'):wikitext('Avg Place')
        th:tag('th'):wikitext('Avg Elims')
        th:tag('th'):wikitext('Avg Total')
        for _, m in ipairs(mapList) do th:tag('th'):wikitext(m) end
        th:tag('th'):wikitext('Win %')
        th:tag('th'):wikitext('Top 5 %')
        th:tag('th'):wikitext('Bot 5 %')
        th:tag('th'):wikitext('< 10 Pts')
        th:tag('th'):wikitext('10-25 Pts')
        th:tag('th'):wikitext('25+ Pts')
        
        -- Build Rows
        for i, d in ipairs(list) do
            local tr = tbl:tag('tr')
            tr:tag('td'):addClass('sticky-1'):css('text-align','center'):css('font-weight','800'):wikitext(i)
            
            -- Team Logic (PC vs Mobile)
            local teamCell = tr:tag('td'):addClass('sticky-2'):css('display','flex'):css('align-items','center'):css('gap','8px')
            teamCell:wikitext(getTeamLogo(d.team))
            teamCell:tag('span'):addClass('pc-only'):wikitext('[[' .. d.team .. ']]')
            local short = (d.short and d.short ~= "") and d.short or d.team
            teamCell:tag('span'):addClass('mobile-only'):wikitext('[[' .. d.team .. '|' .. short .. ']]')
            
            tr:tag('td'):css('text-align','center'):wikitext(d.matches)
            tr:tag('td'):css('text-align','center'):attr('style', getHeatmap(d.avg_place, bounds.avg_place.min, bounds.avg_place.max, true)):wikitext(fmt(d.avg_place, 1))
            tr:tag('td'):css('text-align','center'):attr('style', getHeatmap(d.avg_elims, bounds.avg_elims.min, bounds.avg_elims.max, true)):wikitext(fmt(d.avg_elims, 1))
            tr:tag('td'):css('text-align','center'):css('font-weight','bold'):attr('style', getHeatmap(d.avg_total, bounds.avg_total.min, bounds.avg_total.max, true)):wikitext(fmt(d.avg_total, 1))
            
            for _, m in ipairs(mapList) do
                local mapVal = d["avg_map_"..m]
                local mapCell = tr:tag('td'):css('text-align','center')
                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'):css('text-align','center'):attr('style', getHeatmap(d.win_pct, bounds.win_pct.min, bounds.win_pct.max, true)):wikitext(fmt(d.win_pct, 0) .. '%')
            tr:tag('td'):css('text-align','center'):attr('style', getHeatmap(d.top5_pct, bounds.top5_pct.min, bounds.top5_pct.max, true)):wikitext(fmt(d.top5_pct, 0) .. '%')
            tr:tag('td'):css('text-align','center'):attr('style', getHeatmap(d.bot5_pct, bounds.bot5_pct.min, bounds.bot5_pct.max, false)):wikitext(fmt(d.bot5_pct, 0) .. '%') -- False = Red for Bot 5
            
            tr:tag('td'):css('text-align','center'):attr('style', getHeatmap(d.pts_lt10_pct, bounds.pts_lt10_pct.min, bounds.pts_lt10_pct.max, false)):wikitext(fmt(d.pts_lt10_pct, 0) .. '%') -- False = Red
            tr:tag('td'):css('text-align','center'):attr('style', getHeatmap(d.pts_10_25_pct, bounds.pts_10_25_pct.min, bounds.pts_10_25_pct.max, true)):wikitext(fmt(d.pts_10_25_pct, 0) .. '%')
            tr:tag('td'):css('text-align','center'):attr('style', getHeatmap(d.pts_gt25_pct, bounds.pts_gt25_pct.min, bounds.pts_gt25_pct.max, true)):wikitext(fmt(d.pts_gt25_pct, 0) .. '%')
        end

    -- ==========================================
    -- PLAYER STATISTICS AGGREGATION
    -- ==========================================
    elseif statType == "player" then
        local results = cargo.query("MatchStats_Player", 
            "player, team, player_elims", 
            { 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 for this selection.</div>'
        end
        
        local pData = {}
        for _, r in ipairs(results) do
            local p = r.player
            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 
                }
            end
            
            local d = pData[p]
            d.matches = d.matches + 1
            local e = tonumber(r.player_elims or 0)
            d.elims = d.elims + e
            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
        
        local list = {}
        local ext = { elims={}, fpm={}, max_elims={}, elims_5plus={}, zero_pct={} }
        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
                
                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(list, d)
            end
        end
        
        -- Sort: Elims > FPM > Max Elims
        table.sort(list, function(a, b)
            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 bounds[k] = { min = math.min(unpack(vals)), max = math.max(unpack(vals)) } end

        -- Player Headers
        local th = tbl:tag('tr')
        th:tag('th'):addClass('sticky-1'):wikitext('#')
        th:tag('th'):addClass('sticky-2'):wikitext('Player')
        th:tag('th'):wikitext('M')
        th:tag('th'):wikitext('Elims')
        th:tag('th'):wikitext('FPM')
        th:tag('th'):wikitext('Max Match Elims')
        th:tag('th'):wikitext('5+ Elim Matches')
        th:tag('th'):wikitext('0 Elims %')
        
        for i, d in ipairs(list) do
            local tr = tbl:tag('tr')
            tr:tag('td'):addClass('sticky-1'):css('text-align','center'):css('font-weight','800'):wikitext(i)
            
            -- Player Cell (Name + Small Team Name + Logo)
            local pCell = tr:tag('td'):addClass('sticky-2'):css('display','flex'):css('align-items','center'):css('gap','12px')
            pCell:wikitext(getTeamLogo(d.team))
            local infoDiv = pCell:tag('div'):css('line-height','1.1')
            infoDiv:tag('div'):css('font-weight','bold'):css('font-size','1.1em'):wikitext('[[' .. d.player .. ']]')
            infoDiv:tag('div'):css('font-size','0.75em'):css('color','var(--text-muted)'):css('text-transform','uppercase'):wikitext('[[' .. d.team .. ']]')
            
            tr:tag('td'):css('text-align','center'):wikitext(d.matches)
            tr:tag('td'):css('text-align','center'):css('font-weight','bold'):attr('style', getHeatmap(d.elims, bounds.elims.min, bounds.elims.max, true)):wikitext(d.elims)
            tr:tag('td'):css('text-align','center'):attr('style', getHeatmap(d.fpm, bounds.fpm.min, bounds.fpm.max, true)):wikitext(fmt(d.fpm, 2))
            tr:tag('td'):css('text-align','center'):attr('style', getHeatmap(d.max_elims, bounds.max_elims.min, bounds.max_elims.max, true)):wikitext(d.max_elims)
            tr:tag('td'):css('text-align','center'):attr('style', getHeatmap(d.elims_5plus, bounds.elims_5plus.min, bounds.elims_5plus.max, true)):wikitext(d.elims_5plus)
            tr:tag('td'):css('text-align','center'):attr('style', getHeatmap(d.zero_pct, bounds.zero_pct.min, bounds.zero_pct.max, false)):wikitext(fmt(d.zero_pct, 0) .. '%') -- False = Red heatmap
        end
    end
    
    return tostring(root)
end

return p