Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

Module:BGMIHub: Difference between revisions

From eSportsAmaze
No edit summary
No edit summary
 
(2 intermediate revisions by the same user not shown)
Line 1: Line 1:
-- ================================================================
-- Module:BGMIHub v1
-- Sections:
--  1. Hero Banner  — live stats (tournaments, prize pool, teams, players)
--  2. Tournaments  — upcoming/ongoing + recent results
--  3. Teams        — active teams sorted by Krafton rank
--  4. Rankings      — top players + top teams by Krafton rank
--  5. Prize Money  — top 10 teams + top 10 players by earnings
-- ================================================================
local p    = {}
local p    = {}
local cargo = mw.ext.cargo
local cargo = mw.ext.cargo
Line 62: Line 52:
     local hdr = parent:tag('div'):addClass('hub-section-header')
     local hdr = parent:tag('div'):addClass('hub-section-header')
     local hl  = hdr:tag('div'):addClass('hub-section-hl')
     local hl  = hdr:tag('div'):addClass('hub-section-hl')
    hl:tag('span'):addClass('hub-section-icon'):wikitext(icon)
     hl:tag('h2'):addClass('hub-section-title'):wikitext(title)
     hl:tag('h2'):addClass('hub-section-title'):wikitext(title)
     if subtitle then
     if subtitle then
Line 109: Line 98:
     -- 3. Tournaments — recent results (completed, end_date < today)
     -- 3. Tournaments — recent results (completed, end_date < today)
     local recentRows = cargo.query("Tournaments",
     local recentRows = cargo.query("Tournaments",
         "name,tier,end_date,prize_pool,winner,runner_up,image,image_dark",
         "name,tier,end_date,prize_pool,image,image_dark",
         { where  = "game='"..GAME.."' AND end_date<'"..today.."' AND end_date!=''",
         { where  = "game='"..GAME.."' AND end_date<'"..today.."' AND end_date!=''",
           orderBy= "end_date DESC",
           orderBy= "end_date DESC",
           limit  = 8 })
           limit  = 8 })
     recentRows = recentRows or {}
     recentRows = recentRows or {}
    -- Fetch winner + runner_up from PrizeMoney for each recent tournament
    local recentTournNames = {}
    for _,t in ipairs(recentRows) do
        table.insert(recentTournNames, "'"..esc(t.name).."'")
    end
    local podiumMap = {}  -- tournament -> { winner, runner_up }
    if #recentTournNames > 0 then
        local podiumRows = cargo.query("PrizeMoney", "tournament,team,placement",
            { where  = "tournament IN ("..table.concat(recentTournNames,",")..")"
                        .." AND (player='' OR player IS NULL)"
                        .." AND (placement='1' OR placement='2')",
              orderBy = "tournament, placement ASC",
              limit  = 50 })
        if podiumRows then
            for _,r in ipairs(podiumRows) do
                if not podiumMap[r.tournament] then
                    podiumMap[r.tournament] = {}
                end
                if r.placement == "1" then
                    podiumMap[r.tournament].winner = r.team
                elseif r.placement == "2" then
                    podiumMap[r.tournament].runner_up = r.team
                end
            end
        end
    end


     -- 4. Active teams with Krafton rank
     -- 4. Active teams with Krafton rank
Line 222: Line 238:
     local function statCard(val, label, icon)
     local function statCard(val, label, icon)
         local card = stats:tag('div'):addClass('hub-stat-card')
         local card = stats:tag('div'):addClass('hub-stat-card')
        card:tag('div'):addClass('hub-stat-icon'):wikitext(icon)
         card:tag('div'):addClass('hub-stat-val'):wikitext(val)
         card:tag('div'):addClass('hub-stat-val'):wikitext(val)
         card:tag('div'):addClass('hub-stat-label'):wikitext(label)
         card:tag('div'):addClass('hub-stat-label'):wikitext(label)
     end
     end
     statCard(tostring(totalTourns),  'Tournaments',    '🏟')
     statCard(tostring(totalTourns),  'Tournaments',    '')
     statCard(tostring(totalTeams),  'Active Teams',  '🛡')
     statCard(tostring(totalTeams),  'Active Teams',  '')
     statCard(tostring(totalPlayers), 'Active Players', '🎮')
     statCard(tostring(totalPlayers), 'Active Players', '')
     statCard(fmtCurrency(totalPrize),'Prize Distributed','💰')
     statCard(fmtCurrency(totalPrize),'Prize Distributed','')


     -- ════ 2. TOURNAMENTS ═════════════════════════════════════════
     -- ════ 2. TOURNAMENTS ═════════════════════════════════════════
