Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.
Revision as of 02:36, 19 May 2026 by Esportsamaze (talk | contribs)

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
local function buildLogo(teamName, tData, mData)
    local lightFile = "Shield_team.png"
    local darkFile = "Shield_team_dark.png"

    if tData and tData.image and tData.image ~= "" then
        lightFile = tData.image
        darkFile = (tData.image_dark and tData.image_dark ~= "") and tData.image_dark or lightFile
    elseif mData then
        if mData.image and mData.image ~= "" then 
            lightFile = mData.image 
        else 
            lightFile = teamName .. ".png" 
        end
        if mData.image_dark and mData.image_dark ~= "" then 
            darkFile = mData.image_dark 
        else 
            darkFile = lightFile 
        end
    else
        lightFile = teamName .. ".png"
        darkFile = teamName .. "_dark.png"
    end

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

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

local function fmt(num, decimals)
    if not num then return "0" end
    if decimals and decimals > 0 then
        return string.format("%." .. decimals .. "f", num)
    else
        return tostring(math.floor(num + 0.5))
    end
end

local function getHeatmap(val, min, max, isGood)
    if not val or not min or 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

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 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('standings-wrapper')
    local tbl = root:tag('table'):addClass('standings-card-table stats-card-table sortable')

    local tData = {}
    local pData = {}
    local uniqueTeams = {}

    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.</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, 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
                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", "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.</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 } 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
    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_place={}, avg_elims={}, avg_total={}, win_pct={}, top5_pct={}, bot5_pct={}, pts_lt10_pct={}, pts_10_25_pct={}, pts_gt25_pct={} }
        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_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
                d.tiebreaker = tonumber(args["tiebreaker_" .. d.team]) or 0
                
                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_lt10_pct, d.pts_lt10_pct); table.insert(extremes.pts_10_25_pct, d.pts_10_25_pct); table.insert(extremes.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]
                        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 bounds[k] = { min = math.min(unpack(vals)), max = math.max(unpack(vals)) } 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')
        th:tag('th'):addClass('stat-col'):wikitext('Avg Place')
        th:tag('th'):addClass('stat-col'):wikitext('Avg Elims')
        th:tag('th'):addClass('stat-col'):wikitext('Avg Total')
        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('< 10 Pts')
        th:tag('th'):addClass('stat-col'):wikitext('10-25 Pts')
        th:tag('th'):addClass('stat-col'):wikitext('25+ Pts')
        
        for i, d in ipairs(list) do
            local tr = tbl:tag('tr'):addClass('standings-row')
            tr:tag('td'):addClass('sticky-col sticky-1'):css('text-align','center'):css('font-weight','800'):wikitext(i)
            
            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'):wikitext('[[' .. d.team .. '|' .. dispName .. ']]')
            teamCell:tag('span'):addClass('mobile-only'):wikitext('[[' .. d.team .. '|' .. shortName .. ']]')
            
            tr:tag('td'):addClass('stat-col'):wikitext(d.matches)
            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 _, 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_lt10_pct, bounds.pts_lt10_pct.min, bounds.pts_lt10_pct.max, false)):wikitext(fmt(d.pts_lt10_pct, 0) .. '%')
            tr:tag('td'):addClass('stat-col'):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'):addClass('stat-col'):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

    elseif statType == "player" then
        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
        
        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

        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('Player')
        th:tag('th'):addClass('stat-col'):wikitext('M')
        th:tag('th'):addClass('stat-col'):wikitext('Elims')
        th:tag('th'):addClass('stat-col'):wikitext('FPM')
        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 %')
        
        for i, d in ipairs(list) do
            local tr = tbl:tag('tr'):addClass('standings-row')
            tr:tag('td'):addClass('sticky-col sticky-1'):css('text-align','center'):css('font-weight','800'):wikitext(i)
            
            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 pCell = tr:tag('td'):addClass('sticky-col sticky-2 stat-team team-name-cell')
            pCell:wikitext(buildLogo(d.team, tD, mD))
            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 .. '|' .. dispTeamName .. ']]')
            
            tr:tag('td'):addClass('stat-col'):wikitext(d.matches)
            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))
            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) .. '%')
        end
    end
    
    return tostring(root)
end

return p