Module:Statistics: Difference between revisions
From eSportsAmaze
More actions
No edit summary Tag: Reverted |
Esportsamaze (talk | contribs) No edit summary |
||
| (23 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 function sqlEscape(s) | |||
if not s then return "" end | |||
-- | return s:gsub("\\", "\\\\"):gsub("'", "\\'") | ||
local function | end | ||
local | |||
-- Smart Logo Builder (ZERO Expensive Calls) | |||
if | 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 | end | ||
return | |||
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 | 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 | |||
local | 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 | local function getMinMax(t) | ||
if not | if not t or #t == 0 then return 0, 0 end | ||
local | local mn, mx = t[1], t[1] | ||
if | 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 | end | ||
local function | local function parseTime(s) | ||
if not | if not s or s == "" then return 0 end | ||
local | 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 | |||
if | |||
return | |||
end | end | ||
local function | local function formatTimeStr(totalSeconds) | ||
if not | if not totalSeconds or totalSeconds <= 0 then return "00:00" end | ||
local | 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 | ||
function p.main(frame) | function p.main(frame) | ||
local args = | local args = frame:getParent().args | ||
if not args.type then args = frame.args end | |||
if not | |||
local | local statType = (args.type or "team"):lower() | ||
local | local tournamentName = args.tournament or "" | ||
local limit = tonumber(args.limit) or (statType == "player" and 100 or 500) | |||
local | local optColumns = { | ||
for _, | "damage", "rdamage", "headshots", "assists", "knockouts", "long_elim", | ||
local | "vehicle_elim", "grenade_elim", "smokes", "grenades", "molotovs", "flash", | ||
if | "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 | end | ||
local | 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 | local neededForScore = { damage = true, knockouts = true, survival = true } | ||
local | 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 | end | ||
local whereClause = #whereParts > 0 and table.concat(whereParts, " AND ") or "1=1" | |||
local root = html.create('div'):addClass(' | local root = html.create('div'):addClass('standings-wrapper') | ||
local tbl = root:tag('table'):addClass(' | 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 | local pSet = { player=true, team=true, player_elims=true, mvp=true } | ||
for _ | for col, _ in pairs(activeCols) do | ||
local | 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 | |||
local | 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 | ||
end | end | ||
for | local tourneyDb = {}; local masterDb = {}; local teamListQuoted = {} | ||
local tr = tbl:tag('tr') | for t, _ in pairs(uniqueTeams) do table.insert(teamListQuoted, "'" .. sqlEscape(t) .. "'") end | ||
local teamSql = table.concat(teamListQuoted, ",") | |||
local | if teamSql ~= "" and cargo and cargo.query then | ||
local cell = tr:tag('td') | 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 | 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 | 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 | ||
return tostring(root) | return tostring(root) | ||
end | end | ||
return p | return p | ||
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