Module:Statistics: Difference between revisions
From eSportsAmaze
More actions
Esportsamaze (talk | contribs) No edit summary |
Esportsamaze (talk | contribs) No edit summary |
||
| Line 8: | Line 8: | ||
end | end | ||
local function buildLogo(teamName, tData, mData) | local function buildLogo(teamName, tData, mData) | ||
local lightFile = "Shield_team.png" | local lightFile = "Shield_team.png" | ||
| Line 14: | Line 13: | ||
if tData and tData.image and tData.image ~= "" then | if tData and tData.image and tData.image ~= "" then | ||
lightFile = tData.image | lightFile = tData.image; darkFile = (tData.image_dark and tData.image_dark ~= "") and tData.image_dark or lightFile | ||
elseif mData then | elseif mData then | ||
if mData.image and mData.image ~= "" 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 | |||
if mData.image_dark and mData.image_dark ~= "" then | |||
else | else | ||
lightFile = teamName .. ".png" | lightFile = teamName .. ".png"; darkFile = teamName .. "_dark.png" | ||
end | end | ||
local container = html.create('span'):addClass('team-logo-wrapper') | 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:' .. lightFile).exists then lightFile = "Shield_team.png" end | ||
if not mw.title.new('File:' .. darkFile).exists then | if not mw.title.new('File:' .. darkFile).exists then | ||
| Line 46: | Line 34: | ||
local function fmt(num, decimals) | local function fmt(num, decimals) | ||
if not num then return "0" end | if not num then return "0" end | ||
if decimals and decimals > 0 then | if decimals and decimals > 0 then return string.format("%." .. decimals .. "f", num) else return tostring(math.floor(num + 0.5)) end | ||
end | end | ||
| Line 58: | Line 42: | ||
if ratio < 0 then ratio = 0 end | if ratio < 0 then ratio = 0 end | ||
if ratio > 1 then ratio = 1 end | if ratio > 1 then ratio = 1 end | ||
local opacity = 0.02 + (ratio * 0.23) | local opacity = 0.02 + (ratio * 0.23) | ||
if isGood then | 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 | |||
else | |||
end | end | ||
| Line 73: | Line 53: | ||
local statType = (args.type or "team"):lower() | local statType = (args.type or "team"):lower() | ||
local tournamentName = args.tournament or "" | local tournamentName = args.tournament or "" | ||
local limit = tonumber(args.limit) or (statType == "player" and 100 or 500) -- Default limit 100 for players | |||
-- Dynamic Options | |||
local showDamage = args.show_damage == "true" | |||
local showAssists = args.show_assists == "true" | |||
local showHeadshots = args.show_headshots == "true" | |||
local showKnocks = args.show_knocks == "true" | |||
local whereParts = {} | local whereParts = {} | ||
| Line 78: | Line 65: | ||
for _, f in ipairs(filters) do | for _, f in ipairs(filters) do | ||
local val = args[f] or args[f:gsub("_name", "")] | local val = args[f] or args[f:gsub("_name", "")] | ||
if val and val ~= "" then | if val and val ~= "" then 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 whereClause = #whereParts > 0 and table.concat(whereParts, " AND ") or "1=1" | ||
| Line 90: | Line 75: | ||
local pData = {} | local pData = {} | ||
local uniqueTeams = {} | local uniqueTeams = {} | ||
-- Build Base Query String | |||
local tSelect = "team, short_name, map, rank, wwcd, place_pts, elim_pts, total_pts, number_of_teams" | |||
local pSelect = "player, team, player_elims, mvp" | |||
if showDamage then tSelect = tSelect .. ", damage"; pSelect = pSelect .. ", damage" end | |||
if showAssists then tSelect = tSelect .. ", assists"; pSelect = pSelect .. ", assists" end | |||
if showHeadshots then pSelect = pSelect .. ", headshots" end | |||
if showKnocks then pSelect = pSelect .. ", knockouts" end | |||
if statType == "team" then | if statType == "team" then | ||
local results = cargo.query("MatchStats_Team", | 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 | 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 | ||
| Line 98: | Line 92: | ||
local t = r.team | local t = r.team | ||
uniqueTeams[t] = true | 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 | 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 = {}, damage = 0, assists = 0 } end | ||
local d = tData[t] | local d = tData[t] | ||
d.matches = d.matches + 1 | d.matches = d.matches + 1 | ||
d.place = d.place + tonumber(r.place_pts or 0) | 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 | |||
local pts = tonumber(r.total_pts or 0) | |||
d.wwcd = d.wwcd + tonumber(r.wwcd or 0) | d.wwcd = d.wwcd + tonumber(r.wwcd or 0) | ||
local rank = tonumber(r.rank or 99) | if showDamage then d.damage = d.damage + tonumber(r.damage or 0) end | ||
if showAssists then d.assists = d.assists + tonumber(r.assists or 0) end | |||
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 <= 5 then d.top5 = d.top5 + 1 end | ||
if rank >= (totalTeams - 4) then d.bot5 = d.bot5 + 1 end | if rank >= (totalTeams - 4) then d.bot5 = d.bot5 + 1 end | ||
| Line 120: | Line 114: | ||
end | end | ||
elseif statType == "player" then | elseif statType == "player" then | ||
local results = cargo.query("MatchStats_Player", | 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 | 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 | ||
| Line 126: | Line 120: | ||
local p = r.player | local p = r.player | ||
uniqueTeams[r.team] = true | 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 | 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, damage = 0, assists = 0, headshots = 0, knocks = 0 } end | ||
local d = pData[p] | local d = pData[p] | ||
| Line 132: | Line 126: | ||
local e = tonumber(r.player_elims or 0) | local e = tonumber(r.player_elims or 0) | ||
d.elims = d.elims + e | d.elims = d.elims + e | ||
d.mvps = d.mvps + tonumber(r.mvp or 0) | |||
if showDamage then d.damage = d.damage + tonumber(r.damage or 0) end | |||
if showAssists then d.assists = d.assists + tonumber(r.assists or 0) end | |||
if showHeadshots then d.headshots = d.headshots + tonumber(r.headshots or 0) end | |||
if showKnocks then d.knocks = d.knocks + tonumber(r.knockouts or 0) end | |||
if e > d.max_elims then d.max_elims = e 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 >= 5 then d.elims_5plus = d.elims_5plus + 1 end | ||
| Line 138: | Line 139: | ||
end | end | ||
local tourneyDb = {} | -- Pre-fetch DBs | ||
local tourneyDb = {}; local masterDb = {}; local teamListQuoted = {} | |||
for t, _ in pairs(uniqueTeams) do table.insert(teamListQuoted, "'" .. sqlEscape(t) .. "'") end | for t, _ in pairs(uniqueTeams) do table.insert(teamListQuoted, "'" .. sqlEscape(t) .. "'") end | ||
local teamSql = table.concat(teamListQuoted, ",") | local teamSql = table.concat(teamListQuoted, ",") | ||
| Line 153: | Line 153: | ||
if statType == "team" then | if statType == "team" then | ||
local list = {} | 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 extremes = { avg_place={}, avg_elims={}, avg_total={}, win_pct={}, top5_pct={}, bot5_pct={}, pts_lt10_pct={}, pts_10_25_pct={}, pts_gt25_pct={}, avg_dmg={} } | ||
local mapSet = {} | local mapSet = {} | ||
for _, d in pairs(tData) do for m, _ in pairs(d.map_matches) do mapSet[m] = true end end | for _, d in pairs(tData) do for m, _ in pairs(d.map_matches) do mapSet[m] = true end end | ||
| Line 165: | Line 165: | ||
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.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.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 | ||
if showDamage then d.avg_dmg = d.damage / d.matches; table.insert(extremes.avg_dmg, d.avg_dmg) end | |||
d.tiebreaker = tonumber(args["tiebreaker_" .. d.team]) or 0 | d.tiebreaker = tonumber(args["tiebreaker_" .. d.team]) or 0 | ||
| Line 198: | Line 200: | ||
th:tag('th'):addClass('stat-col'):wikitext('Avg Elims') | th:tag('th'):addClass('stat-col'):wikitext('Avg Elims') | ||
th:tag('th'):addClass('stat-col'):wikitext('Avg Total') | th:tag('th'):addClass('stat-col'):wikitext('Avg Total') | ||
if showDamage then th:tag('th'):addClass('stat-col'):wikitext('Avg Dmg') end | |||
if showAssists then th:tag('th'):addClass('stat-col'):wikitext('Total Assists') end | |||
for _, m in ipairs(mapList) do th:tag('th'):addClass('stat-col'):wikitext(m) 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('Win %') | ||
| Line 206: | Line 210: | ||
th:tag('th'):addClass('stat-col'):wikitext('25+ Pts') | th:tag('th'):addClass('stat-col'):wikitext('25+ Pts') | ||
for i, | for i = 1, math.min(#list, limit) do | ||
local d = list[i] | |||
local tr = tbl:tag('tr'):addClass('standings-row') | 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) | tr:tag('td'):addClass('sticky-col sticky-1'):css('text-align','center'):css('font-weight','800'):wikitext(i) | ||
| Line 217: | Line 222: | ||
local teamCell = tr:tag('td'):addClass('sticky-col sticky-2 stat-team team-name-cell') | local teamCell = tr:tag('td'):addClass('sticky-col sticky-2 stat-team team-name-cell') | ||
teamCell:wikitext(buildLogo(d.team, tD, mD)) | teamCell:wikitext(buildLogo(d.team, tD, mD)) | ||
teamCell:tag('span'):addClass('pc-only'):wikitext('[[' .. d.team .. '|' .. dispName .. ']]') | 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'):wikitext('[[' .. d.team .. '|' .. shortName .. ']]') | 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) | tr:tag('td'):addClass('stat-col'):wikitext(d.matches) | ||
| Line 224: | Line 229: | ||
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'):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)) | 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)) | ||
if showDamage then tr:tag('td'):addClass('stat-col'):attr('style', getHeatmap(d.avg_dmg, bounds.avg_dmg.min, bounds.avg_dmg.max, true)):wikitext(fmt(d.avg_dmg, 0)) end | |||
if showAssists then tr:tag('td'):addClass('stat-col'):wikitext(d.assists) end | |||
for _, m in ipairs(mapList) do | for _, m in ipairs(mapList) do | ||
| Line 242: | Line 250: | ||
elseif statType == "player" then | elseif statType == "player" then | ||
local list = {} | local list = {} | ||
local ext = { elims={}, fpm={}, max_elims={}, elims_5plus={}, zero_pct={} } | local ext = { elims={}, fpm={}, max_elims={}, elims_5plus={}, zero_pct={}, mvps={}, avg_dmg={} } | ||
for _, d in pairs(pData) do | for _, d in pairs(pData) do | ||
if d.matches > 0 then | 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 | 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) | if showDamage then d.avg_dmg = d.damage / d.matches; table.insert(ext.avg_dmg, d.avg_dmg) 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) | |||
table.insert(list, d) | table.insert(list, d) | ||
end | end | ||
| Line 267: | Line 278: | ||
th:tag('th'):addClass('stat-col'):wikitext('Elims') | th:tag('th'):addClass('stat-col'):wikitext('Elims') | ||
th:tag('th'):addClass('stat-col'):wikitext('FPM') | th:tag('th'):addClass('stat-col'):wikitext('FPM') | ||
if showDamage then th:tag('th'):addClass('stat-col'):wikitext('Avg Dmg') end | |||
if showAssists then th:tag('th'):addClass('stat-col'):wikitext('Assists') end | |||
if showHeadshots then th:tag('th'):addClass('stat-col'):wikitext('Headshots') end | |||
if showKnocks then th:tag('th'):addClass('stat-col'):wikitext('Knocks') end | |||
th:tag('th'):addClass('stat-col'):wikitext('Max Elims') | 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('5+ Elims') | ||
th:tag('th'):addClass('stat-col'):wikitext('0 Elims %') | th:tag('th'):addClass('stat-col'):wikitext('0 Elims %') | ||
th:tag('th'):addClass('stat-col'):wikitext('Match MVPs') | |||
for i, | for i = 1, math.min(#list, limit) do | ||
local d = list[i] | |||
local tr = tbl:tag('tr'):addClass('standings-row') | 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) | tr:tag('td'):addClass('sticky-col sticky-1'):css('text-align','center'):css('font-weight','800'):wikitext(i) | ||
| Line 282: | Line 299: | ||
pCell:wikitext(buildLogo(d.team, tD, mD)) | pCell:wikitext(buildLogo(d.team, tD, mD)) | ||
local infoDiv = pCell:tag('div'):css('line-height','1.1') | local infoDiv = pCell:tag('div'):css('line-height','1.1') | ||
infoDiv:tag('div'):css('font-weight',' | infoDiv:tag('div'):css('font-weight','600'):css('font-size','0.95em'):wikitext('[[' .. d.player .. ']]') | ||
infoDiv:tag('div'):css('font-size','0. | infoDiv:tag('div'):css('font-size','0.65em'):css('color','var(--text-muted)'):css('text-transform','uppercase'):css('font-weight','700'):wikitext('[[' .. d.team .. '|' .. dispTeamName .. ']]') | ||
tr:tag('td'):addClass('stat-col'):wikitext(d.matches) | 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'):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.fpm, bounds.fpm.min, bounds.fpm.max, true)):wikitext(fmt(d.fpm, 2)) | ||
if showDamage then tr:tag('td'):addClass('stat-col'):attr('style', getHeatmap(d.avg_dmg, bounds.avg_dmg.min, bounds.avg_dmg.max, true)):wikitext(fmt(d.avg_dmg, 0)) end | |||
if showAssists then tr:tag('td'):addClass('stat-col'):wikitext(d.assists) end | |||
if showHeadshots then tr:tag('td'):addClass('stat-col'):wikitext(d.headshots) end | |||
if showKnocks then tr:tag('td'):addClass('stat-col'):wikitext(d.knocks) 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.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.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) .. '%') | 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) .. '%') | ||
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 | ||
Revision as of 02:51, 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
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 limit = tonumber(args.limit) or (statType == "player" and 100 or 500) -- Default limit 100 for players
-- Dynamic Options
local showDamage = args.show_damage == "true"
local showAssists = args.show_assists == "true"
local showHeadshots = args.show_headshots == "true"
local showKnocks = args.show_knocks == "true"
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 = {}
-- Build Base Query String
local tSelect = "team, short_name, map, rank, wwcd, place_pts, elim_pts, total_pts, number_of_teams"
local pSelect = "player, team, player_elims, mvp"
if showDamage then tSelect = tSelect .. ", damage"; pSelect = pSelect .. ", damage" end
if showAssists then tSelect = tSelect .. ", assists"; pSelect = pSelect .. ", assists" end
if showHeadshots then pSelect = pSelect .. ", headshots" end
if showKnocks then pSelect = pSelect .. ", knockouts" 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, 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 = {}, damage = 0, assists = 0 } 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)
if showDamage then d.damage = d.damage + tonumber(r.damage or 0) end
if showAssists then d.assists = d.assists + tonumber(r.assists or 0) end
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", 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, damage = 0, assists = 0, headshots = 0, knocks = 0 } 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)
if showDamage then d.damage = d.damage + tonumber(r.damage or 0) end
if showAssists then d.assists = d.assists + tonumber(r.assists or 0) end
if showHeadshots then d.headshots = d.headshots + tonumber(r.headshots or 0) end
if showKnocks then d.knocks = d.knocks + tonumber(r.knockouts or 0) 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
-- Pre-fetch DBs
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={}, avg_dmg={} }
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
if showDamage then d.avg_dmg = d.damage / d.matches; table.insert(extremes.avg_dmg, d.avg_dmg) end
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')
if showDamage then th:tag('th'):addClass('stat-col'):wikitext('Avg Dmg') end
if showAssists then th:tag('th'):addClass('stat-col'):wikitext('Total Assists') 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('< 10 Pts')
th:tag('th'):addClass('stat-col'):wikitext('10-25 Pts')
th:tag('th'):addClass('stat-col'):wikitext('25+ 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'):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'):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)
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))
if showDamage then tr:tag('td'):addClass('stat-col'):attr('style', getHeatmap(d.avg_dmg, bounds.avg_dmg.min, bounds.avg_dmg.max, true)):wikitext(fmt(d.avg_dmg, 0)) end
if showAssists then tr:tag('td'):addClass('stat-col'):wikitext(d.assists) 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_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={}, mvps={}, avg_dmg={} }
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 showDamage then d.avg_dmg = d.damage / d.matches; table.insert(ext.avg_dmg, d.avg_dmg) 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)
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')
if showDamage then th:tag('th'):addClass('stat-col'):wikitext('Avg Dmg') end
if showAssists then th:tag('th'):addClass('stat-col'):wikitext('Assists') end
if showHeadshots then th:tag('th'):addClass('stat-col'):wikitext('Headshots') end
if showKnocks then th:tag('th'):addClass('stat-col'):wikitext('Knocks') 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 %')
th:tag('th'):addClass('stat-col'):wikitext('Match MVPs')
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'):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','600'):css('font-size','0.95em'):wikitext('[[' .. d.player .. ']]')
infoDiv:tag('div'):css('font-size','0.65em'):css('color','var(--text-muted)'):css('text-transform','uppercase'):css('font-weight','700'):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))
if showDamage then tr:tag('td'):addClass('stat-col'):attr('style', getHeatmap(d.avg_dmg, bounds.avg_dmg.min, bounds.avg_dmg.max, true)):wikitext(fmt(d.avg_dmg, 0)) end
if showAssists then tr:tag('td'):addClass('stat-col'):wikitext(d.assists) end
if showHeadshots then tr:tag('td'):addClass('stat-col'):wikitext(d.headshots) end
if showKnocks then tr:tag('td'):addClass('stat-col'):wikitext(d.knocks) 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) .. '%')
tr:tag('td'):addClass('stat-col'):attr('style', getHeatmap(d.mvps, bounds.mvps.min, bounds.mvps.max, true)):wikitext(d.mvps)
end
end
return tostring(root)
end
return p