Line 299: Line 314:
         hdr:tag('th'):wikitext('Tier')
         hdr:tag('th'):wikitext('Tier')
         hdr:tag('th'):wikitext('Tournament')
         hdr:tag('th'):wikitext('Tournament')
         hdr:tag('th'):wikitext('🥇 Winner')
         hdr:tag('th'):wikitext('Winner')
         hdr:tag('th'):wikitext('🥈 Runner-up')
         hdr:tag('th'):wikitext('Runner-up')
         hdr:tag('th'):css('text-align','right'):wikitext('Prize Pool')
         hdr:tag('th'):css('text-align','right'):wikitext('Prize Pool')


Line 311: Line 326:
             else tierTd:wikitext("—") end
             else tierTd:wikitext("—") end
             tr:tag('td'):css('font-weight','600'):wikitext("[["..t.name.."]]")
             tr:tag('td'):css('font-weight','600'):wikitext("[["..t.name.."]]")
             tr:tag('td'):wikitext(t.winner and t.winner~="" and "[["..t.winner.."]]" or "—")
            local podium = podiumMap[t.name] or {}
             tr:tag('td'):wikitext(t.runner_up and t.runner_up~="" and "[["..t.runner_up.."]]" or "—")
            local w  = podium.winner    and podium.winner~=""    and podium.winner    or nil
            local ru = podium.runner_up and podium.runner_up~="" and podium.runner_up or nil
             tr:tag('td'):css('font-weight','600')
                :wikitext(and "[[BGMI/Teams/"..w .."|"..w .."]]" or "—")
             tr:tag('td')
                :wikitext(ru and "[[BGMI/Teams/"..ru.."|"..ru.."]]" or "—")
             tr:tag('td'):addClass('ac-prize')
             tr:tag('td'):addClass('ac-prize')
                 :wikitext(tonumber(t.prize_pool) and tonumber(t.prize_pool)>0
                 :wikitext(tonumber(t.prize_pool) and tonumber(t.prize_pool)>0
Line 321: Line 341:
     -- ════ 3. TEAMS ═══════════════════════════════════════════════
     -- ════ 3. TEAMS ═══════════════════════════════════════════════
     local teamsSection = root:tag('div'):addClass('hub-section')
     local teamsSection = root:tag('div'):addClass('hub-section')
     sectionHeader(teamsSection, '🛡', 'Teams',
     sectionHeader(teamsSection, '', 'Teams',
         'Active BGMI organisations, sorted by Krafton rank')
         'Active BGMI organisations, sorted by Krafton rank')


Line 359: Line 379:
     -- Player rankings
     -- Player rankings
     local pRankCol = rankCols:tag('div'):addClass('hub-rank-col')
     local pRankCol = rankCols:tag('div'):addClass('hub-rank-col')
     pRankCol:tag('div'):addClass('hub-rank-col-title'):wikitext('🎮 Top Players')
     pRankCol:tag('div'):addClass('hub-rank-col-title'):wikitext('Top Players')
     local pTbl = pRankCol:tag('table'):addClass('hub-rank-table')
     local pTbl = pRankCol:tag('table'):addClass('hub-rank-table')
     local ph = pTbl:tag('tr')
     local ph = pTbl:tag('tr')
Line 371: Line 391:
         -- Rank with medal for top 3
         -- Rank with medal for top 3
         local rankTd = tr:tag('td'):addClass('hub-rank-num')
         local rankTd = tr:tag('td'):addClass('hub-rank-num')
         local medal = r.rank=="1" and "🥇" or r.rank=="2" and "🥈"
         local rnum = tonumber(r.rank) or 99
            or r.rank=="3" and "🥉" or nil
        if rnum == 1 then rankTd:wikitext('🥇')
        if medal then rankTd:wikitext(medal)
        elseif rnum == 2 then rankTd:wikitext('🥈')
        elseif rnum == 3 then rankTd:wikitext('🥉')
         else rankTd:tag('span'):addClass('hub-rank-badge'):wikitext('#'..r.rank) end
         else rankTd:tag('span'):addClass('hub-rank-badge'):wikitext('#'..r.rank) end


Line 385: Line 406:
     -- Team rankings
     -- Team rankings
     local tRankCol = rankCols:tag('div'):addClass('hub-rank-col')
     local tRankCol = rankCols:tag('div'):addClass('hub-rank-col')
     tRankCol:tag('div'):addClass('hub-rank-col-title'):wikitext('🛡 Top Teams')
     tRankCol:tag('div'):addClass('hub-rank-col-title'):wikitext('Top Teams')
     local tTbl = tRankCol:tag('table'):addClass('hub-rank-table')
     local tTbl = tRankCol:tag('table'):addClass('hub-rank-table')
     local th2 = tTbl:tag('tr')
     local th2 = tTbl:tag('tr')
Line 403: Line 424:
         local tr = tTbl:tag('tr')
         local tr = tTbl:tag('tr')
         local rankTd = tr:tag('td'):addClass('hub-rank-num')
         local rankTd = tr:tag('td'):addClass('hub-rank-num')
         local medal = rank==1 and "🥇" or rank==2 and "🥈"
         if rank == 1 then rankTd:wikitext('🥇')
            or rank==3 and "🥉" or nil
        elseif rank == 2 then rankTd:wikitext('🥈')
        if medal then rankTd:wikitext(medal)
        elseif rank == 3 then rankTd:wikitext('🥉')
         else rankTd:tag('span'):addClass('hub-rank-badge'):wikitext('#'..rank) end
         else rankTd:tag('span'):addClass('hub-rank-badge'):wikitext('#'..rank) end
         local dispName=(t.display_name and t.display_name~="") and t.display_name or t.name
         local dispName=(t.display_name and t.display_name~="") and t.display_name or t.name
Line 421: Line 442:
     -- Team earnings
     -- Team earnings
     local teCol = prizeCols:tag('div'):addClass('hub-rank-col')
     local teCol = prizeCols:tag('div'):addClass('hub-rank-col')
     teCol:tag('div'):addClass('hub-rank-col-title'):wikitext('🛡 Top Teams')
     teCol:tag('div'):addClass('hub-rank-col-title'):wikitext('Top Teams')
     local teTbl = teCol:tag('table'):addClass('hub-rank-table')
     local teTbl = teCol:tag('table'):addClass('hub-rank-table')
     local teh = teTbl:tag('tr')
     local teh = teTbl:tag('tr')

Latest revision as of 05:29, 9 March 2026

Documentation for this module may be created at Module:BGMIHub/doc

local p     = {}
local cargo = mw.ext.cargo
local html  = mw.html

-- ── Helpers ──────────────────────────────────────────────────────

local function esc(s)
    if not s then return "" end
    return s:gsub("\\","\\\\"):gsub("'","\\'")
end
local function clean(s)
    if not s then return nil end
    local c = s:gsub("[\r\n]",""):gsub("^%s*(.-)%s*$","%1")
    return c ~= "" and c or nil
end
local function fmtCurrency(n)
    if not n or n == 0 then return "—" end
    local num = math.floor(tonumber(n) or 0)
    local s   = tostring(num)
    if #s <= 3 then return "₹ " .. s end
    local result = s:sub(-3)
    local rem    = s:sub(1,-4)
    while #rem > 2 do result = rem:sub(-2)..","..result; rem = rem:sub(1,-3) end
    return "₹ " .. rem .. "," .. result
end
local function fmtNumber(n)
    -- Simple comma formatting for integers
    local num = math.floor(tonumber(n) or 0)
    local s   = tostring(num)
    local result = ""
    local len = #s
    for i = 1, len do
        if i > 1 and (len - i + 1) % 3 == 0 then result = result .. "," end
        result = result .. s:sub(i,i)
    end
    return result
end
local function getTierClass(tier)
    if not tier then return "tier-def" end
    local t = tier:lower()
    if t:find("s") and t:find("tier") then return "tier-s" end
    if t:find("a") and t:find("tier") then return "tier-a" end
    if t:find("b") and t:find("tier") then return "tier-b" end
    if t:find("c") and t:find("tier") then return "tier-c" end
    return "tier-def"
end

local GAME = "BGMI"

-- ── Section header builder ────────────────────────────────────────
local function sectionHeader(parent, icon, title, subtitle)
    local hdr = parent:tag('div'):addClass('hub-section-header')
    local hl  = hdr:tag('div'):addClass('hub-section-hl')
    hl:tag('h2'):addClass('hub-section-title'):wikitext(title)
    if subtitle then
        hdr:tag('div'):addClass('hub-section-sub'):wikitext(subtitle)
    end
end

-- ── Main ─────────────────────────────────────────────────────────

function p.main(frame)

    -- ════════════════════════════════════════════════════════════
    -- DATA FETCHING
    -- ════════════════════════════════════════════════════════════

    -- 1. Hero stats
    local totalTeamsRows = cargo.query("Teams","COUNT(name)=n",
        {where="game='"..GAME.."' AND status='Active'", limit=1})
    local totalTeams = (totalTeamsRows and #totalTeamsRows>0)
        and (tonumber(totalTeamsRows[1].n) or 0) or 0

    local totalPlayersRows = cargo.query("Players","COUNT(id)=n",
        {where="game='"..GAME.."' AND status='Active'", limit=1})
    local totalPlayers = (totalPlayersRows and #totalPlayersRows>0)
        and (tonumber(totalPlayersRows[1].n) or 0) or 0

    local totalTournsRows = cargo.query("Tournaments","COUNT(name)=n",
        {where="game='"..GAME.."'", limit=1})
    local totalTourns = (totalTournsRows and #totalTournsRows>0)
        and (tonumber(totalTournsRows[1].n) or 0) or 0

    local totalPrizeRows = cargo.query("PrizeMoney","SUM(prize)=total",
        {where="player='' OR player IS NULL", limit=1})
    local totalPrize = (totalPrizeRows and #totalPrizeRows>0)
        and (tonumber(totalPrizeRows[1].total) or 0) or 0

    -- 2. Tournaments — ongoing/upcoming (end_date >= today or no end_date)
    local today = os.date("%Y-%m-%d")
    local upcomingRows = cargo.query("Tournaments",
        "name,tier,start_date,end_date,prize_pool,image,image_dark,type,location",
        { where  = "game='"..GAME.."' AND (end_date>='"..today.."' OR end_date IS NULL OR end_date='')",
          orderBy= "start_date ASC",
          limit  = 6 })
    upcomingRows = upcomingRows or {}

    -- 3. Tournaments — recent results (completed, end_date < today)
    local recentRows = cargo.query("Tournaments",
        "name,tier,end_date,prize_pool,image,image_dark",
        { where  = "game='"..GAME.."' AND end_date<'"..today.."' AND end_date!=''",
          orderBy= "end_date DESC",
          limit  = 8 })
    recentRows = recentRows or {}

    -- Fetch winner + runner_up from PrizeMoney for each recent tournament
    local recentTournNames = {}
    for _,t in ipairs(recentRows) do
        table.insert(recentTournNames, "'"..esc(t.name).."'")
    end
    local podiumMap = {}  -- tournament -> { winner, runner_up }
    if #recentTournNames > 0 then
        local podiumRows = cargo.query("PrizeMoney", "tournament,team,placement",
            { where   = "tournament IN ("..table.concat(recentTournNames,",")..")"
                        .." AND (player='' OR player IS NULL)"
                        .." AND (placement='1' OR placement='2')",
              orderBy = "tournament, placement ASC",
              limit   = 50 })
        if podiumRows then
            for _,r in ipairs(podiumRows) do
                if not podiumMap[r.tournament] then
                    podiumMap[r.tournament] = {}
                end
                if r.placement == "1" then
                    podiumMap[r.tournament].winner = r.team
                elseif r.placement == "2" then
                    podiumMap[r.tournament].runner_up = r.team
                end
            end
        end
    end

    -- 4. Active teams with Krafton rank
    local teamRows = cargo.query("Teams",
        "name,display_name,short_code,image,image_dark,country,status",
        { where  = "game='"..GAME.."' AND status='Active'",
          orderBy= "name ASC",
          limit  = 50 })
    teamRows = teamRows or {}

    -- Fetch Krafton ranks for teams
    local teamRankMap = {}
    local allTeamRankRows = cargo.query("Krafton_Rankings","name,rank",
        {where="type='Team'", limit=100})
    if allTeamRankRows then
        for _,r in ipairs(allTeamRankRows) do
            teamRankMap[r.name] = tonumber(r.rank) or 999
        end
    end
    -- Sort teams by Krafton rank
    table.sort(teamRows, function(a,b)
        local ra = teamRankMap[a.name] or 999
        local rb = teamRankMap[b.name] or 999
        return ra < rb
    end)

    -- 5. Player Krafton rankings (top 10)
    local playerRankRows = cargo.query("Krafton_Rankings","name,rank",
        {where="type='Player'", orderBy="rank ASC", limit=10})
    playerRankRows = playerRankRows or {}

    -- Fetch player details for ranked players
    local rankedPlayerNames = {}
    for _,r in ipairs(playerRankRows) do
        table.insert(rankedPlayerNames,"'"..esc(r.name).."'")
    end
    local playerDetailMap = {}
    if #rankedPlayerNames>0 then
        local pdRows = cargo.query("Players","id,real_name,current_team,image,_pageName",
            {where="id IN ("..table.concat(rankedPlayerNames,",")..")", limit=20})
        if pdRows then
            for _,d in ipairs(pdRows) do
                playerDetailMap[d.id]={
                    real_name=d.real_name, team=d.current_team,
                    image=d.image, link=d._pageName
                }
            end
        end
    end

    -- 6. Team earnings leaderboard (top 8)
    local teamEarnRows = cargo.query("PrizeMoney","team,SUM(prize)=total",
        { where   = "player='' OR player IS NULL",
          groupBy = "team",
          orderBy = "total DESC",
          limit   = 8 })
    teamEarnRows = teamEarnRows or {}

    -- 7. Player earnings leaderboard (top 8)
    local playerEarnRows = cargo.query("PrizeMoney","player,SUM(prize)=total",
        { where   = "player!='' AND player IS NOT NULL",
          groupBy = "player",
          orderBy = "total DESC",
          limit   = 8 })
    playerEarnRows = playerEarnRows or {}

    -- Fetch player IDs for earnings leaderboard
    local earnPlayerNames = {}
    for _,r in ipairs(playerEarnRows) do
        if r.player and r.player~="" then
            table.insert(earnPlayerNames,"'"..esc(r.player).."'")
        end
    end
    local earnPlayerMap = {}
    if #earnPlayerNames>0 then
        local epRows = cargo.query("Players","id,real_name,current_team,_pageName",
            {where="_pageName IN ("..table.concat(earnPlayerNames,",")..")", limit=20})
        if epRows then
            for _,d in ipairs(epRows) do
                earnPlayerMap[d._pageName]={
                    id=d.id, real_name=d.real_name,
                    team=d.current_team, link=d._pageName
                }
            end
        end
    end

    -- ════════════════════════════════════════════════════════════
    -- RENDER
    -- ════════════════════════════════════════════════════════════

    local root = html.create('div'):addClass('hub-root')

    -- ════ 1. HERO ════════════════════════════════════════════════
    local hero = root:tag('div'):addClass('hub-hero')
    local heroInner = hero:tag('div'):addClass('hub-hero-inner')

    -- Left: title + description
    local heroLeft = heroInner:tag('div'):addClass('hub-hero-left')
    heroLeft:tag('div'):addClass('hub-hero-game-tag'):wikitext('ESPORTS WIKI')
    heroLeft:tag('h1'):addClass('hub-hero-title'):wikitext('BGMI')
    heroLeft:tag('div'):addClass('hub-hero-full'):wikitext('Battlegrounds Mobile India')
    heroLeft:tag('div'):addClass('hub-hero-desc')
        :wikitext('The complete resource for competitive BGMI — teams, players, tournaments and prize money all in one place.')

    -- Right: live stat cards
    local stats = heroInner:tag('div'):addClass('hub-hero-stats')
    local function statCard(val, label, icon)
        local card = stats:tag('div'):addClass('hub-stat-card')
        card:tag('div'):addClass('hub-stat-val'):wikitext(val)
        card:tag('div'):addClass('hub-stat-label'):wikitext(label)
    end
    statCard(tostring(totalTourns),  'Tournaments',    '')
    statCard(tostring(totalTeams),   'Active Teams',   '')
    statCard(tostring(totalPlayers), 'Active Players', '')
    statCard(fmtCurrency(totalPrize),'Prize Distributed','')

    -- ════ 2. TOURNAMENTS ═════════════════════════════════════════
    local tourns = root:tag('div'):addClass('hub-section')
    sectionHeader(tourns, '', 'Tournaments', 'Ongoing, upcoming and recent results')

    -- Upcoming / Ongoing
    if #upcomingRows>0 then
        tourns:tag('div'):addClass('hub-sub-title'):wikitext('Ongoing & Upcoming')
        local tGrid = tourns:tag('div'):addClass('hub-tourn-grid')
        for _,t in ipairs(upcomingRows) do
            local card = tGrid:tag('div'):addClass('hub-tourn-card')

            -- Tier ribbon
            local tier = t.tier or ""
            if tier~="" then
                card:tag('div'):addClass('hub-tourn-tier')
                    :tag('span'):addClass('tier-badge '..getTierClass(tier)):wikitext(tier)
            end

            -- Logo
            local lightL = (t.image and t.image~="") and t.image or "Tournament_Placeholder.png"
            local darkL  = (t.image_dark and t.image_dark~="") and t.image_dark or lightL
            local logoW  = card:tag('div'):addClass('hub-tourn-logo')
            logoW:wikitext("[[File:"..lightL.."|60px|link=|class=logo-lightmode]]")
            logoW:wikitext("[[File:"..darkL .."|60px|link=|class=logo-darkmode]]")

            -- Info
            local info = card:tag('div'):addClass('hub-tourn-info')
            info:tag('div'):addClass('hub-tourn-name')
                :wikitext("[["..t.name.."|"..t.name.."]]")

            -- Dates
            local dateStr = ""
            if t.start_date and t.start_date~="" then
                dateStr = t.start_date
                if t.end_date and t.end_date~="" then
                    dateStr = dateStr.." → "..t.end_date
                end
            end
            if dateStr~="" then
                info:tag('div'):addClass('hub-tourn-dates'):wikitext(dateStr)
            end

            -- Prize + location row
            local meta = info:tag('div'):addClass('hub-tourn-meta')
            if t.prize_pool and tonumber(t.prize_pool) and tonumber(t.prize_pool)>0 then
                meta:tag('span'):addClass('hub-tourn-prize')
                    :wikitext('💰 '..fmtCurrency(tonumber(t.prize_pool)))
            end
            if t.location and t.location~="" then
                meta:tag('span'):addClass('hub-tourn-loc'):wikitext('📍 '..t.location)
            end

            -- Live badge if ongoing
            if t.start_date and t.start_date<=today
               and (not t.end_date or t.end_date=="" or t.end_date>=today) then
                card:tag('div'):addClass('hub-live-badge'):wikitext('LIVE')
            end
        end
    end

    -- Recent Results
    if #recentRows>0 then
        tourns:tag('div'):addClass('hub-sub-title hub-sub-title-gap'):wikitext('Recent Results')
        local tbl = tourns:tag('table'):addClass('hub-results-table')
        local hdr = tbl:tag('tr')
        hdr:tag('th'):wikitext('Date')
        hdr:tag('th'):wikitext('Tier')
        hdr:tag('th'):wikitext('Tournament')
        hdr:tag('th'):wikitext('Winner')
        hdr:tag('th'):wikitext('Runner-up')
        hdr:tag('th'):css('text-align','right'):wikitext('Prize Pool')

        for _,t in ipairs(recentRows) do
            local tr = tbl:tag('tr')
            tr:tag('td'):addClass('ac-date'):wikitext(t.end_date or "—")
            local tierTd = tr:tag('td'):addClass('ac-tier')
            if t.tier and t.tier~="" then
                tierTd:tag('span'):addClass('tier-badge '..getTierClass(t.tier)):wikitext(t.tier)
            else tierTd:wikitext("—") end
            tr:tag('td'):css('font-weight','600'):wikitext("[["..t.name.."]]")
            local podium = podiumMap[t.name] or {}
            local w  = podium.winner    and podium.winner~=""    and podium.winner    or nil
            local ru = podium.runner_up and podium.runner_up~="" and podium.runner_up or nil
            tr:tag('td'):css('font-weight','600')
                :wikitext(w  and "[[BGMI/Teams/"..w .."|"..w .."]]" or "—")
            tr:tag('td')
                :wikitext(ru and "[[BGMI/Teams/"..ru.."|"..ru.."]]" or "—")
            tr:tag('td'):addClass('ac-prize')
                :wikitext(tonumber(t.prize_pool) and tonumber(t.prize_pool)>0
                    and fmtCurrency(tonumber(t.prize_pool)) or "—")
        end
    end

    -- ════ 3. TEAMS ═══════════════════════════════════════════════
    local teamsSection = root:tag('div'):addClass('hub-section')
    sectionHeader(teamsSection, '', 'Teams',
        'Active BGMI organisations, sorted by Krafton rank')

    local teamGrid = teamsSection:tag('div'):addClass('hub-team-grid')
    for _,t in ipairs(teamRows) do
        local card = teamGrid:tag('div'):addClass('hub-team-card')
        local rank = teamRankMap[t.name]

        -- Rank badge
        if rank and rank<999 then
            card:tag('div'):addClass('hub-team-rank'):wikitext('#'..rank)
        end

        -- Logo
        local lightL=(t.image and t.image~="") and t.image or "Shield_team.png"
        local darkL =(t.image_dark and t.image_dark~="") and t.image_dark or lightL
        local logoW = card:tag('div'):addClass('hub-team-logo')
        logoW:wikitext("[[File:"..lightL.."|48px|link="..t.name.."|class=logo-lightmode]]")
        logoW:wikitext("[[File:"..darkL .."|48px|link="..t.name.."|class=logo-darkmode]]")

        -- Name
        local dispName = (t.display_name and t.display_name~="")
            and t.display_name or t.name
        card:tag('div'):addClass('hub-team-name')
            :wikitext("[["..t.name.."|"..dispName.."]]")
        if t.short_code and t.short_code~="" then
            card:tag('div'):addClass('hub-team-tag'):wikitext(t.short_code)
        end
    end

    -- ════ 4. RANKINGS ════════════════════════════════════════════
    local rankSection = root:tag('div'):addClass('hub-section hub-rankings')
    sectionHeader(rankSection, '', 'Krafton Rankings', 'Official BGMI rankings')

    local rankCols = rankSection:tag('div'):addClass('hub-rank-cols')

    -- Player rankings
    local pRankCol = rankCols:tag('div'):addClass('hub-rank-col')
    pRankCol:tag('div'):addClass('hub-rank-col-title'):wikitext('Top Players')
    local pTbl = pRankCol:tag('table'):addClass('hub-rank-table')
    local ph = pTbl:tag('tr')
    ph:tag('th'):wikitext('Rank')
    ph:tag('th'):wikitext('Player')
    ph:tag('th'):wikitext('Team')

    for _,r in ipairs(playerRankRows) do
        local d = playerDetailMap[r.name] or {}
        local tr = pTbl:tag('tr')
        -- Rank with medal for top 3
        local rankTd = tr:tag('td'):addClass('hub-rank-num')
        local rnum = tonumber(r.rank) or 99
        if rnum == 1 then rankTd:wikitext('🥇')
        elseif rnum == 2 then rankTd:wikitext('🥈')
        elseif rnum == 3 then rankTd:wikitext('🥉')
        else rankTd:tag('span'):addClass('hub-rank-badge'):wikitext('#'..r.rank) end

        local link = d.link or r.name
        tr:tag('td'):css('font-weight','700')
            :wikitext("[["..link.."|"..r.name.."]]")
        tr:tag('td'):addClass('hub-rank-team')
            :wikitext(d.team and d.team~="" and "[["..d.team.."]]" or "—")
    end

    -- Team rankings
    local tRankCol = rankCols:tag('div'):addClass('hub-rank-col')
    tRankCol:tag('div'):addClass('hub-rank-col-title'):wikitext('Top Teams')
    local tTbl = tRankCol:tag('table'):addClass('hub-rank-table')
    local th2 = tTbl:tag('tr')
    th2:tag('th'):wikitext('Rank')
    th2:tag('th'):wikitext('Team')

    -- Show top 10 teams by rank
    local topTeams = {}
    for _,t in ipairs(teamRows) do
        if teamRankMap[t.name] and teamRankMap[t.name]<999 then
            table.insert(topTeams,t)
        end
        if #topTeams>=10 then break end
    end
    for _,t in ipairs(topTeams) do
        local rank = teamRankMap[t.name]
        local tr = tTbl:tag('tr')
        local rankTd = tr:tag('td'):addClass('hub-rank-num')
        if rank == 1 then rankTd:wikitext('🥇')
        elseif rank == 2 then rankTd:wikitext('🥈')
        elseif rank == 3 then rankTd:wikitext('🥉')
        else rankTd:tag('span'):addClass('hub-rank-badge'):wikitext('#'..rank) end
        local dispName=(t.display_name and t.display_name~="") and t.display_name or t.name
        tr:tag('td'):css('font-weight','700')
            :wikitext("[["..t.name.."|"..dispName.."]]")
    end

    -- ════ 5. PRIZE LEADERBOARD ═══════════════════════════════════
    local prizeSection = root:tag('div'):addClass('hub-section')
    sectionHeader(prizeSection, '', 'Prize Money Leaderboard',
        'All-time earnings from recorded tournaments')

    local prizeCols = prizeSection:tag('div'):addClass('hub-rank-cols')

    -- Team earnings
    local teCol = prizeCols:tag('div'):addClass('hub-rank-col')
    teCol:tag('div'):addClass('hub-rank-col-title'):wikitext('Top Teams')
    local teTbl = teCol:tag('table'):addClass('hub-rank-table')
    local teh = teTbl:tag('tr')
    teh:tag('th'):wikitext('#')
    teh:tag('th'):wikitext('Team')
    teh:tag('th'):css('text-align','right'):wikitext('Earnings')

    for i,r in ipairs(teamEarnRows) do
        local tr = teTbl:tag('tr')
        tr:tag('td'):addClass('hub-rank-num'):wikitext(tostring(i))
        tr:tag('td'):css('font-weight','700')
            :wikitext(r.team and r.team~="" and "[["..r.team.."]]" or "—")
        tr:tag('td'):addClass('ac-prize'):wikitext(fmtCurrency(tonumber(r.total) or 0))
    end

    -- Player earnings
    local peCol = prizeCols:tag('div'):addClass('hub-rank-col')
    peCol:tag('div'):addClass('hub-rank-col-title'):wikitext('Top Players')
    local peTbl = peCol:tag('table'):addClass('hub-rank-table')
    local peh = peTbl:tag('tr')
    peh:tag('th'):wikitext('#')
    peh:tag('th'):wikitext('Player')
    peh:tag('th'):css('text-align','right'):wikitext('Earnings')

    for i,r in ipairs(playerEarnRows) do
        local d = earnPlayerMap[r.player] or {}
        local dispId = d.id or (r.player or ""):gsub("^.*/","")
        local link   = d.link or r.player or ""
        local tr = peTbl:tag('tr')
        tr:tag('td'):addClass('hub-rank-num'):wikitext(tostring(i))
        tr:tag('td'):css('font-weight','700')
            :wikitext(link~="" and "[["..link.."|"..dispId.."]]" or dispId)
        tr:tag('td'):addClass('ac-prize'):wikitext(fmtCurrency(tonumber(r.total) or 0))
    end

    return tostring(root)
end

return p