Module:Rankings: Difference between revisions
From eSportsAmaze
More actions
Esportsamaze (talk | contribs) No edit summary |
Esportsamaze (talk | contribs) No edit summary |
||
| Line 5: | Line 5: | ||
local function sqlEscape(s) if not s then return "" end return s:gsub("\\", "\\\\"):gsub("'", "\\'") end | local function sqlEscape(s) if not s then return "" end return s:gsub("\\", "\\\\"):gsub("'", "\\'") end | ||
local function | -- Safely converts YYYY-MM-DD into a Unix Timestamp | ||
local function getTimestamps(dateStr) | |||
if not dateStr or dateStr == "" then return 0 end | if not dateStr or dateStr == "" then return 0 end | ||
local y, m, d = dateStr:match("(%d+)-(%d+)-(%d+)") | local y, m, d = dateStr:match("(%d+)-(%d+)-(%d+)") | ||
if not y then return 0 end | if not y then return 0 end | ||
-- Set to noon to avoid timezone shift edge cases | |||
return os.time({year=y, month=m, day=d, hour=12, min=0, sec=0}) | |||
end | end | ||
-- Smart Logo Builder (Checks Tournament_Teams -> Teams -> Shield) | |||
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 | |||
elseif teamName and teamName ~= "" then | |||
lightFile = teamName:gsub("'", "") .. ".png"; darkFile = teamName:gsub("'", "") .. "_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 | |||
-- Team Matrix Points | |||
local function getTeamBasePts(tier, rank) | local function getTeamBasePts(tier, rank) | ||
local t = tier:lower() | local t = (tier or ""):lower() | ||
local col = 0 | local col = 0 | ||
if t:find("publisher") then col = 1 | if t:find("publisher") then col = 1 | ||
| Line 53: | Line 76: | ||
elseif days <= 1095 then return 0.1 | elseif days <= 1095 then return 0.1 | ||
else return 0 end | else return 0 end | ||
end | end | ||
| Line 75: | Line 83: | ||
local statType = (args.type or "team"):lower() | local statType = (args.type or "team"):lower() | ||
local limit = tonumber(args.limit) or 100 | local limit = tonumber(args.limit) or 100 | ||
-- ========================================== | |||
-- TIME MACHINE LOGIC | |||
-- ========================================== | |||
local targetDateStr = args.as_of_date | |||
local targetTs = os.time() -- Defaults to right now (Live Decay) | |||
if targetDateStr and targetDateStr ~= "" then | |||
local targetVal = getTimestamps(targetDateStr) | |||
if targetVal > 0 then targetTs = targetVal end | |||
end | |||
local root = html.create('div'):addClass('standings-wrapper') | local root = html.create('div'):addClass('standings-wrapper') | ||
local tbl = root:tag('table'):addClass('standings-card-table stats-card-table') | local tbl = root:tag('table'):addClass('standings-card-table stats-card-table') | ||
local uniqueTeams = {} | |||
local tData = {} | |||
local pData = {} | |||
if statType == "team" then | if statType == "team" then | ||
| Line 83: | Line 105: | ||
if not results or #results == 0 then return 'No data.' end | if not results or #results == 0 then return 'No data.' end | ||
for _, r in ipairs(results) do | for _, r in ipairs(results) do | ||
local t = r.team | local t = r.team | ||
if t and t ~= "" then | local endTs = getTimestamps(r.end_date) | ||
if not tData[t] then tData[t] = { team = t, points = 0, events = 0 } end | |||
-- If the event happened on or before our target date... | |||
if t and t ~= "" and endTs > 0 and endTs <= targetTs then | |||
if not tData[t] then | |||
tData[t] = { team = t, points = 0, events = 0, latest_tourney = r.tournament } | |||
uniqueTeams[t] = true | |||
end | |||
local diff = os.difftime(targetTs, endTs) | |||
local days = math.max(0, math.floor(diff / 86400)) | |||
local base = getTeamBasePts(r.tier, tonumber(r.rank) or 99) | local base = getTeamBasePts(r.tier, tonumber(r.rank) or 99) | ||
local decay = getTeamDecay(days) | local decay = getTeamDecay(days) | ||
| Line 99: | Line 128: | ||
end | end | ||
end | end | ||
end | end | ||
| Line 128: | Line 134: | ||
if not results or #results == 0 then return 'No data.' end | if not results or #results == 0 then return 'No data.' end | ||
for _, r in ipairs(results) do | for _, r in ipairs(results) do | ||
local p = r.player | local p = r.player | ||
if p and p ~= "" then | local endTs = getTimestamps(r.end_date) | ||
if p and p ~= "" and endTs > 0 and endTs <= targetTs then | |||
if not pData[p] then | if not pData[p] then | ||
pData[p] = { player = p, team = r.team, points = 0, events = 0 } | pData[p] = { player = p, team = r.team, latest_tourney = r.tournament, points = 0, events = 0 } | ||
if r.team and r.team ~= "" then uniqueTeams[r.team] = true end | |||
else | else | ||
if (not pData[p].team or pData[p].team == "") and r.team and r.team ~= "" then | if (not pData[p].team or pData[p].team == "") and r.team and r.team ~= "" then | ||
pData[p].team = r.team | pData[p].team = r.team | ||
uniqueTeams[r.team] = true | |||
end | end | ||
end | end | ||
local days = | local diff = os.difftime(targetTs, endTs) | ||
local days = math.max(0, math.floor(diff / 86400)) | |||
local decay = getPlayerDecay(days) | local decay = getPlayerDecay(days) | ||
local | local tierLow = (r.tier or ""):lower() | ||
local mult = 1 | local mult = 1 | ||
if | if tierLow:find("publisher") then mult = 2 elseif tierLow:find("tier 1") then mult = 1.5 end | ||
local base = (tonumber(r.finishes) or 0) * mult | local base = (tonumber(r.finishes) or 0) * mult | ||
| Line 163: | Line 171: | ||
end | end | ||
end | end | ||
end | |||
-- ========================================== | |||
-- DATABASE PRE-FETCH (Logos) | |||
-- ========================================== | |||
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 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 | |||
local tResults = cargo.query("Tournament_Teams", "tournament, team, image, image_dark", { where = "team IN (" .. teamSql .. ")", limit = 5000 }) | |||
if tResults then | |||
for _, row in ipairs(tResults) do | |||
tourneyDb[row.tournament:lower() .. "|" .. row.team:lower()] = row | |||
end | |||
end | |||
end | |||
-- ========================================== | |||
-- RENDER TABLES | |||
-- ========================================== | |||
if statType == "team" then | |||
local list = {} | |||
for _, d in pairs(tData) do if d.points > 0 then table.insert(list, d) end end | |||
table.sort(list, function(a, b) return a.points > b.points end) | |||
local th = tbl:tag('tr') | |||
th:tag('th'):addClass('sticky-col sticky-1'):wikitext('Rank') | |||
th:tag('th'):addClass('sticky-col sticky-2 stat-team'):wikitext('Team') | |||
th:tag('th'):addClass('stat-col'):wikitext('Events Counted') | |||
th:tag('th'):addClass('stat-col'):wikitext('Total Points') | |||
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') | |||
-- SMART LOGO FETCH | |||
local tD = tourneyDb[d.latest_tourney:lower() .. "|" .. d.team:lower()] | |||
local mD = masterDb[d.team:lower()] | |||
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'):css('font-size','0.95em'):css('font-weight','700'):wikitext('[[' .. d.team .. ']]') | |||
tr:tag('td'):addClass('stat-col'):wikitext(d.events) | |||
tr:tag('td'):addClass('stat-col'):css('font-weight','800'):css('color','#00509d'):css('font-size','1.1em'):wikitext(string.format("%.2f", d.points)) | |||
end | |||
elseif statType == "player" then | |||
local list = {} | local list = {} | ||
for _, d in pairs(pData) do if d.points > 0 then table.insert(list, d) end end | for _, d in pairs(pData) do if d.points > 0 then table.insert(list, d) end end | ||
| Line 181: | Line 243: | ||
local logoCell = tr:tag('td'):addClass('sticky-col sticky-logo') | local logoCell = tr:tag('td'):addClass('sticky-col sticky-logo') | ||
local pCell = tr:tag('td'):addClass('sticky-col sticky-player stat-team team-name-cell') | 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') | local infoDiv = pCell:tag('div'):css('line-height','1.1') | ||
infoDiv:tag('div'):addClass('player-name-txt'):wikitext('[[' .. d.player .. ']]') | infoDiv:tag('div'):addClass('player-name-txt'):wikitext('[[' .. d.player .. ']]') | ||
if d.team and d.team ~= "" then | if d.team and d.team ~= "" then | ||
local tD = tourneyDb[d.latest_tourney:lower() .. "|" .. d.team:lower()] | |||
local mD = masterDb[d.team:lower()] | |||
logoCell:wikitext(buildLogo(d.team, tD, mD)) | |||
infoDiv:tag('div'):css('font-size','0.7em'):css('color','var(--text-muted)'):css('text-transform','uppercase'):css('font-weight','700'):wikitext('[[' .. d.team .. ']]') | infoDiv:tag('div'):css('font-size','0.7em'):css('color','var(--text-muted)'):css('text-transform','uppercase'):css('font-weight','700'):wikitext('[[' .. d.team .. ']]') | ||
else | else | ||
logoCell:wikitext(buildLogo("", nil, nil)) -- Shield | |||
infoDiv:tag('div'):css('font-size','0.7em'):css('color','var(--text-muted)'):css('text-transform','uppercase'):css('font-weight','700'):css('opacity','0.5'):wikitext('Free Agent') | infoDiv:tag('div'):css('font-size','0.7em'):css('color','var(--text-muted)'):css('text-transform','uppercase'):css('font-weight','700'):css('opacity','0.5'):wikitext('Free Agent') | ||
end | end | ||
Revision as of 14:03, 22 May 2026
Documentation for this module may be created at Module:Rankings/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
-- Safely converts YYYY-MM-DD into a Unix Timestamp
local function getTimestamps(dateStr)
if not dateStr or dateStr == "" then return 0 end
local y, m, d = dateStr:match("(%d+)-(%d+)-(%d+)")
if not y then return 0 end
-- Set to noon to avoid timezone shift edge cases
return os.time({year=y, month=m, day=d, hour=12, min=0, sec=0})
end
-- Smart Logo Builder (Checks Tournament_Teams -> Teams -> Shield)
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
elseif teamName and teamName ~= "" then
lightFile = teamName:gsub("'", "") .. ".png"; darkFile = teamName:gsub("'", "") .. "_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
-- Team Matrix Points
local function getTeamBasePts(tier, rank)
local t = (tier or ""):lower()
local col = 0
if t:find("publisher") then col = 1
elseif t:find("tier 1") then col = 2
elseif t:find("tier 2") then col = 3
elseif t:find("tier 3") then col = 4
else return 0 end
if rank == 1 then return ({1000, 800, 600, 400})[col]
elseif rank == 2 then return ({800, 700, 500, 350})[col]
elseif rank == 3 then return ({700, 600, 400, 300})[col]
elseif rank == 4 then return ({600, 500, 300, 250})[col]
elseif rank == 5 then return ({500, 400, 250, 200})[col]
elseif rank >= 6 and rank <= 10 then return ({400, 300, 200, 150})[col]
elseif rank >= 11 and rank <= 20 then return ({300, 200, 150, 100})[col]
elseif rank >= 21 and rank <= 30 then return ({200, 100, 75, 50})[col]
elseif rank >= 31 and rank <= 48 then return ({100, 50, 35, 25})[col]
end return 0
end
local function getTeamDecay(days)
if days <= 180 then return 1
elseif days <= 270 then return 0.75
elseif days <= 365 then return 0.5
elseif days <= 1095 then return 0.1
else return 0 end
end
local function getPlayerDecay(days)
if days <= 180 then return 1
elseif days <= 240 then return 0.75
elseif days <= 300 then return 0.5
elseif days <= 365 then return 0.25
elseif days <= 1095 then return 0.1
else return 0 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 limit = tonumber(args.limit) or 100
-- ==========================================
-- TIME MACHINE LOGIC
-- ==========================================
local targetDateStr = args.as_of_date
local targetTs = os.time() -- Defaults to right now (Live Decay)
if targetDateStr and targetDateStr ~= "" then
local targetVal = getTimestamps(targetDateStr)
if targetVal > 0 then targetTs = targetVal end
end
local root = html.create('div'):addClass('standings-wrapper')
local tbl = root:tag('table'):addClass('standings-card-table stats-card-table')
local uniqueTeams = {}
local tData = {}
local pData = {}
if statType == "team" then
local results = cargo.query("RankingData_Team", "tournament, tier, end_date, team, rank", { orderBy = "end_date DESC", limit = 5000 })
if not results or #results == 0 then return 'No data.' end
for _, r in ipairs(results) do
local t = r.team
local endTs = getTimestamps(r.end_date)
-- If the event happened on or before our target date...
if t and t ~= "" and endTs > 0 and endTs <= targetTs then
if not tData[t] then
tData[t] = { team = t, points = 0, events = 0, latest_tourney = r.tournament }
uniqueTeams[t] = true
end
local diff = os.difftime(targetTs, endTs)
local days = math.max(0, math.floor(diff / 86400))
local base = getTeamBasePts(r.tier, tonumber(r.rank) or 99)
local decay = getTeamDecay(days)
local finalPts = base * decay
if finalPts > 0 then
tData[t].points = tData[t].points + finalPts
tData[t].events = tData[t].events + 1
end
end
end
elseif statType == "player" then
local results = cargo.query("RankingData_Player", "tournament, tier, end_date, player, team, finishes, mvp_tourney, igl, survivor, mvp_finals, emerging", { orderBy = "end_date DESC", limit = 5000 })
if not results or #results == 0 then return 'No data.' end
for _, r in ipairs(results) do
local p = r.player
local endTs = getTimestamps(r.end_date)
if p and p ~= "" and endTs > 0 and endTs <= targetTs then
if not pData[p] then
pData[p] = { player = p, team = r.team, latest_tourney = r.tournament, points = 0, events = 0 }
if r.team and r.team ~= "" then uniqueTeams[r.team] = true end
else
if (not pData[p].team or pData[p].team == "") and r.team and r.team ~= "" then
pData[p].team = r.team
uniqueTeams[r.team] = true
end
end
local diff = os.difftime(targetTs, endTs)
local days = math.max(0, math.floor(diff / 86400))
local decay = getPlayerDecay(days)
local tierLow = (r.tier or ""):lower()
local mult = 1
if tierLow:find("publisher") then mult = 2 elseif tierLow:find("tier 1") then mult = 1.5 end
local base = (tonumber(r.finishes) or 0) * mult
if tonumber(r.mvp_tourney) == 1 then base = base + 20 end
if tonumber(r.igl) == 1 then base = base + 10 end
if tonumber(r.survivor) == 1 then base = base + 10 end
if tonumber(r.mvp_finals) == 1 then base = base + 10 end
if tonumber(r.emerging) == 1 then base = base + 5 end
local finalPts = base * decay
if finalPts > 0 then
pData[p].points = pData[p].points + finalPts
pData[p].events = pData[p].events + 1
end
end
end
end
-- ==========================================
-- DATABASE PRE-FETCH (Logos)
-- ==========================================
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 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
local tResults = cargo.query("Tournament_Teams", "tournament, team, image, image_dark", { where = "team IN (" .. teamSql .. ")", limit = 5000 })
if tResults then
for _, row in ipairs(tResults) do
tourneyDb[row.tournament:lower() .. "|" .. row.team:lower()] = row
end
end
end
-- ==========================================
-- RENDER TABLES
-- ==========================================
if statType == "team" then
local list = {}
for _, d in pairs(tData) do if d.points > 0 then table.insert(list, d) end end
table.sort(list, function(a, b) return a.points > b.points end)
local th = tbl:tag('tr')
th:tag('th'):addClass('sticky-col sticky-1'):wikitext('Rank')
th:tag('th'):addClass('sticky-col sticky-2 stat-team'):wikitext('Team')
th:tag('th'):addClass('stat-col'):wikitext('Events Counted')
th:tag('th'):addClass('stat-col'):wikitext('Total Points')
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')
-- SMART LOGO FETCH
local tD = tourneyDb[d.latest_tourney:lower() .. "|" .. d.team:lower()]
local mD = masterDb[d.team:lower()]
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'):css('font-size','0.95em'):css('font-weight','700'):wikitext('[[' .. d.team .. ']]')
tr:tag('td'):addClass('stat-col'):wikitext(d.events)
tr:tag('td'):addClass('stat-col'):css('font-weight','800'):css('color','#00509d'):css('font-size','1.1em'):wikitext(string.format("%.2f", d.points))
end
elseif statType == "player" then
local list = {}
for _, d in pairs(pData) do if d.points > 0 then table.insert(list, d) end end
table.sort(list, function(a, b) return a.points > b.points end)
local th = tbl:tag('tr')
th:tag('th'):addClass('sticky-col sticky-1'):wikitext('Rank')
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('Events Counted')
th:tag('th'):addClass('stat-col'):wikitext('Total Points')
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 logoCell = tr:tag('td'):addClass('sticky-col sticky-logo')
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 .. ']]')
if d.team and d.team ~= "" then
local tD = tourneyDb[d.latest_tourney:lower() .. "|" .. d.team:lower()]
local mD = masterDb[d.team:lower()]
logoCell:wikitext(buildLogo(d.team, tD, mD))
infoDiv:tag('div'):css('font-size','0.7em'):css('color','var(--text-muted)'):css('text-transform','uppercase'):css('font-weight','700'):wikitext('[[' .. d.team .. ']]')
else
logoCell:wikitext(buildLogo("", nil, nil)) -- Shield
infoDiv:tag('div'):css('font-size','0.7em'):css('color','var(--text-muted)'):css('text-transform','uppercase'):css('font-weight','700'):css('opacity','0.5'):wikitext('Free Agent')
end
tr:tag('td'):addClass('stat-col'):wikitext(d.events)
tr:tag('td'):addClass('stat-col'):css('font-weight','800'):css('color','#00509d'):css('font-size','1.1em'):wikitext(string.format("%.2f", d.points))
end
end
return tostring(root)
end
return p